1 /* ResidualVM - A 3D game interpreter
2  *
3  * ResidualVM is the legal property of its developers, whose names
4  * are too numerous to list here. Please refer to the AUTHORS
5  * file distributed with this source distribution.
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  *
21  */
22 
23 #include "engines/myst3/database.h"
24 
25 #include "common/archive.h"
26 #include "common/debug.h"
27 #include "common/substream.h"
28 
29 namespace Myst3 {
30 
31 /**
32  * An abstract node transformation.
33  *
34  * Subclasses can read data related to a node from a stream in read,
35  * and transform a node using that data in apply.
36  */
37 class NodeTransform {
38 public :
~NodeTransform()39 	virtual ~NodeTransform() {};
40 
41 	virtual void read(Common::SeekableReadStream *file) = 0;
42 	virtual void apply(NodePtr &node) = 0;
43 };
44 
45 /**
46  * A node transformation that reads hotspots and scripts,
47  * and adds them to a node.
48  */
49 class NodeTransformAddHotspots : public NodeTransform {
50 public :
51 	NodeTransformAddHotspots();
52 
53 	void read(Common::SeekableReadStream *file);
54 	void apply(NodePtr &node);
55 
56 private:
57 	int32 _zipBitIndex;
58 	Common::Array<CondScript> _scripts;
59 	Common::Array<HotSpot> _hotspots;
60 };
61 
62 /**
63  * A node transformation that reads scripts, and adds them to a node's
64  * sound scripts.
65  */
66 class NodeTransformAddSoundScripts : public NodeTransform {
67 public :
68 	void read(Common::SeekableReadStream *file) override;
69 	void apply(NodePtr &node) override;
70 
71 private:
72 	Common::Array<CondScript> _scripts;
73 };
74 
75 /**
76  * A node transformation that reads scripts, and adds them to a node's
77  * background sound scripts.
78  */
79 class NodeTransformAddBackgroundSoundScripts : public NodeTransform {
80 public :
81 	void read(Common::SeekableReadStream *file) override;
82 	void apply(NodePtr &node) override;
83 
84 private:
85 	Common::Array<CondScript> _scripts;
86 };
87 
88 /**
89  * Walks through a stream of nodes. For each encountered node, a NodeTransformer is called.
90  */
91 class NodeWalker {
92 public :
93 	NodeWalker(NodeTransform *transform);
94 	~NodeWalker();
95 
96 	void read(Common::SeekableReadStream *file, Common::Array<NodePtr> &allNodes, bool createMissingSharedNodes);
97 
98 private:
99 	NodeTransform *_transform;
100 };
101 
102 /**
103  * A collection of functions used to read script related data
104  */
105 class ScriptData {
106 public:
107 	static Common::Array<CondScript> readCondScripts(Common::SeekableReadStream &s);
108 	static Common::Array<Opcode> readOpcodes(Common::ReadStream &s);
109 	static Common::Array<HotSpot> readHotspots(Common::ReadStream &s);
110 	static Common::Array<PolarRect> readRects(Common::ReadStream &s);
111 	static CondScript readCondScript(Common::SeekableReadStream &s);
112 	static HotSpot readHotspot(Common::ReadStream &s);
113 
114 private:
ScriptData()115 	ScriptData() {};
116 };
117 
readRects(Common::ReadStream & s)118 Common::Array<PolarRect> ScriptData::readRects(Common::ReadStream &s) {
119 	Common::Array<PolarRect> rects;
120 
121 	bool lastRect = false;
122 	do {
123 		PolarRect rect;
124 		rect.centerPitch = s.readUint16LE();
125 		rect.centerHeading = s.readUint16LE();
126 		rect.width = s.readUint16LE();
127 		rect.height = s.readUint16LE();
128 
129 		if (rect.width < 0) {
130 			rect.width = -rect.width;
131 		} else {
132 			lastRect = true;
133 		}
134 
135 		rects.push_back(rect);
136 	} while (!lastRect && !s.eos());
137 
138 	return rects;
139 }
140 
readOpcodes(Common::ReadStream & s)141 Common::Array<Opcode> ScriptData::readOpcodes(Common::ReadStream &s) {
142 	Common::Array<Opcode> script;
143 
144 	while (!s.eos()) {
145 		Opcode opcode;
146 		uint16 code = s.readUint16LE();
147 
148 		opcode.op = code & 0xff;
149 		uint8 count = code >> 8;
150 		if (count == 0 && opcode.op == 0)
151 			break;
152 
153 		for (int i = 0; i < count; i++) {
154 			int16 value = s.readSint16LE();
155 			opcode.args.push_back(value);
156 		}
157 
158 		script.push_back(opcode);
159 	}
160 
161 	return script;
162 }
163 
readCondScript(Common::SeekableReadStream & s)164 CondScript ScriptData::readCondScript(Common::SeekableReadStream &s) {
165 	CondScript script;
166 	script.condition = s.readUint16LE();
167 	if(!script.condition)
168 		return script;
169 
170 	// WORKAROUND: Original data bug in MATO 32765
171 	// The script data for node MATO 32765 is missing its first two bytes
172 	// of data, resulting in incorrect opcodes being read
173 
174 	// Original disassembly:
175 	// init 0 > c[v565 != 0]
176 	//     op 115, ifVarInRange ( )
177 	//     op 45, inventoryAddBack ( )
178 	//     op 53, varSetValue ( vSunspotColor 4090 )
179 	//     op 53, varSetValue ( vSunspotRadius 40 )
180 	//     op 33, waterEffectSetWave ( 100 80 )
181 	//     op 32, waterEffectSetAttenuation ( 359 )
182 	//     op 31, waterEffectSetSpeed ( 15 )
183 
184 	// Fixed disassembly
185 	// init 0 > c[v1 != 0]
186 	//     op 53, varSetValue ( vSunspotIntensity 45 )
187 	//     op 53, varSetValue ( vSunspotColor 4090 )
188 	//     op 53, varSetValue ( vSunspotRadius 40 )
189 	//     op 33, waterEffectSetWave ( 100 80 )
190 	//     op 32, waterEffectSetAttenuation ( 359 )
191 	//     op 31, waterEffectSetSpeed ( 15 )
192 
193 	if (script.condition == 565) {
194 		script.condition = 1;
195 		s.seek(-2, SEEK_CUR);
196 	}
197 	// END WORKAROUND
198 
199 	script.script = readOpcodes(s);
200 
201 	return script;
202 }
203 
readCondScripts(Common::SeekableReadStream & s)204 Common::Array<CondScript> ScriptData::readCondScripts(Common::SeekableReadStream &s) {
205 	Common::Array<CondScript> scripts;
206 
207 	while (!s.eos()) {
208 		CondScript script = readCondScript(s);
209 
210 		if (!script.condition)
211 			break;
212 
213 		scripts.push_back(script);
214 	}
215 
216 	return scripts;
217 }
218 
readHotspot(Common::ReadStream & s)219 HotSpot ScriptData::readHotspot(Common::ReadStream &s) {
220 	HotSpot hotspot;
221 
222 	hotspot.condition = s.readUint16LE();
223 
224 	if (hotspot.condition == 0)
225 		return hotspot;
226 
227 	if (hotspot.condition != -1) {
228 		hotspot.rects = readRects(s);
229 		hotspot.cursor = s.readUint16LE();
230 	}
231 
232 	hotspot.script = readOpcodes(s);
233 
234 	return hotspot;
235 }
236 
readHotspots(Common::ReadStream & s)237 Common::Array<HotSpot> ScriptData::readHotspots(Common::ReadStream &s) {
238 	Common::Array<HotSpot> scripts;
239 
240 	while (!s.eos()) {
241 		HotSpot hotspot = readHotspot(s);
242 
243 		if (!hotspot.condition)
244 			break;
245 
246 		scripts.push_back(hotspot);
247 	}
248 
249 	return scripts;
250 }
251 
NodeTransformAddHotspots()252 NodeTransformAddHotspots::NodeTransformAddHotspots() : _zipBitIndex(-1) {
253 }
254 
read(Common::SeekableReadStream * file)255 void NodeTransformAddHotspots::read(Common::SeekableReadStream *file) {
256 	_zipBitIndex++;
257 	_scripts = ScriptData::readCondScripts(*file);
258 	_hotspots = ScriptData::readHotspots(*file);
259 }
260 
apply(NodePtr & node)261 void NodeTransformAddHotspots::apply(NodePtr &node) {
262 	node->zipBitIndex = _zipBitIndex;
263 	node->scripts.push_back(_scripts);
264 	node->hotspots.push_back(_hotspots);
265 }
266 
read(Common::SeekableReadStream * file)267 void NodeTransformAddSoundScripts::read(Common::SeekableReadStream *file) {
268 	_scripts = ScriptData::readCondScripts(*file);
269 }
270 
apply(NodePtr & node)271 void NodeTransformAddSoundScripts::apply(NodePtr &node) {
272 	node->soundScripts.push_back(_scripts);
273 }
274 
read(Common::SeekableReadStream * file)275 void NodeTransformAddBackgroundSoundScripts::read(Common::SeekableReadStream *file) {
276 	_scripts = ScriptData::readCondScripts(*file);
277 }
278 
apply(NodePtr & node)279 void NodeTransformAddBackgroundSoundScripts::apply(NodePtr &node) {
280 	node->backgroundSoundScripts.push_back(_scripts);
281 }
282 
NodeWalker(NodeTransform * transform)283 NodeWalker::NodeWalker(NodeTransform *transform) : _transform(transform) {
284 }
285 
read(Common::SeekableReadStream * file,Common::Array<NodePtr> & allNodes,bool createMissingSharedNodes)286 void NodeWalker::read(Common::SeekableReadStream *file, Common::Array<NodePtr> &allNodes, bool createMissingSharedNodes) {
287 	while (!file->eos()) {
288 		int16 id = file->readUint16LE();
289 
290 		// End of list
291 		if (id == 0)
292 			break;
293 
294 		if (id < -10)
295 			error("Unimplemented node list command");
296 
297 		if (id > 0) {
298 			// Normal node, find the node if existing
299 			NodePtr node;
300 			for (uint i = 0; i < allNodes.size(); i++)
301 				if (allNodes[i]->id == id) {
302 					node = allNodes[i];
303 					break;
304 				}
305 
306 			// Node not found, create a new one
307 			if (!node) {
308 				node = NodePtr(new NodeData());
309 				node->id = id;
310 				allNodes.push_back(node);
311 			}
312 
313 			_transform->read(file);
314 			_transform->apply(node);
315 		} else {
316 			// Several nodes sharing the same scripts
317 			// Find the node ids the script applies to
318 			Common::Array<int16> scriptNodeIds;
319 
320 			if (id == -10)
321 				do {
322 					id = file->readUint16LE();
323 					if (id < 0) {
324 						uint16 end = file->readUint16LE();
325 						for (int i = -id; i <= end; i++)
326 							scriptNodeIds.push_back(i);
327 
328 					} else if (id > 0) {
329 						scriptNodeIds.push_back(id);
330 					}
331 				} while (id);
332 			else
333 				for (int i = 0; i < -id; i++) {
334 					scriptNodeIds.push_back(file->readUint16LE());
335 				}
336 
337 			// Load the script
338 			_transform->read(file);
339 
340 			// Add the script to each matching node
341 			for (uint i = 0; i < scriptNodeIds.size(); i++) {
342 				NodePtr node;
343 
344 				// Find the current node if existing
345 				for (uint j = 0; j < allNodes.size(); j++) {
346 					if (allNodes[j]->id == scriptNodeIds[i]) {
347 						node = allNodes[j];
348 						break;
349 					}
350 				}
351 
352 				if (!node) {
353 					if (createMissingSharedNodes) {
354 						// Node not found, create a new one
355 						node = NodePtr(new NodeData());
356 						node->id = scriptNodeIds[i];
357 						allNodes.push_back(node);
358 					} else {
359 						// Node not found, skip it
360 						continue;
361 					}
362 				}
363 
364 				_transform->apply(node);
365 			}
366 		}
367 	}
368 }
369 
~NodeWalker()370 NodeWalker::~NodeWalker() {
371 	delete _transform;
372 }
373 
374 static const RoomData roomsXXXX[] = {
375 		{ 101, "XXXX" }
376 };
377 
378 static const RoomData roomsINTR[] = {
379 		{ 201, "INTR" }
380 };
381 
382 static const RoomData roomsTOHO[] = {
383 		{ 301, "TOHO" }
384 };
385 
386 static const RoomData roomsTOHB[] = {
387 		{ 401, "TOHB" }
388 };
389 
390 static const RoomData roomsLE[] = {
391 		{ 501, "LEIS" },
392 		{ 502, "LEOS" },
393 		{ 503, "LEET" },
394 		{ 504, "LELT" },
395 		{ 505, "LEMT" },
396 		{ 506, "LEOF" }
397 };
398 
399 static const RoomData roomsLI[] = {
400 		{ 601, "LIDR" },
401 		{ 602, "LISW" },
402 		{ 603, "LIFO" },
403 		{ 604, "LISP" },
404 		{ 605, "LINE" }
405 };
406 
407 static const RoomData roomsEN[] = {
408 		{ 701, "ENSI" },
409 		{ 703, "ENPP" },
410 		{ 704, "ENEM" },
411 		{ 705, "ENLC" },
412 		{ 706, "ENDD" },
413 		{ 707, "ENCH" },
414 		{ 708, "ENLI" }
415 };
416 
417 static const RoomData roomsNA[] = {
418 		{ 801, "NACH" }
419 };
420 
421 static const RoomData roomsMENU[] = {
422 		{ 901, "MENU" },
423 		{ 902, "JRNL" },
424 		{ 903, "DEMO" },
425 		{ 904, "ATIX" }
426 };
427 
428 static const RoomData roomsMA[] = {
429 		{ 1001, "MACA" },
430 		{ 1002, "MAIS" },
431 		{ 1003, "MALL" },
432 		{ 1004, "MASS" },
433 		{ 1005, "MAWW" },
434 		{ 1006, "MATO" }
435 };
436 
437 static const RoomData roomsLOGO[] = {
438 		{ 1101, "LOGO" }
439 };
440 
441 const AgeData Database::_ages[] = {
442 	{ 1, 0, 1, roomsXXXX, 0 },
443 	{ 2, 1, 1, roomsINTR, 0 },
444 	{ 3, 2, 1, roomsTOHO, 0 },
445 	{ 4, 4, 1, roomsTOHB, 0 },
446 	{ 5, 2, 6, roomsLE, 1 },
447 	{ 6, 4, 5, roomsLI, 2 },
448 	{ 7, 3, 7, roomsEN, 3 },
449 	{ 8, 3, 1, roomsNA, 4 },
450 	{ 9, 0, 4, roomsMENU, 0 },
451 	{ 10, 1, 6, roomsMA, 5 },
452 	{ 11, 0, 1, roomsLOGO, 0 }
453 };
454 
Database(const Common::Platform platform,const Common::Language language,const uint32 localizationType)455 Database::Database(const Common::Platform platform, const Common::Language language, const uint32 localizationType) :
456 		_platform(platform),
457 		_language(language),
458 		_localizationType(localizationType) {
459 
460 	_datFile = SearchMan.createReadStreamForMember("myst3.dat");
461 	if (!_datFile) {
462 		error("Unable to find 'myst3.dat'");
463 	}
464 
465 	uint magic = _datFile->readUint32LE();
466 	if (magic != MKTAG('M', 'Y', 'S', 'T')) {
467 		error("'myst3.dat' is invalid");
468 	}
469 
470 	uint version = _datFile->readUint32LE();
471 	if (version != kDatVersion) {
472 		error("Incorrect 'myst3.dat' version. Expected '%d', found '%d'", kDatVersion, version);
473 	}
474 
475 	bool isWindowMacVersion = _platform == Common::kPlatformWindows || _platform == Common::kPlatformMacintosh;
476 	bool isXboxVersion = _platform == Common::kPlatformXbox;
477 
478 	readScriptIndex(_datFile, isWindowMacVersion);                                          // Main scripts
479 	readScriptIndex(_datFile, isWindowMacVersion && _localizationType == kLocMulti6);      // Menu scripts 6 languages version
480 	readScriptIndex(_datFile, isWindowMacVersion && _localizationType == kLocMulti2);      // Menu scripts 2 languages CD version
481 	readScriptIndex(_datFile, isWindowMacVersion && _localizationType == kLocMonolingual); // Menu scripts english CD version
482 	readScriptIndex(_datFile, isXboxVersion);                                               // Main scripts Xbox version
483 	readScriptIndex(_datFile, isXboxVersion && _localizationType != kLocMonolingual);      // Menu scripts PAL Xbox version
484 	readScriptIndex(_datFile, isXboxVersion && _localizationType == kLocMonolingual);      // Menu scripts NTSC Xbox version
485 	readSoundNames(_datFile, isWindowMacVersion);                                           // Sound names
486 	readSoundNames(_datFile, isXboxVersion);                                                // Sound names Xbox
487 
488 	_roomScriptsStartOffset = _datFile->pos();
489 
490 	Common::SeekableReadStream *initScriptStream = getRoomScriptStream("INIT", kScriptTypeNodeInit);
491 	_nodeInitScript = ScriptData::readOpcodes(*initScriptStream);
492 	delete initScriptStream;
493 
494 	Common::SeekableReadStream *cuesStream = getRoomScriptStream("INIT", kScriptTypeAmbientCue);
495 	loadAmbientCues(cuesStream);
496 	delete cuesStream;
497 
498 	preloadCommonRooms();
499 	initializeZipBitIndexTable();
500 
501 	if (isWindowMacVersion && _localizationType == kLocMulti2) {
502 		patchLanguageMenu();
503 	}
504 }
505 
~Database()506 Database::~Database() {
507 	delete _datFile;
508 }
509 
preloadCommonRooms()510 void Database::preloadCommonRooms() {
511 	for (uint i = 0; i < ARRAYSIZE(_ages); i++) {
512 		const AgeData &age = _ages[i];
513 
514 		for (uint j = 0; j < age.roomCount; j++) {
515 			const RoomData &room = age.rooms[j];
516 
517 			if (isCommonRoom(room.id, age.id)) {
518 				Common::Array<NodePtr> nodes = readRoomScripts(&room);
519 				_roomNodesCache.setVal(RoomKey(room.id, age.id), nodes);
520 			}
521 		}
522 	}
523 }
524 
getRoomNodes(uint32 roomID,uint32 ageID) const525 Common::Array<NodePtr> Database::getRoomNodes(uint32 roomID, uint32 ageID) const {
526 	Common::Array<NodePtr> nodes;
527 
528 	if (_roomNodesCache.contains(RoomKey(roomID, ageID))) {
529 		nodes = _roomNodesCache.getVal(RoomKey(roomID, ageID));
530 	} else {
531 		const RoomData *data = findRoomData(roomID, ageID);
532 		nodes = readRoomScripts(data);
533 	}
534 
535 	return nodes;
536 }
537 
listRoomNodes(uint32 roomID,uint32 ageID)538 Common::Array<uint16> Database::listRoomNodes(uint32 roomID, uint32 ageID) {
539 	Common::Array<NodePtr> nodes;
540 	Common::Array<uint16> list;
541 
542 	nodes = getRoomNodes(roomID, ageID);
543 
544 	for (uint i = 0; i < nodes.size(); i++) {
545 		list.push_back(nodes[i]->id);
546 	}
547 
548 	return list;
549 }
550 
getNodeData(uint16 nodeID,uint32 roomID,uint32 ageID)551 NodePtr Database::getNodeData(uint16 nodeID, uint32 roomID, uint32 ageID) {
552 	Common::Array<NodePtr> nodes = getRoomNodes(roomID, ageID);
553 
554 	for (uint i = 0; i < nodes.size(); i++) {
555 		if (nodes[i]->id == nodeID)
556 			return nodes[i];
557 	}
558 
559 	return NodePtr();
560 }
561 
initializeZipBitIndexTable()562 void Database::initializeZipBitIndexTable() {
563 	int16 zipBit = 0;
564 	for (uint i = 0; i < ARRAYSIZE(_ages); i++) {
565 		for (uint j = 0; j < _ages[i].roomCount; j++) {
566 			_roomZipBitIndex.setVal(_ages[i].rooms[j].id, zipBit);
567 
568 			// Add the highest zip-bit index for the current room
569 			// to get the zip-bit index for the next room
570 			int16 maxZipBitForRoom = 0;
571 			Common::Array<NodePtr> nodes = readRoomScripts(&_ages[i].rooms[j]);
572 			for (uint k = 0; k < nodes.size(); k++) {
573 				maxZipBitForRoom = MAX(maxZipBitForRoom, nodes[k]->zipBitIndex);
574 			}
575 
576 			zipBit += maxZipBitForRoom + 1;
577 		}
578 	}
579 }
580 
getNodeZipBitIndex(uint16 nodeID,uint32 roomID,uint32 ageID)581 int32 Database::getNodeZipBitIndex(uint16 nodeID, uint32 roomID, uint32 ageID) {
582 	if (!_roomZipBitIndex.contains(roomID)) {
583 		error("Unable to find zip-bit index for room %d", roomID);
584 	}
585 
586 	Common::Array<NodePtr> nodes = getRoomNodes(roomID, ageID);
587 
588 	for (uint i = 0; i < nodes.size(); i++) {
589 		if (nodes[i]->id == nodeID) {
590 			return _roomZipBitIndex[roomID] + nodes[i]->zipBitIndex;
591 		}
592 	}
593 
594 	error("Unable to find zip-bit index for node (%d, %d)", nodeID, roomID);
595 }
596 
findRoomData(uint32 roomID,uint32 ageID) const597 const RoomData *Database::findRoomData(uint32 roomID, uint32 ageID) const {
598 	for (uint i = 0; i < ARRAYSIZE(_ages); i++) {
599 		if (_ages[i].id == ageID) {
600 			for (uint j = 0; j < _ages[i].roomCount; j++) {
601 				if (_ages[i].rooms[j].id == roomID) {
602 					return &_ages[i].rooms[j];
603 				}
604 			}
605 		}
606 	}
607 
608 	error("No room with ID %d", roomID);
609 }
610 
readRoomScripts(const RoomData * room) const611 Common::Array<NodePtr> Database::readRoomScripts(const RoomData *room) const {
612 	Common::Array<NodePtr> nodes;
613 
614 	// Load the node scripts
615 	Common::SeekableReadStream *scriptsStream = getRoomScriptStream(room->name, kScriptTypeNode);
616 	if (scriptsStream) {
617 		NodeWalker scriptWalker = NodeWalker(new NodeTransformAddHotspots());
618 		scriptWalker.read(scriptsStream, nodes, true);
619 
620 		delete scriptsStream;
621 	}
622 
623 	// Load the ambient sound scripts, if any
624 	Common::SeekableReadStream *ambientSoundsStream = getRoomScriptStream(room->name, kScriptTypeAmbientSound);
625 	if (ambientSoundsStream) {
626 		NodeWalker scriptWalker = NodeWalker(new NodeTransformAddSoundScripts());
627 		scriptWalker.read(ambientSoundsStream, nodes, false);
628 
629 		delete ambientSoundsStream;
630 	}
631 
632 	Common::SeekableReadStream *backgroundSoundsStream = getRoomScriptStream(room->name, kScriptTypeBackgroundSound);
633 	if (backgroundSoundsStream) {
634 		NodeWalker scriptWalker = NodeWalker(new NodeTransformAddBackgroundSoundScripts());
635 		scriptWalker.read(backgroundSoundsStream, nodes, false);
636 
637 		delete backgroundSoundsStream;
638 	}
639 
640 	patchNodeScripts(room, nodes);
641 
642 	return nodes;
643 }
644 
patchNodeScripts(const RoomData * room,Common::Array<NodePtr> & nodes) const645 void Database::patchNodeScripts(const RoomData *room, Common::Array<NodePtr> &nodes) const {
646 	if (strcmp(room->name, "LEOF") == 0) {
647 		// The room LEOF does not have a script to set default water effect
648 		// parameters when entering a node. As a result, the pool of water
649 		// mainly visible in LEOF 23 uses the last set water effect parameters.
650 		// If the player comes from the top of the tower, the water effect is
651 		// barely visible.
652 		// As a workaround we insert default water effect settings in node
653 		// 32765 which get applied for each node in the room.
654 		// The new script disassembles as follow:
655 
656 		// node: LEOF 32765
657 		// init 0 > c[v1 != 0] (true)
658 		// op 33, waterEffectSetWave ( 100 100 )
659 		// op 32, waterEffectSetAttenuation ( 360 )
660 		// op 31, waterEffectSetSpeed ( 12 )
661 
662 		Opcode waterEffectSetWave;
663 		waterEffectSetWave.op = 33;
664 		waterEffectSetWave.args.push_back(100);
665 		waterEffectSetWave.args.push_back(100);
666 
667 		Opcode waterEffectSetAttenuation;
668 		waterEffectSetAttenuation.op = 32;
669 		waterEffectSetAttenuation.args.push_back(360);
670 
671 		Opcode waterEffectSetSpeed;
672 		waterEffectSetSpeed.op = 31;
673 		waterEffectSetSpeed.args.push_back(12);
674 
675 		CondScript waterEffectScript;
676 		waterEffectScript.condition = 1;
677 		waterEffectScript.script.push_back(waterEffectSetWave);
678 		waterEffectScript.script.push_back(waterEffectSetAttenuation);
679 		waterEffectScript.script.push_back(waterEffectSetSpeed);
680 
681 		NodePtr node32765 = NodePtr(new NodeData());
682 		node32765->id = 32765;
683 		node32765->scripts.push_back(waterEffectScript);
684 
685 		nodes.push_back(node32765);
686 	}
687 }
688 
isCommonRoom(uint32 roomID,uint32 ageID) const689 bool Database::isCommonRoom(uint32 roomID, uint32 ageID) const {
690 	return roomID == 101 || roomID == 901 || roomID == 902;
691 }
692 
cacheRoom(uint32 roomID,uint32 ageID)693 void Database::cacheRoom(uint32 roomID, uint32 ageID) {
694 	if (_roomNodesCache.contains(RoomKey(roomID, ageID))) {
695 		return;
696 	}
697 
698 	// Remove old rooms from cache and add the new one
699 	for (NodesCache::iterator it = _roomNodesCache.begin(); it != _roomNodesCache.end(); it++) {
700 		if (!isCommonRoom(it->_key.roomID, it->_key.ageID)) {
701 			_roomNodesCache.erase(it);
702 		}
703 	}
704 
705 	const RoomData *currentRoomData = findRoomData(roomID, ageID);
706 
707 	if (!currentRoomData)
708 		return;
709 
710 	_roomNodesCache.setVal(RoomKey(roomID, ageID), readRoomScripts(currentRoomData));
711 }
712 
getRoomName(uint32 roomID,uint32 ageID) const713 Common::String Database::getRoomName(uint32 roomID, uint32 ageID) const {
714 	const RoomData *data = findRoomData(roomID, ageID);
715 	return data->name;
716 }
717 
getRoomKey(const char * name)718 RoomKey Database::getRoomKey(const char *name) {
719 	for (uint i = 0; i < ARRAYSIZE(_ages); i++)
720 		for (uint j = 0; j < _ages[i].roomCount; j++) {
721 			if (scumm_stricmp(_ages[i].rooms[j].name, name) == 0) {
722 				return RoomKey(_ages[i].rooms[j].id, _ages[i].id);
723 			}
724 		}
725 
726 	return RoomKey(0, 0);
727 }
728 
getAgeLabelId(uint32 ageID)729 uint32 Database::getAgeLabelId(uint32 ageID) {
730 	for (uint i = 0; i < ARRAYSIZE(_ages); i++)
731 		if (_ages[i].id == ageID)
732 			return _ages[i].labelId;
733 
734 	return 0;
735 }
736 
getSoundName(uint32 id)737 Common::String Database::getSoundName(uint32 id) {
738 	const Common::String result = _soundNames.getVal(id, "");
739 	if (result.empty())
740 		error("Unable to find a sound with id %d", id);
741 
742 	return result;
743 }
744 
loadAmbientCues(Common::ReadStream * s)745 void Database::loadAmbientCues(Common::ReadStream *s) {
746 	_ambientCues.clear();
747 
748 	while (!s->eos()) {
749 		uint16 id = s->readUint16LE();
750 
751 		if (!id)
752 			break;
753 
754 		AmbientCue cue;
755 		cue.id = id;
756 		cue.minFrames = s->readUint16LE();
757 		cue.maxFrames = s->readUint16LE();
758 
759 		while (1) {
760 			uint16 track = s->readUint16LE();
761 
762 			if (!track)
763 				break;
764 
765 			cue.tracks.push_back(track);
766 		}
767 
768 		_ambientCues[id] = cue;
769 	}
770 }
771 
getAmbientCue(uint16 id)772 const AmbientCue &Database::getAmbientCue(uint16 id) {
773 	if (!_ambientCues.contains(id))
774 		error("Unable to find an ambient cue with id %d", id);
775 
776 	return _ambientCues.getVal(id);
777 }
778 
readScriptIndex(Common::SeekableReadStream * stream,bool load)779 void Database::readScriptIndex(Common::SeekableReadStream *stream, bool load) {
780 	uint count = stream->readUint32LE();
781 	for (uint i = 0; i < count; i++) {
782 
783 		RoomScripts roomScripts;
784 
785 		char roomName[5];
786 		stream->read(roomName, sizeof(roomName));
787 		roomName[4] = '\0';
788 
789 		roomScripts.room = Common::String(roomName);
790 		roomScripts.type = (ScriptType) stream->readUint32LE();
791 		roomScripts.offset = stream->readUint32LE();
792 		roomScripts.size = stream->readUint32LE();
793 
794 		if (load) {
795 			_roomScriptsIndex.push_back(roomScripts);
796 		}
797 	}
798 }
799 
readSoundNames(Common::SeekableReadStream * stream,bool load)800 void Database::readSoundNames(Common::SeekableReadStream *stream, bool load) {
801 	uint count = stream->readUint32LE();
802 	for (uint i = 0; i < count; i++) {
803 
804 		uint id = stream->readUint32LE();
805 
806 		char soundName[32];
807 		stream->read(soundName, sizeof(soundName));
808 		soundName[31] = '\0';
809 
810 		if (load) {
811 			_soundNames[id] = Common::String(soundName);
812 		}
813 	}
814 }
815 
getRoomScriptStream(const char * room,ScriptType scriptType) const816 Common::SeekableReadStream *Database::getRoomScriptStream(const char *room, ScriptType scriptType) const {
817 	for (uint i = 0; i < _roomScriptsIndex.size(); i++) {
818 		if (_roomScriptsIndex[i].room.equalsIgnoreCase(room)
819 		    && _roomScriptsIndex[i].type == scriptType) {
820 			uint32 startOffset = _roomScriptsStartOffset + _roomScriptsIndex[i].offset;
821 			uint32 size = _roomScriptsIndex[i].size;
822 
823 			return new Common::SeekableSubReadStream(_datFile, startOffset, startOffset + size);
824 		}
825 	}
826 
827 	return nullptr;
828 }
829 
getGameLanguageCode() const830 int16 Database::getGameLanguageCode() const {
831 	// The monolingual versions of the game always use 0 as the language code
832 	if (_localizationType == kLocMonolingual) {
833 		return kEnglish;
834 	}
835 
836 	switch (_language) {
837 		case Common::FR_FRA:
838 			return kFrench;
839 		case Common::DE_DEU:
840 			return kGerman;
841 		case Common::IT_ITA:
842 			return kItalian;
843 		case Common::ES_ESP:
844 			return kSpanish;
845 		case Common::EN_ANY:
846 			return kEnglish;
847 		default:
848 			return kOther;
849 	}
850 }
851 
patchLanguageMenu()852 void Database::patchLanguageMenu() {
853 	// The menu scripts in 'myst3.dat" for the non English CD versions come from the French version
854 	// The scripts for the other languages only differ by the value set for AudioLanguage variable
855 	// when the language selection is not English.
856 	// This function patches the language selection script to set the appropriate value based
857 	// on the detected game langage.
858 
859 	// Script disassembly:
860 	//	hotspot 5 > c[v1 != 0] (true)
861 	//	rect > pitch: 373 heading: 114 width: 209 height: 28
862 	//	op 206, soundPlayVolume ( 795 5 )
863 	//	op 53, varSetValue ( vLanguageAudio 2 ) // <= The second argument of this opcode is patched
864 	//	op 194, runPuzzle1 ( 18 )
865 	//	op 194, runPuzzle1 ( 19 )
866 
867 	NodePtr languageMenu = getNodeData(530, 901, 9);
868 	languageMenu->hotspots[5].script[1].args[1] = getGameLanguageCode();
869 }
870 
871 } // End of namespace Myst3
872