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 }