unit gxtutils; interface uses SysUtils, Classes; type TGTAMessage = record key: AnsiString; guy: AnsiChar; msg: WideString; end; TGTAMessagesArray = array of TGTAMessage; function DecodeGXT(fs: TFileStream; silent: boolean = true): TGTAMessagesArray; function EncodeGXT(messages: TGTAMessagesArray; fsOut: TFileStream; lang: AnsiChar = '?'): string; implementation const MAX_KEY_SIZE = 8; GXT_MAGIC = 'GBL'; GXT_VER = 100; EU_Charset_Convert: array[0..48,0..1] of WideChar = ( (#$80, #$C0), (#$81, #$C1), (#$82, #$C2), (#$83, #$C4), (#$84, #$C6), (#$85, #$C7), (#$86, #$C8), (#$87, #$C9), (#$88, #$CA), (#$89, #$CB), (#$8A, #$CC), (#$8B, #$CD), (#$8C, #$CE), (#$8D, #$CF), (#$8E, #$D2), (#$8F, #$D3), (#$90, #$D4), (#$91, #$D6), (#$92, #$D9), (#$93, #$DA), (#$94, #$DB), (#$95, #$DC), (#$96, #$DF), (#$97, #$E0), (#$98, #$E1), (#$99, #$E2), (#$9A, #$E4), (#$9B, #$E6), (#$9C, #$E7), (#$9D, #$E8), (#$9E, #$E9), (#$9F, #$EA), (#$A0, #$EB), (#$A1, #$EC), (#$A2, #$ED), (#$A3, #$EE), (#$A4, #$EF), (#$A5, #$F2), (#$A6, #$F3), (#$A7, #$F4), (#$A8, #$F6), (#$A9, #$F9), (#$AA, #$FA), (#$AB, #$FB), (#$AC, #$FC), (#$AD, #$D1), (#$AE, #$F1), (#$AF, #$BF), (#$B0, #$A1) ); type TGXTHeader = packed record magic: array[0..2] of AnsiChar; // always "GBL" lang: AnsiChar; // E,G,F,I,S,J version: word // always 64 00 = 100 end; TGXTSectionHeader = packed record sectionKey: array[0..3] of AnsiChar; // TKEY or TDAT sectionLength: Cardinal; end; TKEY = packed record relDataOffset: Cardinal; key: array[0..MAX_KEY_SIZE-1] of AnsiChar; end; function ANSItoGTA2(s: WideString): WideString; var i, j: integer; begin result := s; for i := 1 to Length(s) do begin for j := Low(EU_Charset_Convert) to High(EU_Charset_Convert) do begin if result[i] = EU_Charset_Convert[j][1] then begin result[i] := EU_Charset_Convert[j][0]; break; end; end; end; end; function GTA2toANSI(s: WideString): WideString; var i, j: integer; begin result := s; for i := 1 to Length(s) do begin for j := Low(EU_Charset_Convert) to High(EU_Charset_Convert) do begin if result[i] = EU_Charset_Convert[j][0] then begin result[i] := EU_Charset_Convert[j][1]; break; end; end; end; end; function ZeroPad(s: string; len: integer): string; begin result := s; while Length(result) < len do result := result + #0; end; function CharToLang(c: AnsiChar): string; begin case c of 'E': result := 'English'; 'F': result := 'French'; 'G': result := 'German'; 'I': result := 'Italian'; 'J': result := 'Japanese'; 'S': result := 'Spanish'; else result := 'Unknown ('+c+')'; end; end; function EncodeGXT(messages: TGTAMessagesArray; fsOut: TFileStream; lang: AnsiChar = '?'): string; var i: integer; msg: WideString; guywc: WideChar; curoffset: Cardinal; curkey: AnsiString; msKey, msDat: TMemoryStream; gxtHead: TGXTHeader; secHead: TGXTSectionHeader; begin WriteLn('Lang: ' + CharToLang(lang)); WriteLn(''); msKey := TMemoryStream.Create; msDat := TMemoryStream.Create; try for i := Low(messages) to High(messages) do begin if Length(messages[i].key) > 7 then begin WriteLn('Attention: Line was ignored because key was too long: ' + messages[i].key); Continue; end; curoffset := msDat.Position; msKey.Write(curoffset, SizeOf(curoffset)); curkey := ZeroPad(messages[i].key, MAX_KEY_SIZE); msKey.Write(curkey[1], Length(curkey)*SizeOf(AnsiChar)); if messages[i].guy <> #0 then begin guywc := WideChar(Ord(messages[i].guy) + Ord('!') shl 8); msg := guywc + messages[i].msg; end else msg := messages[i].msg; msg := ANSItoGTA2(msg) + #0; msDat.Write(msg[1], Length(msg) * SizeOf(WideChar)); end; gxtHead.magic := GXT_MAGIC; gxtHead.lang := lang; gxtHead.version := GXT_VER; fsOut.Write(gxtHead, SizeOf(gxtHead)); secHead.sectionKey := 'TKEY'; secHead.sectionLength := msKey.Size; fsOut.Write(secHead, SizeOf(secHead)); msKey.Position := 0; fsOut.CopyFrom(msKey, msKey.Size); secHead.sectionKey := 'TDAT'; secHead.sectionLength := msDat.Size; fsOut.Write(secHead, SizeOf(secHead)); msDat.Position := 0; fsOut.CopyFrom(msDat, msDat.Size); finally msKey.Free; msDat.Free; end; end; function DecodeGXT(fs: TFileStream; silent: boolean = true): TGTAMessagesArray; var numEntries: integer; i: integer; filHead: TGXTHeader; tkAry: array of TKEY; tkLen: integer; secHead: TGXTSectionHeader; ws: WideString; extra: integer; p, op: PWideChar; ap: PAnsiChar; speaker: word; begin fs.Position := 0; fs.Read(filHead, SizeOf(filHead)); // Optional checks if filHead.magic <> GXT_MAGIC then begin WriteLn('ERROR: Magic header "'+GXT_MAGIC+'" missing.'); ExitCode := 1; Exit; end; if not silent then begin Writeln('Lang: ' + CharToLang(filHead.lang)); WriteLn(''); end; if filHead.version <> GXT_VER then begin WriteLn('ERROR: GXT version doesn''t has the value '+IntToStr(GXT_VER)+'.'); ExitCode := 1; Exit; end; fs.Seek(SizeOf(filHead), soFromBeginning); while fs.Position < fs.Size do begin fs.Read(secHead, SizeOf(secHead)); if (secHead.sectionKey <> 'TKEY') and (secHead.sectionKey <> 'TDAT') then begin WriteLn('Attention: Unknown section type '+secHead.sectionKey); end; fs.Seek(secHead.sectionLength, soCurrent); end; extra := fs.Position - fs.Size; if extra > 0 then begin WriteLn('Attention: Extra bytes at end of file: ' + IntToStr(extra)); end; // Step 1: Collect all keys fs.Seek(SizeOf(filHead), soFromBeginning); while fs.Position < fs.Size do begin fs.Read(secHead, SizeOf(secHead)); if secHead.sectionKey = 'TKEY' then Break; fs.Seek(secHead.sectionLength, soCurrent); end; if secHead.sectionKey <> 'TKEY' then begin WriteLn('ERROR: "TKEY" not found'); ExitCode := 1; Exit; end; tkLen := secHead.sectionLength; numEntries := tkLen div SizeOf(TKEY); SetLength(tkAry, numEntries); fs.Read(tkAry[0], tkLen); // Step 2: Collect all texts fs.Seek(SizeOf(filHead), soFromBeginning); while fs.Position < fs.Size do begin fs.Read(secHead, SizeOf(secHead)); if secHead.sectionKey = 'TDAT' then Break; fs.Seek(secHead.sectionLength, soCurrent); end; if secHead.sectionKey <> 'TDAT' then begin WriteLn('ERROR: "TDAT" not found'); ExitCode := 1; Exit; end; SetLength(ws, secHead.sectionLength div SizeOf(WideChar)); fs.Read(ws[1], secHead.sectionLength); // Step 3: Generating output "messages" Variable op := PWideChar(ws); SetLength(result, numEntries); for i := 0 to numEntries-1 do begin p := op + tkAry[i].relDataOffset div SizeOf(WideChar); result[i].msg := PWideChar(p); // Decode Speaker result[i].guy := #0; if result[i].msg <> '' then begin speaker := word(result[i].msg[1]); if Hi(speaker) = ord('!') then begin result[i].guy := Chr(Lo(speaker)); result[i].msg := Copy(result[i].msg, 2, Length(result[i].msg)-1); end; end; result[i].msg := GTA2toANSI(result[i].msg); ap := PAnsiChar(AnsiString(tkAry[i].key)); result[i].key := ap; end; end; end.