/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "ags/shared/game/tra_file.h" #include "ags/shared/ac/words_dictionary.h" #include "ags/shared/debugging/out.h" #include "ags/shared/util/data_ext.h" #include "ags/shared/util/string_compat.h" #include "ags/shared/util/string_utils.h" namespace AGS3 { namespace AGS { namespace Shared { const char *TRASignature = "AGSTranslation"; String GetTraFileErrorText(TraFileErrorType err) { switch (err) { case kTraFileErr_NoError: return "No error."; case kTraFileErr_SignatureFailed: return "Not an AGS translation file or an unsupported format."; case kTraFileErr_FormatNotSupported: return "Format version not supported."; case kTraFileErr_GameIDMismatch: return "Game ID does not match, translation is meant for a different game."; case kTraFileErr_UnexpectedEOF: return "Unexpected end of file."; case kTraFileErr_UnknownBlockType: return "Unknown block type."; case kTraFileErr_BlockDataOverlapping: return "Block data overlapping."; default: return "Unknown error."; } } String GetTraBlockName(TraFileBlock id) { switch (id) { case kTraFblk_Dict: return "Dictionary"; case kTraFblk_GameID: return "GameID"; case kTraFblk_TextOpts: return "TextOpts"; default: return "unknown"; } } HError OpenTraFile(Stream *in) { // Test the file signature char sigbuf[16] = { 0 }; in->Read(sigbuf, 15); if (ags_stricmp(TRASignature, sigbuf) != 0) return new TraFileError(kTraFileErr_SignatureFailed); return HError::None(); } HError ReadTraBlock(Translation &tra, Stream *in, TraFileBlock block, const String &ext_id, soff_t /*block_len*/) { switch (block) { case kTraFblk_Dict: { std::vector buf; // Read lines until we find zero-length key & value while (true) { String src_line = read_string_decrypt(in, buf); String dst_line = read_string_decrypt(in, buf); if (src_line.IsEmpty() || dst_line.IsEmpty()) break; tra.Dict.insert(std::make_pair(src_line, dst_line)); } return HError::None(); } case kTraFblk_GameID: { char gamename[256]; tra.GameUid = in->ReadInt32(); read_string_decrypt(in, gamename, sizeof(gamename)); tra.GameName = gamename; return HError::None(); } case kTraFblk_TextOpts: tra.NormalFont = in->ReadInt32(); tra.SpeechFont = in->ReadInt32(); tra.RightToLeft = in->ReadInt32(); return HError::None(); case kTraFblk_None: // continue reading extensions with string ID break; default: return new TraFileError(kTraFileErr_UnknownBlockType, String::FromFormat("Type: %d, known range: %d - %d.", block, kTraFblk_Dict, kTraFblk_TextOpts)); } if (ext_id.CompareNoCase("ext_sopts") == 0) { StrUtil::ReadStringMap(tra.StrOptions, in); return HError::None(); } return new TraFileError(kTraFileErr_UnknownBlockType, String::FromFormat("Type: %s", ext_id.GetCStr())); } // TRABlockReader reads whole TRA data, block by block class TRABlockReader : public DataExtReader { public: TRABlockReader(Translation &tra, Stream *in) : DataExtReader(in, kDataExt_NumID32 | kDataExt_File32) , _tra(tra) { } // Reads only the Game ID block and stops HError ReadGameID() { HError err = FindOne(kTraFblk_GameID); if (!err) return err; return ReadTraBlock(_tra, _in, kTraFblk_GameID, "", _blockLen); } private: String GetOldBlockName(int block_id) const override { return GetTraBlockName((TraFileBlock)block_id); } soff_t GetOverLeeway(int block_id) const override { // TRA files made by pre-3.0 editors have a block length miscount by 1 byte if (block_id == kTraFblk_GameID) return 1; return 0; } HError ReadBlock(int block_id, const String &ext_id, soff_t block_len, bool &read_next) override { read_next = true; return ReadTraBlock(_tra, _in, (TraFileBlock)block_id, ext_id, block_len); } Translation &_tra; }; HError TestTraGameID(int game_uid, const String &game_name, Stream *in) { HError err = OpenTraFile(in); if (!err) return err; Translation tra; TRABlockReader reader(tra, in); err = reader.ReadGameID(); if (!err) return err; // Test the identifiers, if they are not present then skip the test if ((tra.GameUid != 0 && (game_uid != tra.GameUid)) || (!tra.GameName.IsEmpty() && (game_name != tra.GameName))) return new TraFileError(kTraFileErr_GameIDMismatch, String::FromFormat("The translation is designed for '%s'", tra.GameName.GetCStr())); return HError::None(); } HError ReadTraData(Translation &tra, Stream *in) { HError err = OpenTraFile(in); if (!err) return err; TRABlockReader reader(tra, in); return reader.Read(); } // TODO: perhaps merge with encrypt/decrypt utilities static const char *EncryptText(std::vector &en_buf, const String &s) { if (en_buf.size() < s.GetLength() + 1) en_buf.resize(s.GetLength() + 1); strncpy(&en_buf.front(), s.GetCStr(), s.GetLength() + 1); encrypt_text(&en_buf.front()); return &en_buf.front(); } // TODO: perhaps merge with encrypt/decrypt utilities static const char *EncryptEmptyString(std::vector &en_buf) { en_buf[0] = 0; encrypt_text(&en_buf.front()); return &en_buf.front(); } void WriteGameID(const Translation &tra, Stream *out) { std::vector en_buf; out->WriteInt32(tra.GameUid); StrUtil::WriteString(EncryptText(en_buf, tra.GameName), tra.GameName.GetLength() + 1, out); } void WriteDict(const Translation &tra, Stream *out) { std::vector en_buf; for (const auto &kv : tra.Dict) { const String &src = kv._key; const String &dst = kv._value; if (!dst.IsNullOrSpace()) { String unsrc = StrUtil::Unescape(src); String undst = StrUtil::Unescape(dst); StrUtil::WriteString(EncryptText(en_buf, unsrc), unsrc.GetLength() + 1, out); StrUtil::WriteString(EncryptText(en_buf, undst), undst.GetLength() + 1, out); } } // Write a pair of empty key/values StrUtil::WriteString(EncryptEmptyString(en_buf), 1, out); StrUtil::WriteString(EncryptEmptyString(en_buf), 1, out); } void WriteTextOpts(const Translation &tra, Stream *out) { out->WriteInt32(tra.NormalFont); out->WriteInt32(tra.SpeechFont); out->WriteInt32(tra.RightToLeft); } static const Translation *writer_tra; static void(*writer_writer)(const Translation &tra, Stream *out); void WriteStrOptions(const Translation &tra, Stream *out) { StrUtil::WriteStringMap(tra.StrOptions, out); } static void WriteTraBlockWriter(Stream *out) { writer_writer(*writer_tra, out); } inline void WriteTraBlock(const Translation &tra, TraFileBlock block, void(*writer)(const Translation &tra, Stream *out), Stream *out) { writer_tra = &tra; writer_writer = writer; WriteExtBlock(block, WriteTraBlockWriter, kDataExt_NumID32 | kDataExt_File32, out); } inline void WriteTraBlock(const Translation &tra, const String &ext_id, void(*writer)(const Translation &tra, Stream *out), Stream *out) { writer_tra = &tra; writer_writer = writer; WriteExtBlock(ext_id, WriteTraBlockWriter, kDataExt_NumID32 | kDataExt_File32, out); } void WriteTraData(const Translation &tra, Stream *out) { // Write header out->Write(TRASignature, strlen(TRASignature) + 1); // Write all blocks WriteTraBlock(tra, kTraFblk_GameID, WriteGameID, out); WriteTraBlock(tra, kTraFblk_Dict, WriteDict, out); WriteTraBlock(tra, kTraFblk_TextOpts, WriteTextOpts, out); WriteTraBlock(tra, "ext_sopts", WriteStrOptions, out); // Write ending out->WriteInt32(kTraFile_EOF); } } // namespace Shared } // namespace AGS } // namespace AGS3