1 /// TalkTable (tlk)
2 module nwn.tlk;
3
4 import std.stdint;
5 import std.string;
6 import std.conv;
7 import std.traits: EnumMembers;
8
9 debug import std.stdio: writeln;
10 version(unittest) import std.exception: assertThrown, assertNotThrown;
11
12 public import nwn.constants: Language, LanguageGender;
13
14 class TlkOutOfBoundsException : Exception{
15 @safe pure nothrow this(string msg, string f=__FILE__, size_t l=__LINE__, Throwable t=null){
16 super(msg, f, l, t);
17 }
18 }
19
20 /// String ref base type
21 alias StrRef = uint32_t;
22
23 /// Utility class to resolve string refs using two TLKs
24 class StrRefResolver{
25 this(in Tlk standartTable, in Tlk userTable = null){
26 this.standartTable = standartTable;
27 this.userTable = userTable;
28 }
29
30 /// Get a localized string in the tlk tables (can be a standard or user strref)
31 string opIndex(in StrRef strref)const{
32 if(strref < Tlk.UserTlkIndexOffset){
33 assert(standartTable !is null, "standartTable is null");
34 return standartTable[strref];
35 }
36 else if(userTable !is null){
37 return userTable[strref-Tlk.UserTlkIndexOffset];
38 }
39 return "unknown_strref_" ~ strref.to!string;
40 }
41
42 const Tlk standartTable;
43 const Tlk userTable;
44 }
45 unittest{
46 auto resolv = new StrRefResolver(
47 new Tlk(cast(immutable ubyte[])import("dialog.tlk")),
48 new Tlk(cast(immutable ubyte[])import("user.tlk"))
49 );
50
51 enum string lastLine =
52 "Niveau(x) de lanceur de sorts : prêtre 1, paladin 1\n"
53 ~"Niveau inné : 1\n"
54 ~"Ecole : Evocation\n"
55 ~"Registre(s) : \n"
56 ~"Composante(s) : verbale, gestuelle\n"
57 ~"Portée : personnelle\n"
58 ~"Zone d'effet / cible : lanceur\n"
59 ~"Durée : 60 secondes\n"
60 ~"Jet de sauvegarde : aucun\n"
61 ~"Résistance à la magie : non\n"
62 ~"\n"
63 ~"Tous les trois niveaux effectifs de lanceur de sorts, vous gagnez un bonus de +1 à vos jets d'attaque et un bonus de +1 de dégâts magiques (minimum +1, maximum +3).";
64
65 assert(resolv.standartTable.language == Language.French);
66 assert(resolv[0] == "Bad Strref");
67 assert(resolv[54] == lastLine);
68 assertThrown!TlkOutOfBoundsException(resolv[55]);
69
70 assert(resolv[Tlk.UserTlkIndexOffset + 0] == "Hello world");
71 assert(resolv[Tlk.UserTlkIndexOffset + 1] == "Café liégeois");
72 }
73
74 /// TLK (read only)
75 class Tlk{
76 ///
77 this(Language langId, immutable(char[4]) tlkVersion = "V3.0"){
78 header.file_type = "TLK ";
79 header.file_version = tlkVersion;
80 }
81 ///
82 this(in string path){
83 import std.file: read;
84 this(cast(ubyte[])path.read());
85 }
86 ///
87 this(in ubyte[] rawData){
88 header = *cast(TlkHeader*)rawData.ptr;
89 strData = (cast(TlkStringData[])rawData[TlkHeader.sizeof .. header.string_entries_offset]).dup();
90 strEntries = cast(char[])rawData[header.string_entries_offset .. $];
91 }
92
93 /// tlk[strref]
94 string opIndex(in StrRef strref) const{
95 assert(strref < UserTlkIndexOffset, "Tlk indexes must be lower than "~UserTlkIndexOffset.to!string);
96
97 if(strref >= header.string_count)
98 throw new TlkOutOfBoundsException("strref "~strref.to!string~" out of bounds");
99
100 immutable data = strData[strref];
101 return cast(immutable)strEntries[data.offset_to_string .. data.offset_to_string + data.string_size];
102 }
103
104 /// Number of entries
105 @property size_t length() const{
106 return header.string_count;
107 }
108
109 /// foreach(text ; tlk)
110 int opApply(scope int delegate(in string) dlg) const{
111 int res = 0;
112 foreach(ref data ; strData){
113 res = dlg(cast(immutable)strEntries[data.offset_to_string .. data.offset_to_string + data.string_size]);
114 if(res != 0) break;
115 }
116 return res;
117 }
118 /// foreach(strref, text ; tlk)
119 int opApply(scope int delegate(StrRef, in string) dlg) const{
120 int res = 0;
121 foreach(i, ref data ; strData){
122 res = dlg(cast(StrRef)i, cast(immutable)strEntries[data.offset_to_string .. data.offset_to_string + data.string_size]);
123 if(res != 0) break;
124 }
125 return res;
126 }
127
128 @property{
129 /// TLK language ID
130 Language language() const{
131 return cast(Language)(header.language_id);
132 }
133 }
134
135 enum UserTlkIndexOffset = 16777216;
136
137 private:
138 TlkHeader header;
139 TlkStringData[] strData;
140 char[] strEntries;
141
142
143 align(1) struct TlkHeader{
144 char[4] file_type;
145 char[4] file_version;
146 uint32_t language_id;
147 uint32_t string_count;
148 uint32_t string_entries_offset;
149 }
150 align(1) struct TlkStringData{
151 uint32_t flags;
152 char[16] sound_resref;
153 uint32_t _volume_variance;
154 uint32_t _pitch_variance;
155 uint32_t offset_to_string;
156 uint32_t string_size;
157 float sound_length;
158 }
159
160 enum StringFlag{
161 TEXT_PRESENT=0x1,
162 SND_PRESENT=0x2,
163 SNDLENGTH_PRESENT=0x4,
164 }
165 }