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 }