/* 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 "engines/myst3/database.h" #include "common/archive.h" #include "common/debug.h" #include "common/substream.h" namespace Myst3 { /** * An abstract node transformation. * * Subclasses can read data related to a node from a stream in read, * and transform a node using that data in apply. */ class NodeTransform { public : virtual ~NodeTransform() {}; virtual void read(Common::SeekableReadStream *file) = 0; virtual void apply(NodePtr &node) = 0; }; /** * A node transformation that reads hotspots and scripts, * and adds them to a node. */ class NodeTransformAddHotspots : public NodeTransform { public : NodeTransformAddHotspots(); void read(Common::SeekableReadStream *file) override; void apply(NodePtr &node) override; private: int32 _zipBitIndex; Common::Array _scripts; Common::Array _hotspots; }; /** * A node transformation that reads scripts, and adds them to a node's * sound scripts. */ class NodeTransformAddSoundScripts : public NodeTransform { public : void read(Common::SeekableReadStream *file) override; void apply(NodePtr &node) override; private: Common::Array _scripts; }; /** * A node transformation that reads scripts, and adds them to a node's * background sound scripts. */ class NodeTransformAddBackgroundSoundScripts : public NodeTransform { public : void read(Common::SeekableReadStream *file) override; void apply(NodePtr &node) override; private: Common::Array _scripts; }; /** * Walks through a stream of nodes. For each encountered node, a NodeTransformer is called. */ class NodeWalker { public : NodeWalker(NodeTransform *transform); ~NodeWalker(); void read(Common::SeekableReadStream *file, Common::Array &allNodes, bool createMissingSharedNodes); private: NodeTransform *_transform; }; /** * A collection of functions used to read script related data */ class ScriptData { public: static Common::Array readCondScripts(Common::SeekableReadStream &s); static Common::Array readOpcodes(Common::ReadStream &s); static Common::Array readHotspots(Common::ReadStream &s); static Common::Array readRects(Common::ReadStream &s); static CondScript readCondScript(Common::SeekableReadStream &s); static HotSpot readHotspot(Common::ReadStream &s); private: ScriptData() {}; }; Common::Array ScriptData::readRects(Common::ReadStream &s) { Common::Array rects; bool lastRect = false; do { PolarRect rect; rect.centerPitch = s.readUint16LE(); rect.centerHeading = s.readUint16LE(); rect.width = s.readUint16LE(); rect.height = s.readUint16LE(); if (rect.width < 0) { rect.width = -rect.width; } else { lastRect = true; } rects.push_back(rect); } while (!lastRect && !s.eos()); return rects; } Common::Array ScriptData::readOpcodes(Common::ReadStream &s) { Common::Array script; while (!s.eos()) { Opcode opcode; uint16 code = s.readUint16LE(); opcode.op = code & 0xff; uint8 count = code >> 8; if (count == 0 && opcode.op == 0) break; for (int i = 0; i < count; i++) { int16 value = s.readSint16LE(); opcode.args.push_back(value); } script.push_back(opcode); } return script; } CondScript ScriptData::readCondScript(Common::SeekableReadStream &s) { CondScript script; script.condition = s.readUint16LE(); if(!script.condition) return script; // WORKAROUND: Original data bug in MATO 32765 // The script data for node MATO 32765 is missing its first two bytes // of data, resulting in incorrect opcodes being read // Original disassembly: // init 0 > c[v565 != 0] // op 115, ifVarInRange ( ) // op 45, inventoryAddBack ( ) // op 53, varSetValue ( vSunspotColor 4090 ) // op 53, varSetValue ( vSunspotRadius 40 ) // op 33, waterEffectSetWave ( 100 80 ) // op 32, waterEffectSetAttenuation ( 359 ) // op 31, waterEffectSetSpeed ( 15 ) // Fixed disassembly // init 0 > c[v1 != 0] // op 53, varSetValue ( vSunspotIntensity 45 ) // op 53, varSetValue ( vSunspotColor 4090 ) // op 53, varSetValue ( vSunspotRadius 40 ) // op 33, waterEffectSetWave ( 100 80 ) // op 32, waterEffectSetAttenuation ( 359 ) // op 31, waterEffectSetSpeed ( 15 ) if (script.condition == 565) { script.condition = 1; s.seek(-2, SEEK_CUR); } // END WORKAROUND script.script = readOpcodes(s); return script; } Common::Array ScriptData::readCondScripts(Common::SeekableReadStream &s) { Common::Array scripts; while (!s.eos()) { CondScript script = readCondScript(s); if (!script.condition) break; scripts.push_back(script); } return scripts; } HotSpot ScriptData::readHotspot(Common::ReadStream &s) { HotSpot hotspot; hotspot.condition = s.readUint16LE(); if (hotspot.condition == 0) return hotspot; if (hotspot.condition != -1) { hotspot.rects = readRects(s); hotspot.cursor = s.readUint16LE(); } hotspot.script = readOpcodes(s); return hotspot; } Common::Array ScriptData::readHotspots(Common::ReadStream &s) { Common::Array scripts; while (!s.eos()) { HotSpot hotspot = readHotspot(s); if (!hotspot.condition) break; scripts.push_back(hotspot); } return scripts; } NodeTransformAddHotspots::NodeTransformAddHotspots() : _zipBitIndex(-1) { } void NodeTransformAddHotspots::read(Common::SeekableReadStream *file) { _zipBitIndex++; _scripts = ScriptData::readCondScripts(*file); _hotspots = ScriptData::readHotspots(*file); } void NodeTransformAddHotspots::apply(NodePtr &node) { node->zipBitIndex = _zipBitIndex; node->scripts.push_back(_scripts); node->hotspots.push_back(_hotspots); } void NodeTransformAddSoundScripts::read(Common::SeekableReadStream *file) { _scripts = ScriptData::readCondScripts(*file); } void NodeTransformAddSoundScripts::apply(NodePtr &node) { node->soundScripts.push_back(_scripts); } void NodeTransformAddBackgroundSoundScripts::read(Common::SeekableReadStream *file) { _scripts = ScriptData::readCondScripts(*file); } void NodeTransformAddBackgroundSoundScripts::apply(NodePtr &node) { node->backgroundSoundScripts.push_back(_scripts); } NodeWalker::NodeWalker(NodeTransform *transform) : _transform(transform) { } void NodeWalker::read(Common::SeekableReadStream *file, Common::Array &allNodes, bool createMissingSharedNodes) { while (!file->eos()) { int16 id = file->readUint16LE(); // End of list if (id == 0) break; if (id < -10) error("Unimplemented node list command"); if (id > 0) { // Normal node, find the node if existing NodePtr node; for (uint i = 0; i < allNodes.size(); i++) if (allNodes[i]->id == id) { node = allNodes[i]; break; } // Node not found, create a new one if (!node) { node = NodePtr(new NodeData()); node->id = id; allNodes.push_back(node); } _transform->read(file); _transform->apply(node); } else { // Several nodes sharing the same scripts // Find the node ids the script applies to Common::Array scriptNodeIds; if (id == -10) do { id = file->readUint16LE(); if (id < 0) { uint16 end = file->readUint16LE(); for (int i = -id; i <= end; i++) scriptNodeIds.push_back(i); } else if (id > 0) { scriptNodeIds.push_back(id); } } while (id); else for (int i = 0; i < -id; i++) { scriptNodeIds.push_back(file->readUint16LE()); } // Load the script _transform->read(file); // Add the script to each matching node for (uint i = 0; i < scriptNodeIds.size(); i++) { NodePtr node; // Find the current node if existing for (uint j = 0; j < allNodes.size(); j++) { if (allNodes[j]->id == scriptNodeIds[i]) { node = allNodes[j]; break; } } if (!node) { if (createMissingSharedNodes) { // Node not found, create a new one node = NodePtr(new NodeData()); node->id = scriptNodeIds[i]; allNodes.push_back(node); } else { // Node not found, skip it continue; } } _transform->apply(node); } } } } NodeWalker::~NodeWalker() { delete _transform; } static const RoomData roomsXXXX[] = { { kRoomShared, "XXXX" } }; static const RoomData roomsINTR[] = { { kRoomIntro, "INTR" } }; static const RoomData roomsTOHO[] = { { kRoomTomahnaStart, "TOHO" } }; static const RoomData roomsTOHB[] = { { kRoomTomahnaReturn, "TOHB" } }; static const RoomData roomsLE[] = { { kJnaninStart, "LEIS" }, { kRoomLeos, "LEOS" }, { kRoomLeet, "LEET" }, { kRoomLelt, "LELT" }, { kRoomLemt, "LEMT" }, { kRoomLeof, "LEOF" } }; static const RoomData roomsLI[] = { { kRoomEdannaStart, "LIDR" }, { kRoomLisw, "LISW" }, { kRoomLifo, "LIFO" }, { kRoomLisp, "LISP" }, { kRoomLine, "LINE" } }; static const RoomData roomsEN[] = { { kRoomVoltaicStart, "ENSI" }, { kRoomEnpp, "ENPP" }, { kRoomEnem, "ENEM" }, { kRoomEnlc, "ENLC" }, { kRoomEndd, "ENDD" }, { kRoomEnch, "ENCH" }, { kRoomEnli, "ENLI" } }; static const RoomData roomsNA[] = { { kRoomNarayan, "NACH" } }; static const RoomData roomsMENU[] = { { kRoomMenu, "MENU" }, { kRoomJournals, "JRNL" }, { kRoomDemo, "DEMO" }, { kRoomAtix, "ATIX" } }; static const RoomData roomsMA[] = { { kRoomAmateriaStart, "MACA" }, { kRoomMais, "MAIS" }, { kRoomMall, "MALL" }, { kRoomMass, "MASS" }, { kRoomMaww, "MAWW" }, { kRoomMato, "MATO" } }; static const RoomData roomsLOGO[] = { { kLogo, "LOGO" } }; const AgeData Database::_ages[] = { { 1, 0, 1, roomsXXXX, 0 }, { 2, 1, 1, roomsINTR, 0 }, { 3, 2, 1, roomsTOHO, 0 }, { 4, 4, 1, roomsTOHB, 0 }, { 5, 2, 6, roomsLE, 1 }, { 6, 4, 5, roomsLI, 2 }, { 7, 3, 7, roomsEN, 3 }, { 8, 3, 1, roomsNA, 4 }, { 9, 0, 4, roomsMENU, 0 }, { 10, 1, 6, roomsMA, 5 }, { 11, 0, 1, roomsLOGO, 0 } }; Database::Database(const Common::Platform platform, const Common::Language language, const uint32 localizationType) : _platform(platform), _language(language), _localizationType(localizationType), _soundIdMin(0), _soundIdMax(0) { _datFile = SearchMan.createReadStreamForMember("myst3.dat"); if (!_datFile) { error("Unable to find 'myst3.dat'"); } uint magic = _datFile->readUint32LE(); if (magic != MKTAG('M', 'Y', 'S', 'T')) { error("'myst3.dat' is invalid"); } uint version = _datFile->readUint32LE(); if (version != kDatVersion) { error("Incorrect 'myst3.dat' version. Expected '%d', found '%d'", kDatVersion, version); } bool isWindowMacVersion = _platform == Common::kPlatformWindows || _platform == Common::kPlatformMacintosh; bool isXboxVersion = _platform == Common::kPlatformXbox; readScriptIndex(_datFile, isWindowMacVersion); // Main scripts readScriptIndex(_datFile, isWindowMacVersion && _localizationType == kLocMulti6); // Menu scripts 6 languages version readScriptIndex(_datFile, isWindowMacVersion && _localizationType == kLocMulti2); // Menu scripts 2 languages CD version readScriptIndex(_datFile, isWindowMacVersion && _localizationType == kLocMonolingual); // Menu scripts english CD version readScriptIndex(_datFile, isXboxVersion); // Main scripts Xbox version readScriptIndex(_datFile, isXboxVersion && _localizationType != kLocMonolingual); // Menu scripts PAL Xbox version readScriptIndex(_datFile, isXboxVersion && _localizationType == kLocMonolingual); // Menu scripts NTSC Xbox version readSoundNames(_datFile, isWindowMacVersion); // Sound names readSoundNames(_datFile, isXboxVersion); // Sound names Xbox _roomScriptsStartOffset = _datFile->pos(); Common::SeekableReadStream *initScriptStream = getRoomScriptStream("INIT", kScriptTypeNodeInit); _nodeInitScript = ScriptData::readOpcodes(*initScriptStream); delete initScriptStream; Common::SeekableReadStream *cuesStream = getRoomScriptStream("INIT", kScriptTypeAmbientCue); loadAmbientCues(cuesStream); delete cuesStream; preloadCommonRooms(); initializeZipBitIndexTable(); if (isWindowMacVersion && _localizationType == kLocMulti2) { patchLanguageMenu(); } } Database::~Database() { delete _datFile; } void Database::preloadCommonRooms() { for (uint i = 0; i < ARRAYSIZE(_ages); i++) { const AgeData &age = _ages[i]; for (uint j = 0; j < age.roomCount; j++) { const RoomData &room = age.rooms[j]; if (isCommonRoom(room.id, age.id)) { Common::Array nodes = readRoomScripts(&room); _roomNodesCache.setVal(RoomKey(room.id, age.id), nodes); } } } } Common::Array Database::getRoomNodes(uint32 roomID, uint32 ageID) const { Common::Array nodes; if (_roomNodesCache.contains(RoomKey(roomID, ageID))) { nodes = _roomNodesCache.getVal(RoomKey(roomID, ageID)); } else { const RoomData *data = findRoomData(roomID, ageID); nodes = readRoomScripts(data); } return nodes; } Common::Array Database::listRoomNodes(uint32 roomID, uint32 ageID) { Common::Array nodes; Common::Array list; nodes = getRoomNodes(roomID, ageID); for (uint i = 0; i < nodes.size(); i++) { list.push_back(nodes[i]->id); } return list; } NodePtr Database::getNodeData(uint16 nodeID, uint32 roomID, uint32 ageID) { Common::Array nodes = getRoomNodes(roomID, ageID); for (uint i = 0; i < nodes.size(); i++) { if (nodes[i]->id == nodeID) return nodes[i]; } return NodePtr(); } void Database::initializeZipBitIndexTable() { int16 zipBit = 0; for (uint i = 0; i < ARRAYSIZE(_ages); i++) { for (uint j = 0; j < _ages[i].roomCount; j++) { _roomZipBitIndex.setVal(_ages[i].rooms[j].id, zipBit); // Add the highest zip-bit index for the current room // to get the zip-bit index for the next room int16 maxZipBitForRoom = 0; Common::Array nodes = readRoomScripts(&_ages[i].rooms[j]); for (uint k = 0; k < nodes.size(); k++) { maxZipBitForRoom = MAX(maxZipBitForRoom, nodes[k]->zipBitIndex); } zipBit += maxZipBitForRoom + 1; } } } int32 Database::getNodeZipBitIndex(uint16 nodeID, uint32 roomID, uint32 ageID) { if (!_roomZipBitIndex.contains(roomID)) { error("Unable to find zip-bit index for room %d", roomID); } Common::Array nodes = getRoomNodes(roomID, ageID); for (uint i = 0; i < nodes.size(); i++) { if (nodes[i]->id == nodeID) { return _roomZipBitIndex[roomID] + nodes[i]->zipBitIndex; } } error("Unable to find zip-bit index for node (%d, %d)", nodeID, roomID); } const RoomData *Database::findRoomData(uint32 roomID, uint32 ageID) const { for (uint i = 0; i < ARRAYSIZE(_ages); i++) { if (_ages[i].id == ageID) { for (uint j = 0; j < _ages[i].roomCount; j++) { if (_ages[i].rooms[j].id == roomID) { return &_ages[i].rooms[j]; } } } } error("No room with ID %d in age %d", roomID, ageID); } Common::Array Database::readRoomScripts(const RoomData *room) const { Common::Array nodes; // Load the node scripts Common::SeekableReadStream *scriptsStream = getRoomScriptStream(room->name, kScriptTypeNode); if (scriptsStream) { NodeWalker scriptWalker = NodeWalker(new NodeTransformAddHotspots()); scriptWalker.read(scriptsStream, nodes, true); delete scriptsStream; } // Load the ambient sound scripts, if any Common::SeekableReadStream *ambientSoundsStream = getRoomScriptStream(room->name, kScriptTypeAmbientSound); if (ambientSoundsStream) { NodeWalker scriptWalker = NodeWalker(new NodeTransformAddSoundScripts()); scriptWalker.read(ambientSoundsStream, nodes, false); delete ambientSoundsStream; } Common::SeekableReadStream *backgroundSoundsStream = getRoomScriptStream(room->name, kScriptTypeBackgroundSound); if (backgroundSoundsStream) { NodeWalker scriptWalker = NodeWalker(new NodeTransformAddBackgroundSoundScripts()); scriptWalker.read(backgroundSoundsStream, nodes, false); delete backgroundSoundsStream; } patchNodeScripts(room, nodes); return nodes; } void Database::patchNodeScripts(const RoomData *room, Common::Array &nodes) const { if (strcmp(room->name, "LEOF") == 0) { // The room LEOF does not have a script to set default water effect // parameters when entering a node. As a result, the pool of water // mainly visible in LEOF 23 uses the last set water effect parameters. // If the player comes from the top of the tower, the water effect is // barely visible. // As a workaround we insert default water effect settings in node // 32765 which get applied for each node in the room. // The new script disassembles as follow: // node: LEOF 32765 // init 0 > c[v1 != 0] (true) // op 33, waterEffectSetWave ( 100 100 ) // op 32, waterEffectSetAttenuation ( 360 ) // op 31, waterEffectSetSpeed ( 12 ) Opcode waterEffectSetWave; waterEffectSetWave.op = 33; waterEffectSetWave.args.push_back(100); waterEffectSetWave.args.push_back(100); Opcode waterEffectSetAttenuation; waterEffectSetAttenuation.op = 32; waterEffectSetAttenuation.args.push_back(360); Opcode waterEffectSetSpeed; waterEffectSetSpeed.op = 31; waterEffectSetSpeed.args.push_back(12); CondScript waterEffectScript; waterEffectScript.condition = 1; waterEffectScript.script.push_back(waterEffectSetWave); waterEffectScript.script.push_back(waterEffectSetAttenuation); waterEffectScript.script.push_back(waterEffectSetSpeed); NodePtr node32765 = NodePtr(new NodeData()); node32765->id = 32765; node32765->scripts.push_back(waterEffectScript); nodes.push_back(node32765); } } bool Database::isCommonRoom(uint32 roomID, uint32 ageID) const { return roomID == kRoomShared || roomID == kRoomMenu || roomID == kRoomJournals; } void Database::cacheRoom(uint32 roomID, uint32 ageID) { if (_roomNodesCache.contains(RoomKey(roomID, ageID))) { return; } // Remove old rooms from cache and add the new one for (NodesCache::iterator it = _roomNodesCache.begin(); it != _roomNodesCache.end(); it++) { if (!isCommonRoom(it->_key.roomID, it->_key.ageID)) { _roomNodesCache.erase(it); } } const RoomData *currentRoomData = findRoomData(roomID, ageID); if (!currentRoomData) return; _roomNodesCache.setVal(RoomKey(roomID, ageID), readRoomScripts(currentRoomData)); } Common::String Database::getRoomName(uint32 roomID, uint32 ageID) const { const RoomData *data = findRoomData(roomID, ageID); return data->name; } RoomKey Database::getRoomKey(const char *name) { for (uint i = 0; i < ARRAYSIZE(_ages); i++) for (uint j = 0; j < _ages[i].roomCount; j++) { if (scumm_stricmp(_ages[i].rooms[j].name, name) == 0) { return RoomKey(_ages[i].rooms[j].id, _ages[i].id); } } return RoomKey(0, 0); } uint32 Database::getAgeLabelId(uint32 ageID) { for (uint i = 0; i < ARRAYSIZE(_ages); i++) if (_ages[i].id == ageID) return _ages[i].labelId; return 0; } Common::String Database::getSoundName(uint32 id) { return _soundNames.getVal(id); } void Database::loadAmbientCues(Common::ReadStream *s) { _ambientCues.clear(); while (!s->eos()) { uint16 id = s->readUint16LE(); if (!id) break; AmbientCue cue; cue.id = id; cue.minFrames = s->readUint16LE(); cue.maxFrames = s->readUint16LE(); while (1) { uint16 track = s->readUint16LE(); if (!track) break; cue.tracks.push_back(track); } _ambientCues[id] = cue; } } const AmbientCue &Database::getAmbientCue(uint16 id) { if (!_ambientCues.contains(id)) error("Unable to find an ambient cue with id %d", id); return _ambientCues.getVal(id); } void Database::readScriptIndex(Common::SeekableReadStream *stream, bool load) { uint count = stream->readUint32LE(); for (uint i = 0; i < count; i++) { RoomScripts roomScripts; char roomName[5]; stream->read(roomName, sizeof(roomName)); roomName[4] = '\0'; roomScripts.room = Common::String(roomName); roomScripts.type = (ScriptType) stream->readUint32LE(); roomScripts.offset = stream->readUint32LE(); roomScripts.size = stream->readUint32LE(); if (load) { _roomScriptsIndex.push_back(roomScripts); } } } void Database::readSoundNames(Common::SeekableReadStream *stream, bool load) { uint count = stream->readUint32LE(); for (uint i = 0; i < count; i++) { uint id = stream->readUint32LE(); char soundName[32]; stream->read(soundName, sizeof(soundName)); soundName[31] = '\0'; if (load) { _soundNames[id] = Common::String(soundName); if (_soundIdMin == 0 || id < _soundIdMin) { _soundIdMin = id; } if (_soundIdMax == 0 || id > _soundIdMax) { _soundIdMax = id; } } } } Common::SeekableReadStream *Database::getRoomScriptStream(const char *room, ScriptType scriptType) const { for (uint i = 0; i < _roomScriptsIndex.size(); i++) { if (_roomScriptsIndex[i].room.equalsIgnoreCase(room) && _roomScriptsIndex[i].type == scriptType) { uint32 startOffset = _roomScriptsStartOffset + _roomScriptsIndex[i].offset; uint32 size = _roomScriptsIndex[i].size; return new Common::SeekableSubReadStream(_datFile, startOffset, startOffset + size); } } return nullptr; } bool Database::areRoomsScriptsEqual(uint32 roomID1, uint32 ageID1, uint32 roomID2, uint32 ageID2, ScriptType scriptType) { const RoomData *data1 = findRoomData(roomID1, ageID1); const RoomData *data2 = findRoomData(roomID2, ageID2); int32 startOffset1 = -1; int32 startOffset2 = -1; for (uint i = 0; i < _roomScriptsIndex.size(); i++) { if (_roomScriptsIndex[i].room.equalsIgnoreCase(data1->name) && _roomScriptsIndex[i].type == scriptType) { startOffset1 = _roomScriptsStartOffset + _roomScriptsIndex[i].offset; } if (_roomScriptsIndex[i].room.equalsIgnoreCase(data2->name) && _roomScriptsIndex[i].type == scriptType) { startOffset2 = _roomScriptsStartOffset + _roomScriptsIndex[i].offset; } } return startOffset1 == startOffset2; } int16 Database::getGameLanguageCode() const { // The monolingual versions of the game always use 0 as the language code if (_localizationType == kLocMonolingual) { return kEnglish; } switch (_language) { case Common::FR_FRA: return kFrench; case Common::DE_DEU: return kGerman; case Common::IT_ITA: return kItalian; case Common::ES_ESP: return kSpanish; case Common::EN_ANY: return kEnglish; default: return kOther; } } void Database::patchLanguageMenu() { // The menu scripts in 'myst3.dat" for the non English CD versions come from the French version // The scripts for the other languages only differ by the value set for AudioLanguage variable // when the language selection is not English. // This function patches the language selection script to set the appropriate value based // on the detected game language. // Script disassembly: // hotspot 5 > c[v1 != 0] (true) // rect > pitch: 373 heading: 114 width: 209 height: 28 // op 206, soundPlayVolume ( 795 5 ) // op 53, varSetValue ( vLanguageAudio 2 ) // <= The second argument of this opcode is patched // op 194, runPuzzle1 ( 18 ) // op 194, runPuzzle1 ( 19 ) NodePtr languageMenu = getNodeData(530, kRoomMenu, 9); languageMenu->hotspots[5].script[1].args[1] = getGameLanguageCode(); } } // End of namespace Myst3