[Scummvm-git-logs] scummvm master -> 0a9539d1df057e95e76cc953e0709f996b985422

sev- noreply at scummvm.org
Thu Jun 16 19:59:29 UTC 2022


This automated email contains information about 171 new commits which have been
pushed to the 'scummvm' repo located at https://github.com/scummvm/scummvm .

Summary:
aa337b416e MTROPOLIS: Some initial work
59aea6e16a MTROPOLIS: Add behavior + modifier container loaders
4d4588e675 MTROPOLIS: More modifiers
349bfa9694 MTROPOLIS: Plug-in base work, add Mac Obsidian support
48c3a89025 MTROPOLIS: Add Obsidian plug-in stubs and STransCt modifier stub
6f16b72d07 MTROPOLIS: Convert Mac version 47e15 floats to IEEE double
257ce80381 MTROPOLIS: Parse entire float80 values
a888c537fa MTROPOLIS: Add keyboard messenger modifier loader and merge some messenger functionality
1cdd7682d2 MTROPOLIS: Recognize modifier flags and editor layout position fields
a8fe716044 MTROPOLIS: Add graphic modifier loader
9c0337294f MTROPOLIS: Add text style modifier
3779d4acae MTROPOLIS: Add set modifier loader
4bcdf6785f MTROPOLIS: Move plug-ins to new dir, add timer messenger and collision detection messenger modifiers
5303419e1a MTROPOLIS: Add drag motion modifier loader
31fa569947 MTROPOLIS: Load integer, integer range, float, and string variables.  Fix label values in Miniscript.
f262a1f6ba MTROPOLIS: Add vector motion and vector variable modifier loaders
0e26437384 MTROPOLIS: Add boundary detection messenger modifier loader, add label data locators, consolidate some messenger structu
cb91300e61 MTROPOLIS: Properly recognize dynamic typed values for non-reference message payloads, add media cue modifier.
bf1a7d881d MTROPOLIS: Add change scene, vector motion, scene transition, element transition, compound variable, and MIDI modifier l
e69e1f7754 MTROPOLIS: Add Obsidian movement and rectshift loader stubs
abb8ccb79d MTROPOLIS: Add list modifier and preliminary structural element loading
4bb2ad12d1 MTROPOLIS: Add remaining asset and modifier types to get Obsidian boot stream parsed
d1f74fa80c MTROPOLIS: Add fault handling to VThread
b6189a7bea MTROPOLIS: Add a bunch of object materialization stubs, and notes.  Boot stream loading for Obsidian completed.
a3b0cb1436 MTROPOLIS: Stub out scene transitioning and loading
3f6c5d37bc MTROPOLIS: Recognize some missing Miniscript instruction flags
9a7acd460e MTROPOLIS: Fix build
3432d0180b MTROPOLIS: Fix text comment
b55268fa7e MTROPOLIS: Stub out some debugger things, add Win cursor loading
7d055f332b MTROPOLIS: Fix up Mac resource loading, add debug stub things
ca3ddbf23f MTROPOLIS: Add toast notifications and scene header to debug dashboard.
d7f3a26c74 MTROPOLIS: Fix layer field not being set
d93775e31e MTROPOLIS: Implement more miniscript
b6204b1f28 MTROPOLIS: Adjusted framerate control
88a4890c19 MTROPOLIS: Miniscript GetChild instruction
7ee9d0224e MTROPOLIS: More Miniscript work, overhaul lvalue handling to handle nested members (e.g. element.pos.x)
8010e6dff1 MTROPOLIS: Fix point and variable vector factories being reversed
79041d4df4 MTROPOLIS: Implement SysInfo modifier
dc0fe94ef5 MTROPOLIS: Stub out some scene transition behavior
b5ebc6eb36 MTROPOLIS: More work to get splash screen working
18f0271beb MTROPOLIS: Add messenger and timer messenger support
6e53185f52 MTROPOLIS: Initial movie element support
2764565331 MTROPOLIS: Fix up modifier child cloning not working correctly, fix crash on invalid Miniscript send destination
a1940c352a MTROPOLIS: Add at last cel signal
c5434a80dc MTROPOLIS: Add image assets and elements
f5887475a9 MTROPOLIS: Add text and sound skeletal defs to get to first scene
17458248c2 MTROPOLIS: Preliminary MIDI stuff
dc333593f1 MTROPOLIS: Debug overlay base work
f44d85fd61 MTROPOLIS: Debug overlay work
8acc940639 MTROPOLIS: Add go to scene buttons to debugger scene tree view
55e0290adf MTROPOLIS: Update pointer API
78742414b4 MTROPOLIS: Stepthrough debugger work
286a5dac92 MTROPOLIS: Fix stack reloc crash
389ad1c195 MTROPOLIS: Work to get object reference vars working, refactor a bunch of subfield reference stuff
f36ac72be3 MTROPOLIS: More script work
d71d8e566c MTROPOLIS: Add arith ops, fix compound-in-compound crash
367afa4b53 MTROPOLIS: Fix debugger scroll bar not animating
5e9f44d731 MTROPOLIS: Annotate fields of STransCt (plug-in scene transition) modifier.
d966b988b7 MTROPOLIS: Add width/height read attribs, fix up some error messages
089335f531 MTROPOLIS: Pause refactor, allow proxy writes to suspend execution and trigger VThread tasks
b04eff6895 MTROPOLIS: Add keyboard messenger modifier stuff and partial Project Started support
7b48322af5 MTROPOLIS: Get transition to game menu queue working (fails though)
ea7eaa4a15 MTROPOLIS: Add mToon assets and elements and fix some things
256b2f9ef5 MTROPOLIS: Improve image optimization
0248d394a9 MTROPOLIS: Add more attributes
b84f7be7d6 MTROPOLIS: Fix wrong channel indexes for Mac format 32-bit images
2a90865bcc MTROPOLIS: More script attribs, enough to boot to start scene on Mac version
b64d73849d MTROPOLIS: Refactor key message dispatch, add mouse messages, fix elements being parented incorrectly.
49f7dc0c2f MTROPOLIS: Fix some wrong IDs, debugging menu MIDI not working
69459bd470 MTROPOLIS: Script fixes
957037b6b6 MTROPOLIS: Auto-start movies that aren't paused
fd57b23890 MTROPOLIS: Fix up some script things, get music in intro credits working (partially)
00adebcb81 MTROPOLIS: Refactor MIDI player to support all of the necessary multi-input jank
bc6d7b3e8f MTROPOLIS: Save games
68be38a707 MTROPOLIS: Script/modifier behavior fixes, mostly get cursors working
6ed7aae75d MTROPOLIS: Fix "not" operator, fix If Messenger
da5f851633 MTROPOLIS: Fix corrupted lists in saves
db48ad6640 MTROPOLIS: C++11 compile fixes, debug inspector base work
92cd3de764 MTROPOLIS: Get rid of some more standard types and values
d6b1074f52 MTROPOLIS: Debug inspector drawing
3a9c8def13 MTROPOLIS: Debug inspector
f27703204b MTROPOLIS: Fix incorrect text bitmap size check
32640bcd88 MTROPOLIS: Add sounds, refactor media playback to start after scene transition.
9c16acd00e MTROPOLIS: mToon support, more support for weird attribs.
5ab54de82a MTROPOLIS: Fix enough things for Obsidian forest intro to be completable (sometimes)
9e010ccfd9 MTROPOLIS: Fix buggy mToon decompression
087d97f0e8 MTROPOLIS: Flag ImageElement as Done
c18b00f5be MTROPOLIS: Fix mToon decode bug
790ed24635 MTROPOLIS: Fix save corruption again
62accd5196 MTROPOLIS: Load data for Obsidian word games
4d8f8bc43b MTROPOLIS: Fix decompression of temporally-compressed mToons that don't have the flag set
ab07cd3e56 MTROPOLIS: mToon fixes (get Obsidian file cabinets working)
63af8a8146 MTROPOLIS: Handle bad mToon size fields
a4c8fb1b85 MTROPOLIS: Add support for QuickTime ranges, fix Obsidian Bureau light carousel triggering without interaction.
5f2f692acc MTROPOLIS: More Obsidian Bureau fixes
2cdd270e2c MTROPOLIS: Allow arithmetic on booleans
a9d94c2ed7 MTROPOLIS: Fix timers not reactivating after termination
f1a680864e MTROPOLIS: Fix some bugs affecting Spider fire puzzle
9a3b35deb7 MTROPOLIS: Don't reject invalid rects in drag motion modifier since they're insets and not actual rects
b3678090ae MTROPOLIS: Silently ignore attempts to set position to invalid value
f9c294fd83 MTROPOLIS: Fix mToon rate being off by a factor of 10, fix temporal compression detection (kind of)
1050da2a12 MTROPOLIS: Text rendering and Obsidian WordMixer
c487268ab3 MTROPOLIS: Fix some things that were spamming errors a lot
fee75068d1 MTROPOLIS: Fix temporal mToon sizes
2dc4c8232b MTROPOLIS: Drag motion modifier
1ec0d2a4cd MTROPOLIS: Forward object variable attribute references
52900575f1 MTROPOLIS: Fix some metal puzzle bugs
435fddeb55 MTROPOLIS: Stub out Movement/RectShift attributes
939420f64e MTROPOLIS: Make failed sends non-fatal (fixes being unable to click aircraft propulsion puzzle)
ea0406b6a5 MTROPOLIS: Add path motion modifier stub
b8dde4b4c5 MTROPOLIS: Add timevalue sets and vector motion modifier
9b0fae37c9 MTROPOLIS: Add media cue messenger, workaround for garbled section GUIDs in scene change modifiers
b013a3ce5d MTROPOLIS: Fix reversed mToons firing the wrong events
09ca43e78e MTROPOLIS: Add XOR mod support
1a8a0ed29c MTROPOLIS: Add "celcount" attribute
1975bd6b3c MTROPOLIS: Inherit asset name from asset on unnamed elements.  Fixes Obsidian Piazza not working.
547e02c749 MTROPOLIS: Handle direct assignment to variables
e8812e9444 MTROPOLIS: Add enhanced color support for 32-bit rendering
5eb9c0cf8c MTROPOLIS: Add transparent mToon blit mode
956c85c259 MTROPOLIS: Add NoteNum attribute
86eaedfa0c MTROPOLIS: Implement graphic modifiers, add close project command
9abae5362c MTROPOLIS: Fix mToon temporal decompression flag being set incorrectly
8461f9c845 MTROPOLIS: Fix up auto-play event behavior to hopefully fix Obsidian rebel VO not triggering
cae3ae0dd5 MTROPOLIS: Add trans blit for images
6db206acf4 MTROPOLIS: Add undocumented list "random" attrib
c60c554b0d MTROPOLIS: Restart timer modifier when executed while the timer is already running
7977def141 MTROPOLIS: Properly resolve messenger variable reference payloads
a0ad61ab3f MTROPOLIS: Fix graphics draw crash
2fed763839 MTROPOLIS: Fix backwards horizontal/vertical constraint flags on Mac
be1fc4c3ed MTROPOLIS: Add default names to modifiers so Piazza TextWork modifier resolves correctly.
69305a1a74 MTROPOLIS: Fix multi-scene layer interleaving not working correctly
a3f5b993e0 MTROPOLIS: Promote sound element to Done
d00a058d3e MTROPOLIS: Handle show/hide events, stub out element transition modifier.
9a4e8cb007 MTROPOLIS: Add collision detection modifier
35a3ad904d MTROPOLIS: Fix hang in Piazza tutorial
18466585a1 MTROPOLIS: Fix crash in Spider oil puzzle tower map viewer
45147c0140 MTROPOLIS: Discard "flushpriority" sets on images
0b47b7150c MTROPOLIS: Promote save/restore modifier to Done
060fbb2e6f MTROPOLIS: Add some missing attributes
f54a8864a4 MTROPOLIS: Fix if messengers not firing if the condition was a variable
2bc8af92fa MTROPOLIS: Stub out MIDI mute tracks
b8e510f9e4 MTROPOLIS: Replicate a ton of quirks needed to get the Bureau booth hint room working correctly
d7a8f4e674 MTROPOLIS: Fix pause event regression
6f0ada67d7 MTROPOLIS: Fix off-by-one clipping last frame of mToon animations
227bd68cc6 MTROPOLIS: Prioritize direct-to-screen videos over layer order for mouse collision
6fd87b94cd MTROPOLIS: Fix direct-to-screen prioritization not working
0497f6c9ec MTROPOLIS: Fix missing "cache" attribute and fix mToon "at last cel" triggering multiple times
c14ece6d94 MTROPOLIS: Off by default
da8d251713 MTROPOLIS: Clean up some things from initial review
af25941669 MTROPOLIS: Replace Rect16 with Common::Rect
7ed8358691 MTROPOLIS: Change Point16 to Common::Point
09a885e49d MTROPOLIS: Switch to using common XPFloat
06963b2f3d MTROPOLIS: Fix missing header
4bc7b46440 MTROPOLIS: Add credits
53dde2193e MTROPOLIS: Merge prep header and warning cleanup
1dae5a71eb MTROPOLIS: Remove MIDI source allocation/deallocation to fix compile error when INT8_MAX is unavailable
3baac5a15e MTROPOLIS: Prep for moving some things from runtime to core, add IInterfaceBase to quiet warnings about missing virtual 
b52b80c8fa MTROPOLIS: Warning/error cleanup
eb450cf5c4 MTROPOLIS: Warning/error cleanup, fix Xcode build
ca322dbfbd MTROPOLIS: Add saveload.cpp to POTFILES
0365b9c76d MTROPOLIS: Cleanup
3b68a84f0e MTROPOLIS: Remove notes since they are in the wiki now
5d738ee6ac MTROPOLIS: Fix formatting
39ac21f6b6 MTROPOLIS: Fix Missing Field Initializers for Group ID of Extra GUI Options
46d70265a6 MTROPOLIS: Fix Stray Extra Semicolon Causing GCC Pedantic Warnings
9fdb8d3e83 MTROPOLIS: Fix GCC Compiler Shadowing Warnings
1d0ea143a7 MTROPOLIS: Fix Set But Unused Variable GCC Compiler Warnings
17c24579b0 MTROPOLIS: Make Explicit Unhandled Type Cases in DynamicValueType Change
e58ae77728 MTROPOLIS: Fix Signed vs. Unsigned Comparison GCC Compiler Warnings
3192daf0a7 VIDEO: Fix Signed vs. Unsigned Warnings in MTROPOLIS changes to QT Decoder
0a9539d1df MTROPOLIS: Fix Deprecated Implicit Copy Operator GCC Warning


Commit: aa337b416ebd4aeeb9e40929de99985efa2836f4
    https://github.com/scummvm/scummvm/commit/aa337b416ebd4aeeb9e40929de99985efa2836f4
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Some initial work

Changed paths:
  A engines/mtropolis/configure.engine
  A engines/mtropolis/console.cpp
  A engines/mtropolis/console.h
  A engines/mtropolis/data.cpp
  A engines/mtropolis/data.h
  A engines/mtropolis/detection.cpp
  A engines/mtropolis/detection.h
  A engines/mtropolis/detection_tables.h
  A engines/mtropolis/metaengine.cpp
  A engines/mtropolis/module.mk
  A engines/mtropolis/mtropolis.cpp
  A engines/mtropolis/mtropolis.h
  A engines/mtropolis/runtime.cpp
  A engines/mtropolis/runtime.h
  A engines/mtropolis/vthread.cpp
  A engines/mtropolis/vthread.h


diff --git a/engines/mtropolis/configure.engine b/engines/mtropolis/configure.engine
new file mode 100644
index 00000000000..cb109add02a
--- /dev/null
+++ b/engines/mtropolis/configure.engine
@@ -0,0 +1,3 @@
+# This file is included from the main "configure" script
+# add_engine [name] [desc] [build-by-default] [subengines] [base games] [deps]
+add_engine mtropolis "mTropolis" yes
diff --git a/engines/mtropolis/console.cpp b/engines/mtropolis/console.cpp
new file mode 100644
index 00000000000..35b3045c433
--- /dev/null
+++ b/engines/mtropolis/console.cpp
@@ -0,0 +1,30 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mtropolis/console.h"
+
+namespace MTropolis {
+
+Console::Console() {
+}
+
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/console.h b/engines/mtropolis/console.h
new file mode 100644
index 00000000000..46a730dc29f
--- /dev/null
+++ b/engines/mtropolis/console.h
@@ -0,0 +1,36 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_CONSOLE_H
+#define MTROPOLIS_CONSOLE_H
+
+#include "gui/debugger.h"
+
+namespace MTropolis {
+
+class Console : public GUI::Debugger {
+public:
+	explicit Console();
+	~Console(void) override {}
+};
+}
+
+#endif
diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
new file mode 100644
index 00000000000..ebf009e9358
--- /dev/null
+++ b/engines/mtropolis/data.cpp
@@ -0,0 +1,371 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mtropolis/data.h"
+
+namespace MTropolis {
+
+namespace Data {
+
+
+DataReader::DataReader(Common::SeekableReadStreamEndian &stream, ProjectFormat projectFormat) : _stream(stream), _projectFormat(projectFormat) {
+}
+
+bool DataReader::readU8(uint8 &value) {
+	value = _stream.readByte();
+	return !_stream.err();
+}
+
+bool DataReader::readU16(uint16 &value) {
+	value = _stream.readUint16();
+	return !_stream.err();
+}
+
+bool DataReader::readU32(uint32 &value) {
+	value = _stream.readUint32();
+	return !_stream.err();
+}
+
+bool DataReader::readU64(uint64 &value) {
+	value = _stream.readUint64();
+	return !_stream.err();
+}
+
+bool DataReader::readS8(int8 &value) {
+	value = _stream.readSByte();
+	return !_stream.err();
+}
+
+bool DataReader::readS16(int16 &value) {
+	value = _stream.readSint16();
+	return !_stream.err();
+}
+
+bool DataReader::readS32(int32 &value) {
+	value = _stream.readSint32();
+	return !_stream.err();
+}
+
+bool DataReader::readS64(int64 &value) {
+	value = _stream.readSint64();
+	return !_stream.err();
+}
+
+bool DataReader::readF32(float &value) {
+	value = _stream.readFloat();
+	return !_stream.err();
+}
+
+bool DataReader::readF64(double &value) {
+	value = _stream.readDouble();
+	return !_stream.err();
+}
+
+bool DataReader::read(void *dest, size_t size) {
+	while (size > 0) {
+		uint32 thisChunkSize = UINT32_MAX;
+		if (size < thisChunkSize) {
+			thisChunkSize = static_cast<uint32>(size);
+		}
+
+		_stream.read(dest, thisChunkSize);
+		if (_stream.err()) {
+			return false;
+		}
+
+		dest = static_cast<char *>(dest) + thisChunkSize;
+		size -= thisChunkSize;
+	}
+
+	return true;
+}
+
+bool DataReader::readTerminatedStr(Common::String& value, size_t size) {
+	if (size > 0) {
+		Common::Array<char> strChars;
+		strChars.resize(size);
+		if (!this->read(&strChars[0], size)) {
+			return false;
+		}
+		if (strChars[size - 1] != 0) {
+			return false;
+		}
+		value = Common::String(&strChars[0], size - 1);
+	} else {
+		value.clear();
+	}
+
+	return true;
+}
+
+bool DataReader::readNonTerminatedStr(Common::String &value, size_t size) {
+	if (size > 0) {
+		Common::Array<char> strChars;
+		strChars.resize(size);
+		if (!this->read(&strChars[0], size)) {
+			return false;
+		}
+		value = Common::String(&strChars[0], size);
+	} else {
+		value.clear();
+	}
+
+	return true;
+}
+
+bool DataReader::skip(size_t count) {
+	while (count > 0) {
+		uint64 thisChunkSize = INT64_MAX;
+		if (count < thisChunkSize) {
+			thisChunkSize = static_cast<uint64>(count);
+		}
+
+		if (!_stream.seek(static_cast<int64>(count), SEEK_CUR)) {
+			return false;
+		}
+
+		count -= static_cast<size_t>(thisChunkSize);
+	}
+	return true;
+}
+
+ProjectFormat DataReader::getProjectFormat() const {
+	return _projectFormat;
+}
+
+bool Rect::load(DataReader &reader) {
+	if (reader.getProjectFormat() == kProjectFormatMacintosh)
+		return reader.readS16(top) && reader.readS16(left) && reader.readS16(bottom) && reader.readS16(right);
+	else if (reader.getProjectFormat() == kProjectFormatWindows)
+		return reader.readS16(left) && reader.readS16(top) && reader.readS16(right) && reader.readS16(bottom);
+	else
+		return false;
+}
+
+bool Point::load(DataReader &reader) {
+	if (reader.getProjectFormat() == kProjectFormatMacintosh)
+		return reader.readS16(y) && reader.readS16(x);
+	else if (reader.getProjectFormat() == kProjectFormatWindows)
+		return reader.readS16(x) && reader.readS16(y);
+	else
+		return false;
+}
+
+bool Event::load(DataReader& reader) {
+	return reader.readU32(eventID) && reader.readU32(eventInfo);
+}
+
+DataObject::DataObject() : _type(DataObjectTypes::kUnknown), _revision(0) {
+}
+
+DataObject::~DataObject() {
+}
+
+DataReadErrorCode DataObject::load(DataObjectTypes::DataObjectType type, uint16 revision, DataReader &reader) {
+	_type = type;
+	_revision = revision;
+	return this->load(reader);
+}
+
+uint16 DataObject::getRevision() const {
+	return _revision;
+}
+
+DataObjectTypes::DataObjectType DataObject::getType() const {
+	return _type;
+}
+
+DataReadErrorCode ProjectHeader::load(DataReader &reader) {
+	if (_revision != 0) {
+		return kDataReadErrorUnsupportedRevision;
+	}
+
+	if (!reader.readU32(persistFlags) || !reader.readU32(sizeIncludingTag) || !reader.readU16(unknown1) || !reader.readU32(catalogFilePosition)) {
+		return kDataReadErrorReadFailed;
+	}
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode PresentationSettings::load(DataReader &reader) {
+
+	if (_revision != 2)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(persistFlags) ||
+		!reader.readU32(sizeIncludingTag) ||
+		!reader.readBytes(unknown1) ||
+		!dimensions.load(reader) ||
+		!reader.readU16(bitsPerPixel) ||
+		!reader.readU16(unknown4))
+		return kDataReadErrorReadFailed;
+
+	if (sizeIncludingTag != 24)
+		return kDataReadErrorUnrecognized;
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode AssetCatalog::load(DataReader& reader) {
+	if (_revision != 4)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(persistFlags) ||
+		!reader.readU32(totalNameSizePlus22) ||
+		!reader.readBytes(unknown1) ||
+		!reader.readU32(numAssets))
+		return kDataReadErrorReadFailed;
+
+	assets.resize(numAssets);
+
+	for (size_t i = 0; i < numAssets; i++) {
+		AssetInfo &asset = assets[i];
+		if (!reader.readU32(asset.flags1) || !reader.readU16(asset.nameLength) || !reader.readU16(asset.alwaysZero) || !reader.readU32(asset.unknown1) || !reader.readU32(asset.filePosition) || !reader.readU32(asset.assetType) || !reader.readU32(asset.flags2))
+			return kDataReadErrorReadFailed;
+
+		if (!reader.readNonTerminatedStr(asset.name, asset.nameLength))
+			return kDataReadErrorReadFailed;
+	}
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode ProjectCatalog::load(DataReader &reader) {
+	if (_revision != 2 && _revision != 3) {
+		return kDataReadErrorUnsupportedRevision;
+	}
+
+	uint16 numSegments;
+	uint16 numStreams;
+
+	if (!reader.readU32(persistFlags) || !reader.readU32(sizeOfStreamAndSegmentDescs)
+		|| !reader.readU16(numStreams) || !reader.readU16(unknown1)
+		|| !reader.readU16(unknown2) || !reader.readU16(numSegments)) {
+		return kDataReadErrorReadFailed;
+	}
+
+	streams.resize(numStreams);
+	segments.resize(numSegments);
+
+	for (size_t i = 0; i < numStreams; i++) {
+		StreamDesc &streamDesc = streams[i];
+
+		streamDesc.streamType[24] = 0;
+		if (!reader.read(streamDesc.streamType, 24) || !reader.readU16(streamDesc.segmentIndexPlusOne)) {
+			return kDataReadErrorReadFailed;
+		}
+
+		if (_revision >= 3 && !reader.skip(8)) {
+			return kDataReadErrorReadFailed;
+		}
+
+		if (!reader.readU32(streamDesc.pos) || !reader.readU32(streamDesc.size)) {
+			return kDataReadErrorReadFailed;
+		}
+	}
+
+	uint32 unknownSegmentPrefix = 0;
+	if (!reader.readU32(unknownSegmentPrefix) || unknownSegmentPrefix != 1) {
+		return kDataReadErrorUnrecognized;
+	}
+
+	for (size_t i = 0; i < numSegments; i++) {
+		SegmentDesc &segDesc = segments[i];
+
+		uint16 lengthOfLabel;
+		uint16 lengthOfExportedPath;
+		if (!reader.readU32(segDesc.segmentID)
+			|| !reader.readU16(lengthOfLabel) || !reader.readTerminatedStr(segDesc.label, lengthOfLabel)
+			|| !reader.readU16(lengthOfExportedPath) || !reader.readTerminatedStr(segDesc.exportedPath, lengthOfExportedPath)) {
+			return kDataReadErrorReadFailed;
+		}
+	}
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode StreamHeader::load(DataReader& reader) {
+	if (_revision != 0)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(marker) ||
+		!reader.readU32(sizeIncludingTag) ||
+		!reader.read(name, 16) ||
+		!reader.readBytes(projectID) ||
+		!reader.readBytes(unknown1) ||
+		!reader.readU16(unknown2))
+		return kDataReadErrorReadFailed;
+
+	if (sizeIncludingTag != 38)
+		return kDataReadErrorUnrecognized;
+
+	name[16] = 0;
+
+	return kDataReadErrorNone;
+}
+
+
+DataReadErrorCode loadDataObject(DataReader& reader, Common::SharedPtr<DataObject>& outObject) {
+	uint32 type;
+	uint16 revision;
+	if (!reader.readU32(type) || !reader.readU16(revision)) {
+		return kDataReadErrorReadFailed;
+	}
+
+	DataObject *dataObject = nullptr;
+	switch (type) {
+	case DataObjectTypes::kProjectHeader:
+		dataObject = new ProjectHeader();
+		break;
+	case DataObjectTypes::kProjectCatalog:
+		dataObject = new ProjectCatalog();
+		break;
+	case DataObjectTypes::kStreamHeader:
+		dataObject = new StreamHeader();
+		break;
+	case DataObjectTypes::kPresentationSettings:
+		dataObject = new PresentationSettings();
+		break;
+	case DataObjectTypes::kAssetCatalog:
+		dataObject = new AssetCatalog();
+		break;
+	default:
+		break;
+	}
+
+	if (dataObject == nullptr) {
+		return kDataReadErrorUnrecognized;
+	}
+
+	Common::SharedPtr<DataObject> sharedPtr(dataObject);
+	DataReadErrorCode errorCode = dataObject->load(static_cast<DataObjectTypes::DataObjectType>(type), revision, reader);
+	if (errorCode != kDataReadErrorNone) {
+		outObject.reset();
+		return errorCode;
+	}
+
+	outObject = sharedPtr;
+	return errorCode;
+}
+
+} // End of namespace Data
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
new file mode 100644
index 00000000000..32139e28d78
--- /dev/null
+++ b/engines/mtropolis/data.h
@@ -0,0 +1,252 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_DATA_H
+#define MTROPOLIS_DATA_H
+
+#include "common/array.h"
+#include "common/error.h"
+#include "common/ptr.h"
+#include "common/stream.h"
+
+namespace MTropolis {
+
+namespace Data {
+
+enum ProjectFormat {
+	kProjectFormatUnknown,
+
+	kProjectFormatMacintosh,
+	kProjectFormatWindows,
+	kProjectFormatNeutral,
+};
+
+enum DataReadErrorCode {
+	kDataReadErrorNone = 0,
+
+	kDataReadErrorUnsupportedRevision,
+	kDataReadErrorReadFailed,
+	kDataReadErrorUnrecognized,
+};
+
+namespace DataObjectTypes {
+
+enum DataObjectType {
+	kUnknown = 0,
+
+	kProjectCatalog       = 0x3e8,
+	kStreamHeader         = 0x3e9,
+	kProjectHeader        = 0x3ea,
+	kPresentationSettings = 0x3ec,
+
+	kAssetCatalog         = 0xd,
+};
+
+} // End of namespace DataObjectTypes
+
+class DataReader {
+
+public:
+	DataReader(Common::SeekableReadStreamEndian &stream, ProjectFormat projectFormat);
+
+	bool readU8(uint8 &value);
+	bool readU16(uint16 &value);
+	bool readU32(uint32 &value);
+	bool readU64(uint64 &value);
+	bool readS8(int8 &value);
+	bool readS16(int16 &value);
+	bool readS32(int32 &value);
+	bool readS64(int64 &value);
+	bool readF32(float &value);
+	bool readF64(double &value);
+	bool read(void *dest, size_t size);
+
+	// Reads a terminated string where "length" is the number of characters including a null terminator
+	bool readTerminatedStr(Common::String &value, size_t length);
+
+	bool readNonTerminatedStr(Common::String &value, size_t length);
+
+	template<size_t TSize>
+	bool readChars(char (&arr)[TSize]);
+
+	template<size_t TSize>
+	bool readBytes(uint8 (&arr)[TSize]);
+
+	bool skip(size_t count);
+
+	ProjectFormat getProjectFormat() const;
+
+private:
+	Common::SeekableReadStreamEndian &_stream;
+	ProjectFormat _projectFormat;
+};
+
+struct Rect {
+	bool load(DataReader &reader);
+
+	int16 top;
+	int16 left;
+	int16 bottom;
+	int16 right;
+};
+
+struct Point {
+	bool load(DataReader &reader);
+
+	int16 x;
+	int16 y;
+};
+
+struct Event {
+	bool load(DataReader &reader);
+
+	uint32 eventID;
+	uint32 eventInfo;
+};
+
+class DataObject : public Common::NonCopyable {
+
+public:
+	DataObject();
+	virtual ~DataObject();
+	DataReadErrorCode load(DataObjectTypes::DataObjectType type, uint16 revision, DataReader &reader);
+
+	uint16 getRevision() const;
+	DataObjectTypes::DataObjectType getType() const;
+
+protected:
+	virtual DataReadErrorCode load(DataReader &reader) = 0;
+
+	DataObjectTypes::DataObjectType _type;
+	uint16 _revision;
+};
+
+class ProjectHeader : public DataObject {
+
+public:
+	uint32 persistFlags;
+	uint32 sizeIncludingTag;
+	uint16 unknown1;
+	uint32 catalogFilePosition;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+struct PresentationSettings : public DataObject {
+
+public:
+	DataReadErrorCode load(DataReader &reader) override;
+
+	uint32 persistFlags;
+	uint32 sizeIncludingTag;
+	uint8 unknown1[2];
+	Point dimensions;
+	uint16 bitsPerPixel;
+	uint16 unknown4;
+};
+
+struct AssetCatalog : public DataObject {
+	DataReadErrorCode load(DataReader &reader) override;
+
+	enum {
+		kFlag1Deleted = 1,
+		kFlag1LimitOnePerSegment = 2,
+	};
+
+	struct AssetInfo {
+		uint32 flags1;
+		uint16 nameLength;
+		uint16 alwaysZero;
+		uint32 unknown1;     // Possibly scene ID
+		uint32 filePosition; // Contains a static value in Obsidian
+		uint32 assetType;
+		uint32 flags2;
+		Common::String name;
+	};
+
+	uint32 persistFlags;
+	uint32 totalNameSizePlus22;
+	uint8 unknown1[4];
+	uint32 numAssets;
+	Common::Array<AssetInfo> assets;
+};
+
+class ProjectCatalog : public DataObject {
+
+public:
+	struct StreamDesc {
+		char streamType[25];
+		uint16 segmentIndexPlusOne;
+		uint32 size;
+		uint32 pos;
+	};
+
+	struct SegmentDesc {
+		uint32 segmentID;
+		Common::String label;
+		Common::String exportedPath;
+	};
+
+	uint32 persistFlags;
+	uint32 sizeOfStreamAndSegmentDescs;
+	uint16 unknown1;
+	uint16 unknown2;
+	uint32 unknown3;
+
+	Common::Array<SegmentDesc> segments;
+	Common::Array<StreamDesc> streams;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+class StreamHeader : public DataObject {
+
+public:
+	uint32 marker;
+	uint32 sizeIncludingTag;
+	char name[17];
+	uint8 projectID[2];
+	uint8 unknown1[4]; // Seems to be consistent across builds
+	uint16 unknown2;   // 0
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+DataReadErrorCode loadDataObject(DataReader &reader, Common::SharedPtr<DataObject> &outObject);
+
+template<size_t TSize>
+inline bool DataReader::readBytes(uint8(&arr)[TSize]) {
+	return this->read(arr, TSize);
+}
+
+template<size_t TSize>
+inline bool DataReader::readChars(char (&arr)[TSize]) {
+	return this->read(arr, TSize);
+}
+
+} // End of namespace Data
+
+} // End of namespace MTropolis
+
+#endif /* MTROPOLIS_DATA_H */
diff --git a/engines/mtropolis/detection.cpp b/engines/mtropolis/detection.cpp
new file mode 100644
index 00000000000..52b665e6991
--- /dev/null
+++ b/engines/mtropolis/detection.cpp
@@ -0,0 +1,87 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "base/plugins.h"
+#include "engines/advancedDetector.h"
+
+#include "mtropolis/detection.h"
+
+#include "common/config-manager.h"
+
+static const PlainGameDescriptor mTropolisGames[] = {
+	{"obsidian", "Obsidian"},
+	{nullptr, nullptr}
+};
+
+#include "mtropolis/detection_tables.h"
+
+static const char *directoryGlobs[] = {
+	"Obsidian",
+	"RESOURCE",
+	"Saved Games",
+	nullptr
+};
+
+class MTropolisMetaEngineDetection : public AdvancedMetaEngineDetection {
+public:
+	MTropolisMetaEngineDetection() : AdvancedMetaEngineDetection(MTropolis::gameDescriptions, sizeof(MTropolis::MTropolisGameDescription), mTropolisGames) {
+		_maxScanDepth = 3;
+		_directoryGlobs = directoryGlobs;
+	}
+
+	const char *getEngineId() const override {
+		return "mtropolis";
+	}
+
+	const char *getName() const override {
+		return "mTropolis";
+	}
+
+	const char *getOriginalCopyright() const override {
+		return "mTropolis (C) mFactory/Quark";
+	}
+
+	const ExtraGuiOptions getExtraGuiOptions(const Common::String &target) const override;
+
+	ADDetectedGame fallbackDetect(const FileMap &allFiles, const Common::FSList &fslist, ADDetectedGameExtraInfo **extra) const override;
+};
+
+const ExtraGuiOptions MTropolisMetaEngineDetection::getExtraGuiOptions(const Common::String &target) const {
+
+	ExtraGuiOptions options;
+	return options;
+}
+
+ADDetectedGame MTropolisMetaEngineDetection::fallbackDetect(const FileMap &allFiles, const Common::FSList &fslist, ADDetectedGameExtraInfo **extra) const {
+	// Set the default values for the fallback descriptor's ADGameDescription part.
+	MTropolis::g_fallbackDesc.desc.language = Common::UNK_LANG;
+	MTropolis::g_fallbackDesc.desc.platform = Common::kPlatformDOS;
+	MTropolis::g_fallbackDesc.desc.flags = ADGF_NO_FLAGS;
+
+	// Set default values for the fallback descriptor's MTropolisGameDescription part.
+	MTropolis::g_fallbackDesc.gameID = 0;
+	MTropolis::g_fallbackDesc.version = 0;
+
+	//return (const ADGameDescription *)&MTropolis::g_fallbackDesc;
+	return ADDetectedGame();
+}
+
+REGISTER_PLUGIN_STATIC(MTROPOLIS_DETECTION, PLUGIN_TYPE_ENGINE_DETECTION, MTropolisMetaEngineDetection);
diff --git a/engines/mtropolis/detection.h b/engines/mtropolis/detection.h
new file mode 100644
index 00000000000..e72a7072fe0
--- /dev/null
+++ b/engines/mtropolis/detection.h
@@ -0,0 +1,44 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_DETECTION_H
+#define MTROPOLIS_DETECTION_H
+
+#include "engines/advancedDetector.h"
+
+namespace MTropolis {
+
+enum MTropolisGameID {
+	GID_OBSIDIAN			= 0,
+	GID_LEARNING_MTROPOLIS	= 1,
+};
+
+struct MTropolisGameDescription {
+	ADGameDescription desc;
+
+	int gameID;
+	int gameType;
+	uint16 version;
+};
+
+} // End of namespace MTropolis
+
+#endif // MTROPOLIS_DETECTION_H
diff --git a/engines/mtropolis/detection_tables.h b/engines/mtropolis/detection_tables.h
new file mode 100644
index 00000000000..21f06f2580f
--- /dev/null
+++ b/engines/mtropolis/detection_tables.h
@@ -0,0 +1,108 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_DETECTION_TABLES_H
+#define MTROPOLIS_DETECTION_TABLES_H
+
+#include "engines/advancedDetector.h"
+
+namespace MTropolis {
+
+static const MTropolisGameDescription gameDescriptions[] = {
+#if 0
+	{
+		// Obsidian Macintosh
+		{
+			"obsidian",
+			"V1.0, 1/13/97, installed, CD",
+			{
+				//{ "Obsidian Data 0", 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -1 },  // Only used for launch loading screen
+				{ "Obsidian Data 1", 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -1 },
+				{ "Obsidian Data 2", 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -1 },
+				{ "Obsidian Data 3", 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -1 },
+				{ "Obsidian Data 4", 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -1 },
+				{ "Obsidian Data 5", 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -1 },
+				{ "Obsidian Data 6", 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -1 },
+				AD_LISTEND
+			},
+			Common::EN_ANY,
+			Common::kPlatformMacintosh,
+			ADGF_NO_FLAGS,
+			GUIO0()
+		},
+		GID_OBSIDIAN,
+		0,
+		0,
+	},
+#endif
+	{
+		// Obsidian Windows, installed
+		{
+			"obsidian",
+			"V1.0, 1/13/97, installed, CD",
+			{
+				{ "Obsidian.c95", 0, "fea68ff30ff319cdab30b79d2850a480", -1 },
+				{ "RSGKit.r95", 0, "071dc9098f9610fcec45c96342b1b69a", -1 },
+				{ "MCURSORS.C95", 0, "dcbe480913eebf233d0cdc33809bf048", -1 },
+				//{ "Start Obsidian", 0, "51a4980089bb35da0f7f6381382d2889", -1 },
+				{ "Obsidian Data 1.MPL", 0, "9531162c32272c33837074be4646422a", -1 },
+				{ "Obsidian Data 2.MPX", 0, "c13c9be0ab0482a952532fa647a67a7a", -1 },
+				{ "Obsidian Data 3.MPX", 0, "35d8332221a7236b122b43233428f5dc", -1 },
+				{ "Obsidian Data 4.MPX", 0, "263fe824a1dd6f91390bce447c01e54c", -1 },
+				{ "Obsidian Data 5.MPX", 0, "894e4712a7bfb1b3c54086d43e6f3bb7", -1 },
+				{ "Obsidian Data 6.MPX", 0, "f491955b858e1a41d25efbb060424833", -1 },
+				AD_LISTEND
+			},
+			Common::EN_ANY,
+			Common::kPlatformWindows,
+			ADGF_NO_FLAGS,
+			GUIO0()
+		},
+		GID_OBSIDIAN,
+		0,
+		0,
+	},
+
+	{ AD_TABLE_END_MARKER, 0, 0, 0 }
+};
+
+/**
+ * The fallback game descriptor used by the Made engine's fallbackDetector.
+ * Contents of this struct are to be overwritten by the fallbackDetector.
+ */
+static MTropolisGameDescription g_fallbackDesc = {
+	{
+		"",
+		"",
+		AD_ENTRY1(0, 0), // This should always be AD_ENTRY1(0, 0) in the fallback descriptor
+		Common::UNK_LANG,
+		Common::kPlatformWindows,
+		ADGF_NO_FLAGS,
+		GUIO0()
+	},
+	0,
+	0,
+	0,
+};
+
+} // End of namespace MTropolisEngine
+
+#endif
diff --git a/engines/mtropolis/metaengine.cpp b/engines/mtropolis/metaengine.cpp
new file mode 100644
index 00000000000..d80b93f72ef
--- /dev/null
+++ b/engines/mtropolis/metaengine.cpp
@@ -0,0 +1,69 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "engines/advancedDetector.h"
+#include "mtropolis/detection.h"
+#include "mtropolis/mtropolis.h"
+
+namespace MTropolis {
+
+uint32 MTropolisEngine::getGameID() const {
+	return _gameDescription->gameID;
+}
+
+Common::Platform MTropolisEngine::getPlatform() const {
+	return _gameDescription->desc.platform;
+}
+
+uint16 MTropolisEngine::getVersion() const {
+	return _gameDescription->version;
+}
+
+} // End of namespace MTropolis
+
+class MTropolisMetaEngine : public AdvancedMetaEngine {
+public:
+	const char *getName() const override {
+		return "mtropolis";
+	}
+
+	bool hasFeature(MetaEngineFeature f) const override;
+	Common::Error createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const override;
+};
+
+bool MTropolisMetaEngine::hasFeature(MetaEngineFeature f) const {
+	return false;
+}
+
+bool MTropolis::MTropolisEngine::hasFeature(EngineFeature f) const {
+	return (f == kSupportsReturnToLauncher);
+}
+
+Common::Error MTropolisMetaEngine::createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const {
+	*engine = new MTropolis::MTropolisEngine(syst, (const MTropolis::MTropolisGameDescription *)desc);
+	return Common::kNoError;
+}
+
+#if PLUGIN_ENABLED_DYNAMIC(MTROPOLIS)
+REGISTER_PLUGIN_DYNAMIC(MTROPOLIS, PLUGIN_TYPE_ENGINE, MTropolisMetaEngine);
+#else
+REGISTER_PLUGIN_STATIC(MTROPOLIS, PLUGIN_TYPE_ENGINE, MTropolisMetaEngine);
+#endif
diff --git a/engines/mtropolis/module.mk b/engines/mtropolis/module.mk
new file mode 100644
index 00000000000..69406ac5e00
--- /dev/null
+++ b/engines/mtropolis/module.mk
@@ -0,0 +1,21 @@
+MODULE := engines/mtropolis
+
+MODULE_OBJS = \
+	console.o \
+	data.o \
+	detection.o \
+	metaengine.o \
+	mtropolis.o \
+	runtime.o \
+	vthread.o
+
+# This module can be built as a plugin
+ifeq ($(ENABLE_MTROPOLIS), DYNAMIC_PLUGIN)
+PLUGIN := 1
+endif
+
+# Include common rules
+include $(srcdir)/rules.mk
+
+# Detection objects
+DETECT_OBJS += $(MODULE)/detection.o
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
new file mode 100644
index 00000000000..459f0ebd76c
--- /dev/null
+++ b/engines/mtropolis/mtropolis.cpp
@@ -0,0 +1,83 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mtropolis/mtropolis.h"
+#include "mtropolis/console.h"
+#include "mtropolis/runtime.h"
+
+#include "common/config-manager.h"
+
+namespace MTropolis {
+
+MTropolisEngine::MTropolisEngine(OSystem *syst, const MTropolisGameDescription *gameDesc) : Engine(syst), _gameDescription(gameDesc) {
+
+	if (gameDesc->gameID == GID_OBSIDIAN) {
+		const Common::FSNode gameDataDir(ConfMan.get("path"));
+		SearchMan.addSubDirectoryMatching(gameDataDir, "Obsidian");
+		SearchMan.addSubDirectoryMatching(gameDataDir, "Obsidian/RESOURCE");
+	}
+}
+
+MTropolisEngine::~MTropolisEngine() {
+
+}
+
+void MTropolisEngine::handleEvents() {
+
+}
+
+Common::Error MTropolisEngine::run() {
+
+	_runtime.reset(new Runtime());
+
+	if (_gameDescription->gameID == GID_OBSIDIAN && _gameDescription->desc.platform == Common::kPlatformWindows) {
+		_runtime->addVolume(0, "Installed", true);
+		_runtime->addVolume(1, "OBSIDIAN1", true);
+		_runtime->addVolume(2, "OBSIDIAN2", true);
+		_runtime->addVolume(3, "OBSIDIAN3", true);
+		_runtime->addVolume(4, "OBSIDIAN4", true);
+		_runtime->addVolume(5, "OBSIDIAN5", true);
+
+		Common::SharedPtr<ProjectDescription> desc(new ProjectDescription());
+		desc->addSegment(0, "Obsidian Data 1.MPL");
+		desc->addSegment(1, "Obsidian Data 2.MPX");
+		desc->addSegment(2, "Obsidian Data 3.MPX");
+		desc->addSegment(3, "Obsidian Data 4.MPX");
+		desc->addSegment(4, "Obsidian Data 5.MPX");
+		desc->addSegment(5, "Obsidian Data 6.MPX");
+
+		_runtime->queueProject(desc);
+	}
+
+	while (!shouldQuit()) {
+		_runtime->runFrame();
+	}
+
+	_runtime.release();
+
+	return Common::kNoError;
+}
+
+void MTropolisEngine::pauseEngineIntern(bool pause) {
+	Engine::pauseEngineIntern(pause);
+}
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/mtropolis.h b/engines/mtropolis/mtropolis.h
new file mode 100644
index 00000000000..f38ee178ca3
--- /dev/null
+++ b/engines/mtropolis/mtropolis.h
@@ -0,0 +1,74 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_MTROPOLIS_H
+#define MTROPOLIS_MTROPOLIS_H
+
+#include "mtropolis/detection.h"
+
+#include "engines/engine.h"
+
+#include "common/random.h"
+
+/**
+ * This is the namespace of the mTropolis engine.
+ *
+ * Status of this engine: ???
+ *
+ * Games using this engine:
+ * - Obsidian
+ */
+namespace MTropolis {
+
+class Runtime;
+
+class MTropolisEngine : public ::Engine {
+protected:
+
+	// Engine APIs
+	Common::Error run() override;
+
+public:
+	MTropolisEngine(OSystem *syst, const MTropolisGameDescription *gameDesc);
+	~MTropolisEngine() override;
+
+	bool hasFeature(EngineFeature f) const override;
+	//void syncSoundSettings() override;
+
+	const MTropolisGameDescription *_gameDescription;
+	uint32 getGameID() const;
+	uint16 getVersion() const;
+	Common::Platform getPlatform() const;
+
+public:
+
+	void handleEvents();
+
+protected:
+	void pauseEngineIntern(bool pause) override;
+
+private:
+	Common::ScopedPtr<Runtime> _runtime;
+};
+
+} // End of namespace MTropolis
+
+#endif /* MTROPOLIS_H */
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
new file mode 100644
index 00000000000..66a683cab6c
--- /dev/null
+++ b/engines/mtropolis/runtime.cpp
@@ -0,0 +1,291 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mtropolis/runtime.h"
+#include "mtropolis/data.h"
+#include "mtropolis/vthread.h"
+
+#include "common/file.h"
+#include "common/substream.h"
+
+
+namespace MTropolis {
+
+ProjectDescription::ProjectDescription() {
+}
+
+ProjectDescription::~ProjectDescription() {
+}
+
+void ProjectDescription::addSegment(int volumeID, const char *filePath) {
+	SegmentDescription desc;
+	desc.volumeID = volumeID;
+	desc.filePath = filePath;
+
+	_segments.push_back(desc);
+}
+
+const Common::Array<SegmentDescription> &ProjectDescription::getSegments() const {
+	return _segments;
+}
+
+Structural::~Structural() {
+}
+
+ProjectPresentationSettings::ProjectPresentationSettings() : width(640), height(480), bitsPerPixel(8) {
+}
+
+const Common::Array<Common::SharedPtr<Structural> > &Structural::getChildren() const {
+	return _children;
+}
+
+const Common::Array<Common::SharedPtr<Modifier> > &Structural::getModifiers() const {
+	return _modifiers;
+}
+
+Runtime::Runtime() {
+	_vthread.reset(new VThread());
+}
+
+void Runtime::runFrame() {
+	VThreadState state = _vthread->step();
+	if (state != kVThreadReturn) {
+		// Still doing blocking tasks
+		return;
+	}
+
+	if (_queuedProjectDesc) {
+		Common::SharedPtr<ProjectDescription> desc = _queuedProjectDesc;
+		_queuedProjectDesc.reset();
+
+		_project.reset();
+		_project.reset(new Project());
+
+		_project->loadFromDescription(*desc);
+	}
+}
+
+void Runtime::queueProject(const Common::SharedPtr<ProjectDescription> &desc) {
+	_queuedProjectDesc = desc;
+}
+
+void Runtime::addVolume(int volumeID, const char *name, bool isMounted) {
+	VolumeState volume;
+	volume.name = name;
+	volume.isMounted = isMounted;
+	volume.volumeID = volumeID;
+
+	_volumes.push_back(volume);
+}
+
+
+Project::Project()
+	: _projectFormat(Data::kProjectFormatUnknown), _isBigEndian(false) {
+}
+
+Project::~Project() {
+}
+
+void Project::loadFromDescription(const ProjectDescription& desc) {
+	size_t numSegments = desc.getSegments().size();
+	_segments.resize(numSegments);
+
+	for (size_t i = 0; i < numSegments; i++) {
+		_segments[i].desc = desc.getSegments()[i];
+	}
+
+	// Try to open the first segment
+	openSegmentStream(0);
+
+	Common::SeekableReadStream *baseStream = _segments[0].stream.get();
+	uint16_t startValue = baseStream->readUint16LE();
+
+	if (startValue == 1) {
+		// Windows format
+		_isBigEndian = false;
+		_projectFormat = Data::kProjectFormatWindows;
+	} else if (startValue == 0) {
+		// Mac format
+		_isBigEndian = true;
+		_projectFormat = Data::kProjectFormatMacintosh;
+	} else {
+		error("Unrecognized project segment header");
+	}
+
+	Common::SeekableSubReadStreamEndian stream(baseStream, 2, baseStream->size(), _isBigEndian);
+	if (stream.readUint32() != 0xaa55a5a5 || stream.readUint32() != 0 || stream.readUint32() != 14) {
+		error("Unrecognized project segment header");
+	}
+
+	Data::DataReader reader(stream, _projectFormat);
+
+	Common::SharedPtr<Data::DataObject> dataObject;
+	Data::loadDataObject(reader, dataObject);
+
+	if (!dataObject || dataObject->getType() != Data::DataObjectTypes::kProjectHeader) {
+		error("Expected project header but found something else");
+
+	}
+
+	Data::loadDataObject(reader, dataObject);
+	if (!dataObject || dataObject->getType() != Data::DataObjectTypes::kProjectCatalog) {
+		error("Expected project catalog but found something else");
+	}
+
+	Data::ProjectCatalog *catalog = static_cast<Data::ProjectCatalog *>(dataObject.get());
+
+	if (catalog->segments.size() != desc.getSegments().size()) {
+		error("Project declared a different number of segments than the project description provided");
+	}
+
+	_streams.resize(catalog->streams.size());
+	for (size_t i = 0; i < _streams.size(); i++) {
+		StreamDesc &streamDesc = _streams[i];
+		const Data::ProjectCatalog::StreamDesc &srcStream = catalog->streams[i];
+
+		if (!strcmp(srcStream.streamType, "assetStream"))
+			streamDesc.streamType = kStreamTypeAsset;
+		else if (!strcmp(srcStream.streamType, "bootStream"))
+			streamDesc.streamType = kStreamTypeBoot;
+		else if (!strcmp(srcStream.streamType, "sceneStream"))
+			streamDesc.streamType = kStreamTypeScene;
+		else
+			streamDesc.streamType = kStreamTypeUnknown;
+
+		streamDesc.segmentIndex = srcStream.segmentIndexPlusOne - 1;
+		streamDesc.size = srcStream.size;
+		streamDesc.pos = srcStream.pos;
+	}
+
+	// Locate the boot stream
+	size_t bootStreamIndex = 0;
+	bool foundBootStream = false;
+	for (size_t i = 0; i < _streams.size(); i++) {
+		if (_streams[i].streamType == kStreamTypeBoot) {
+			bootStreamIndex = i;
+			foundBootStream = true;
+			break;
+		}
+	}
+	if (!foundBootStream) {
+		error("Failed to find boot stream");
+	}
+
+	loadBootStream(bootStreamIndex);
+}
+
+void Project::openSegmentStream(int segmentIndex) {
+	if (segmentIndex < 0 || static_cast<size_t>(segmentIndex) > _segments.size()) {
+		error("Invalid segment index %i", segmentIndex);
+	}
+
+	Segment &segment = _segments[segmentIndex];
+
+	if (segment.stream)
+		return;
+
+	Common::File *f = new Common::File();
+	segment.stream.reset(f);
+
+	if (!f->open(segment.desc.filePath)) {
+		error("Failed to open segment file %s", segment.desc.filePath.c_str());
+	}
+}
+
+void Project::loadBootStream(size_t streamIndex) {
+	const StreamDesc &streamDesc = _streams[streamIndex];
+
+	size_t segmentIndex = streamDesc.segmentIndex;
+	openSegmentStream(segmentIndex);
+
+	Common::SeekableSubReadStreamEndian stream(_segments[segmentIndex].stream.get(), streamDesc.pos, streamDesc.pos + streamDesc.size, _isBigEndian);
+	Data::DataReader reader(stream, _projectFormat);
+
+	for (;;) {
+		Common::SharedPtr<Data::DataObject> dataObject;
+		Data::loadDataObject(reader, dataObject);
+
+		if (!dataObject) {
+			error("Failed to load project boot data");
+		}
+
+		switch (dataObject->getType()) {
+		case Data::DataObjectTypes::kPresentationSettings:
+			loadPresentationSettings(*static_cast<const Data::PresentationSettings *>(dataObject.get()));
+			break;
+		case Data::DataObjectTypes::kAssetCatalog:
+			loadAssetCatalog(*static_cast<const Data::AssetCatalog *>(dataObject.get()));
+			break;
+		case Data::DataObjectTypes::kStreamHeader:
+			// Ignore
+			break;
+		default:
+			error("Unexpected object type in boot stream");
+		}
+	}
+}
+
+void Project::loadPresentationSettings(const Data::PresentationSettings &presentationSettings) {
+	_presentationSettings.bitsPerPixel = presentationSettings.bitsPerPixel;
+	if (_presentationSettings.bitsPerPixel != 16) {
+		error("Unsupported bit depth");
+	}
+	_presentationSettings.width = presentationSettings.dimensions.x;
+	_presentationSettings.height = presentationSettings.dimensions.y;
+}
+
+void Project::loadAssetCatalog(const Data::AssetCatalog &assetCatalog) {
+	_assetsByID.clear();
+	_realAssets.clear();
+	_assetNameToID.clear();
+
+	size_t numRealAssets = 0;
+	for (size_t i = 0; i < assetCatalog.assets.size(); i++) {
+		const Data::AssetCatalog::AssetInfo &assetInfo = assetCatalog.assets[i];
+		if ((assetInfo.flags1 & Data::AssetCatalog::kFlag1Deleted) == 0)
+			numRealAssets++;
+	}
+
+	_realAssets.resize(numRealAssets);
+	_assetsByID.resize(assetCatalog.assets.size() + 1);
+
+	_assetsByID[0] = nullptr;
+
+	numRealAssets = 0;
+	for (size_t i = 0; i < assetCatalog.assets.size(); i++) {
+		const Data::AssetCatalog::AssetInfo &assetInfo = assetCatalog.assets[i];
+		if (assetInfo.flags1 & Data::AssetCatalog::kFlag1Deleted) {
+			_assetsByID[i + 1] = nullptr;
+		} else {
+			AssetDesc &assetDesc = _realAssets[numRealAssets++];
+
+			assetDesc.id = i + 1;
+			assetDesc.name = assetInfo.name;
+			assetDesc.typeCode = assetInfo.assetType;
+
+			_assetsByID[assetDesc.id] = &assetDesc;
+			if (!assetDesc.name.empty())
+				_assetNameToID[assetDesc.name] = assetDesc.id;
+		}
+	}
+}
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
new file mode 100644
index 00000000000..4a09851e633
--- /dev/null
+++ b/engines/mtropolis/runtime.h
@@ -0,0 +1,185 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_RUNTIME_H
+#define MTROPOLIS_RUNTIME_H
+
+#include "common/array.h"
+#include "common/platform.h"
+#include "common/ptr.h"
+#include "common/stream.h"
+#include "common/hashmap.h"
+#include "common/hash-str.h"
+
+#include "mtropolis/data.h"
+#include "mtropolis/vthread.h"
+
+namespace MTropolis {
+
+class Project;
+class Modifier;
+
+struct SegmentDescription {
+	int volumeID;
+	Common::String filePath;
+};
+
+class ProjectDescription {
+
+public:
+	ProjectDescription(); 
+	~ProjectDescription();
+
+	void addSegment(int volumeID, const char *filePath);
+	const Common::Array<SegmentDescription> &getSegments() const;
+
+private:
+	Common::Array<SegmentDescription> _segments;
+};
+
+struct VolumeState {
+	Common::String name;
+	int volumeID;
+	bool isMounted;
+};
+
+class Runtime {
+public:
+	Runtime();
+
+	void runFrame();
+	void queueProject(const Common::SharedPtr<ProjectDescription> &desc);
+
+	void addVolume(int volumeID, const char *name, bool isMounted);
+
+private:
+	Common::Array<VolumeState> _volumes;
+	Common::SharedPtr<ProjectDescription> _queuedProjectDesc;
+	Common::SharedPtr<Project> _project;
+	Common::ScopedPtr<VThread> _vthread;
+};
+
+class Structural {
+
+public:
+	virtual ~Structural();
+
+	const Common::Array<Common::SharedPtr<Structural> > &getChildren() const;
+	const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const;
+
+private:
+	Common::Array<Common::SharedPtr<Structural> > _children;
+	Common::Array<Common::SharedPtr<Modifier> > _modifiers;
+};
+
+struct ProjectPresentationSettings {
+	ProjectPresentationSettings();
+
+	uint16 width;
+	uint16 height;
+	uint32 bitsPerPixel;
+};
+
+class AssetInfo {
+
+public:
+	AssetInfo();
+	virtual ~AssetInfo();
+
+	void setSegmentIndex(int segmentIndex);
+	int getSegmentIndex() const;
+
+private:
+	int _segmentIndex;
+};
+
+class Project : public Structural {
+
+public:
+	Project();
+	~Project();
+
+	void loadFromDescription(const ProjectDescription &desc);
+
+private:
+	struct Segment {
+		SegmentDescription desc;
+		Common::SharedPtr<Common::SeekableReadStream> stream;
+	};
+
+	enum StreamType {
+		kStreamTypeUnknown,
+
+		kStreamTypeAsset,
+		kStreamTypeBoot,
+		kStreamTypeScene,
+	};
+
+	struct StreamDesc {
+		StreamType streamType;
+		uint16 segmentIndex;
+		uint32 size;
+		uint32 pos;
+	};
+
+	struct AssetDesc {
+		uint32 typeCode;
+		size_t id;
+		Common::String name;
+
+		// If the asset is live, this will be its asset info
+		Common::SharedPtr<AssetInfo> assetInfo;
+	};
+
+	void openSegmentStream(int segmentIndex);
+	void loadBootStream(size_t streamIndex);
+
+	void loadPresentationSettings(const Data::PresentationSettings &presentationSettings);
+	void loadAssetCatalog(const Data::AssetCatalog &assetCatalog);
+
+	Common::Array<Segment> _segments;
+	Common::Array<StreamDesc> _streams;
+	Data::ProjectFormat _projectFormat;
+	bool _isBigEndian;
+
+	Common::Array<AssetDesc *> _assetsByID;
+	Common::Array<AssetDesc> _realAssets;
+
+	Common::HashMap<Common::String, size_t> _assetNameToID;
+
+	ProjectPresentationSettings _presentationSettings;
+};
+
+class Section : public Structural {
+};
+
+class Subsection : public Structural {
+};
+
+class Scene : public Structural {
+};
+
+class Modifier {
+};
+
+} // End of namespace MTropolis
+
+#endif
diff --git a/engines/mtropolis/vthread.cpp b/engines/mtropolis/vthread.cpp
new file mode 100644
index 00000000000..a6f6606927c
--- /dev/null
+++ b/engines/mtropolis/vthread.cpp
@@ -0,0 +1,175 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mtropolis/vthread.h"
+
+namespace MTropolis {
+
+VThreadTaskData::~VThreadTaskData() {
+}
+
+VThread::VThread() : _stackUnalignedBase(nullptr), _stackAlignedBase(nullptr), _size(0), _alignment(1), _used(0) {
+}
+
+
+VThread::~VThread() {
+	void *dataPtr;
+	void *framePtr;
+	while (popFrame(dataPtr, framePtr)) {
+		static_cast<VThreadStackFrame *>(framePtr)->~VThreadStackFrame();
+		static_cast<VThreadTaskData *>(dataPtr)->~VThreadTaskData();
+	}
+
+	if (_stackUnalignedBase)
+		free(_stackUnalignedBase);
+}
+
+VThreadState VThread::step() {
+	void *dataPtr;
+	void *framePtr;
+	while (popFrame(dataPtr, framePtr)) {
+		static_cast<VThreadStackFrame *>(framePtr)->~VThreadStackFrame();
+		VThreadState state = static_cast<VThreadTaskData *>(dataPtr)->destructAndRunTask();
+		if (state != kVThreadReturn)
+			return state;
+	}
+
+	return kVThreadReturn;
+}
+
+void VThread::reserveFrame(size_t size, size_t alignment, void *&outFramePtr, void *&outUnadjustedDataPtr, size_t &outPrevFrameOffset) {
+	const size_t frameAlignment = offsetof(VThreadAlignmentHelper<VThreadStackFrame>, item);
+	const size_t frameAlignmentMask = frameAlignment - 1;
+
+	size_t dataAlignmentMask = alignment - 1;
+
+	bool needToReallocate = false;
+	if (alignment > _alignment || frameAlignment > _alignment) {
+		if ((reinterpret_cast<uintptr_t>(_stackAlignedBase) & dataAlignmentMask) != 0) {
+			needToReallocate = true;
+		}
+	}
+
+	size_t dataAlignmentPaddingNeeded = (alignment - (_used & dataAlignmentMask));
+	if (dataAlignmentPaddingNeeded == alignment)
+		dataAlignmentPaddingNeeded = 0;
+
+	size_t offsetOfData = dataAlignmentPaddingNeeded + _used;
+	size_t offsetOfEndOfData = offsetOfData + size;
+	size_t frameAlignmentPaddingNeeded = (frameAlignment - (offsetOfData & frameAlignmentMask));
+	if (frameAlignmentPaddingNeeded == frameAlignment)
+		frameAlignmentPaddingNeeded = 0;
+
+	size_t offsetOfFrame = offsetOfEndOfData + frameAlignmentPaddingNeeded;
+	size_t offsetOfEndOfFrame = offsetOfFrame + sizeof(VThreadStackFrame);
+
+	size_t offsetOfPrevFrame = 0;
+	if (_used > 0)
+		offsetOfPrevFrame = _used - sizeof(VThreadStackFrame);
+
+	if (offsetOfEndOfFrame > _used)
+		needToReallocate = true;
+
+	if (needToReallocate) {
+		size_t maxAlignment = alignment;
+		if (maxAlignment < frameAlignment)
+			maxAlignment = frameAlignment;
+
+		void *unalignedBase = malloc(offsetOfEndOfFrame + maxAlignment - 1);
+		size_t alignPadding = maxAlignment - (reinterpret_cast<uintptr_t>(unalignedBase) % maxAlignment);
+		if (alignPadding == maxAlignment)
+			alignPadding = 0;
+
+		void *alignedBase = static_cast<char *>(unalignedBase) + alignPadding;
+
+		// Copy the previous frames
+		size_t framePos = 0;
+		if (_used > 0) {
+			framePos = _used - sizeof(VThreadStackFrame);
+#ifdef MTROPOLIS_DEBUG_VTHREAD_STACKS
+			VThreadStackFrame *nextFrame = nullptr;
+#endif
+
+			while (framePos != 0) {
+				VThreadStackFrame *oldFrame = reinterpret_cast<VThreadStackFrame *>(static_cast<char *>(_stackAlignedBase) + framePos);
+				size_t dataPos = oldFrame->taskDataOffset;
+				VThreadTaskData *oldData = reinterpret_cast<VThreadTaskData *>(static_cast<char *>(_stackAlignedBase) + dataPos);
+				size_t nextPos = oldFrame->prevFrameOffset;
+
+				VThreadStackFrame *newFrame = reinterpret_cast<VThreadStackFrame *>(static_cast<char *>(alignedBase) + framePos);
+				VThreadTaskData *newData = reinterpret_cast<VThreadTaskData *>(static_cast<char *>(alignedBase) + dataPos);
+
+				// Relocate the frame
+				new (newFrame) VThreadStackFrame(*oldFrame);
+				oldFrame->~VThreadStackFrame();
+
+				// Relocate the data
+				oldData->relocateTo(newData);
+				oldData->~VThreadTaskData();
+
+#ifdef MTROPOLIS_DEBUG_VTHREAD_STACKS
+				newFrame->data = newData;
+				newFrame->prevFrame = nullptr;
+				if (nextFrame)
+					nextFrame->prevFrame = newFrame;
+
+				nextFrame = newFrame;
+#endif
+
+				framePos = nextPos;
+			}
+		}
+
+		if (_stackUnalignedBase)
+			free(_stackUnalignedBase);
+
+		_stackUnalignedBase = unalignedBase;
+		_stackAlignedBase = alignedBase;
+	}
+
+	VThreadStackFrame *newFrame = reinterpret_cast<VThreadStackFrame *>(static_cast<char *>(_stackAlignedBase) + offsetOfFrame);
+	void *newData = static_cast<char *>(_stackAlignedBase) + offsetOfData;
+	_used = offsetOfEndOfFrame;
+
+	outFramePtr = newFrame;
+	outUnadjustedDataPtr = newData;
+	outPrevFrameOffset = offsetOfPrevFrame;
+}
+
+bool VThread::popFrame(void *&dataPtr, void *&outFramePtr) {
+	if (_used == 0)
+		return false;
+
+	VThreadStackFrame *frame = reinterpret_cast<VThreadStackFrame *>(static_cast<char *>(_stackAlignedBase) + _used - sizeof(VThreadStackFrame));
+	VThreadTaskData *data = reinterpret_cast<VThreadTaskData *>(static_cast<char *>(_stackAlignedBase) + frame->taskDataOffset);
+
+	dataPtr = data;
+	outFramePtr = frame;
+
+	if (frame->prevFrameOffset == 0)
+		_used = 0;
+	else
+		_used = frame->prevFrameOffset + sizeof(VThreadStackFrame);
+
+	return true;
+}
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/vthread.h b/engines/mtropolis/vthread.h
new file mode 100644
index 00000000000..fd07ed5bb44
--- /dev/null
+++ b/engines/mtropolis/vthread.h
@@ -0,0 +1,285 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_TASKSTACK_H
+#define MTROPOLIS_TASKSTACK_H
+
+#include <new>
+
+namespace MTropolis {
+
+#define MTROPOLIS_DEBUG_VTHREAD_STACKS
+
+// Virtual thread, really a task stack
+enum VThreadState {
+	kVThreadReturn,
+	kVThreadSuspend,
+	kVThreadError,
+};
+
+template<typename TClass>
+struct VThreadAlignmentHelper {
+	char prefix;
+	TClass item;
+};
+
+
+class VThreadTaskData {
+
+public:
+	virtual ~VThreadTaskData();
+
+	virtual VThreadState destructAndRunTask() = 0;
+	virtual void relocateTo(void *newPosition) = 0;
+};
+
+struct VThreadStackFrame {
+	size_t taskDataOffset;	// Offset to VThreadTaskData
+	size_t prevFrameOffset;
+
+#ifdef MTROPOLIS_DEBUG_VTHREAD_STACKS
+	VThreadTaskData *data;
+	VThreadStackFrame *prevFrame;
+#endif
+};
+
+template<typename TClass, typename TData>
+class VThreadMethodData : public VThreadTaskData {
+
+public:
+	VThreadMethodData(TClass *target, VThreadState (TClass::*method)(const TData &data));
+	VThreadMethodData(const VThreadMethodData &other);
+
+#if __cplusplus >= 201103L
+	VThreadMethodData(VThreadMethodData &&other);
+#endif
+
+	VThreadState destructAndRunTask() override;
+	void relocateTo(void *newPosition) override;
+
+	TData &getData();
+
+private:
+	TClass *_target;
+	VThreadState (TClass::*_method)(const TData &data);
+	TData _data;
+};
+
+template<typename TData>
+class VThreadFunctionData : public VThreadTaskData {
+
+public:
+	explicit VThreadFunctionData(VThreadState (*func)(const TData &data));
+	VThreadFunctionData(const VThreadFunctionData &other);
+
+#if __cplusplus >= 201103L
+	VThreadFunctionData(VThreadMethodData &&other);
+#endif
+
+	VThreadState destructAndRunTask() override;
+	void relocateTo(void *newPosition) override;
+
+	TData &getData();
+
+private:
+	VThreadState (*_func)(const TData &data);
+	TData _data;
+};
+
+class VThread {
+
+public:
+	VThread();
+	~VThread();
+
+	template<typename TClass, typename TData>
+	TData *pushTask(TClass *obj, VThreadState (TClass::*method)(const TData &data));
+
+	template<typename TData>
+	TData *pushTask(VThreadState (*func)(const TData &data));
+
+	VThreadState step();
+
+private:
+
+	void reserveFrame(size_t size, size_t alignment, void *&outFramePtr, void *&outUnadjustedDataPtr, size_t &outPrevFrameOffset);
+	bool popFrame(void *&dataPtr, void *&outFramePtr);
+
+	void *_stackUnalignedBase;
+	void *_stackAlignedBase;
+	size_t _size;
+	size_t _alignment;
+	size_t _used;
+};
+
+template<typename TClass, typename TData>
+VThreadMethodData<TClass, TData>::VThreadMethodData(TClass *target, VThreadState (TClass::*method)(const TData &data)) : _target(target), _method(method) {
+}
+
+template<typename TClass, typename TData>
+VThreadMethodData<TClass, TData>::VThreadMethodData(const VThreadMethodData& other) : _target(other._target), _method(other._method), _data(other._data) {
+}
+
+#if __cplusplus >= 201103L
+
+template<typename TClass, typename TData>
+VThreadMethodData<TClass, TData>::VThreadMethodData(VThreadMethodData &&other) : _target(other._target), _method(other._method), _data(static_cast<TData &&>(*static_cast<TData *>(other._data))) {
+}
+
+#endif
+
+template<typename TClass, typename TData>
+VThreadState VThreadMethodData<TClass, TData>::destructAndRunTask() {
+#if __cplusplus >= 201103L
+	TData data(static_cast<TData &&>(_data));
+#else
+	TData data(_data);
+#endif
+
+	TClass *target = _target;
+	VThreadState (TClass::*method)(const TData &) = _method;
+
+	this->~VThreadMethodData<TClass, TData>();
+
+	return target->*method(data);
+}
+
+template<typename TClass, typename TData>
+void VThreadMethodData<TClass, TData>::relocateTo(void *newPosition) {
+	void *adjustedPtr = static_cast<VThreadMethodData<TClass, TData> *>(static_cast<VThreadTaskData *>(newPosition));
+
+#if __cplusplus >= 201103L
+	new (adjustedPtr) VThreadMethodData<TClass, TData>(static_cast<VThreadMethodData<TClass, TData> &&>(*this));
+#else
+	new (adjustedPtr) VThreadMethodData<TClass, TData>(*this);
+#endif
+}
+
+template<typename TClass, typename TData>
+TData &VThreadMethodData<TClass, TData>::getData() {
+	return _data;
+}
+
+template<typename TData>
+VThreadFunctionData<TData>::VThreadFunctionData(VThreadState (*func)(const TData &data)) : _func(func) {
+}
+
+template<typename TData>
+VThreadFunctionData<TData>::VThreadFunctionData(const VThreadFunctionData &other) : _func(other._func), _data(other._data) {
+}
+
+#if __cplusplus >= 201103L
+
+template<typename TData>
+VThreadFunctionData<TData>::VThreadFunctionData(VThreadMethodData &&other) : func(other._func), data(static_cast<TData &&>(other._data)) {
+}
+
+#endif
+
+template<typename TData>
+VThreadState VThreadFunctionData<TData>::destructAndRunTask() {
+#if __cplusplus >= 201103L
+	TData data(static_cast<TData &&>(_data));
+#else
+	TData data(_data);
+#endif
+
+	VThreadState (*func)(const TData &) = _func;
+
+	this->~VThreadFunctionData<TData>();
+
+	return func(data);
+}
+
+template<typename TData>
+void VThreadFunctionData<TData>::relocateTo(void *newPosition) {
+	void *adjustedPtr = static_cast<VThreadFunctionData<TData> *>(static_cast<VThreadTaskData *>(newPosition));
+
+#if __cplusplus >= 201103L
+	new (adjustedPtr) VThreadFunctionData<TData>(static_cast<VThreadFunctionData<TData> &&>(*this));
+#else
+	new (adjustedPtr) VThreadFunctionData<TData>(*this);
+#endif
+}
+
+template<typename TData>
+TData &VThreadFunctionData<TData>::getData() {
+	return _data;
+}
+
+template<typename TClass, typename TData>
+TData *VThread::pushTask(TClass *obj, VThreadState (TClass::*method)(const TData &data)) {
+	typedef VThreadMethodData<TClass, TData> FrameData_t; 
+
+	const size_t frameAlignment = offsetof(VThreadAlignmentHelper<VThreadStackFrame>, item);
+	const size_t dataAlignment = offsetof(VThreadAlignmentHelper<FrameData_t>, item);
+	const size_t maxAlignment = (frameAlignment < dataAlignment) ? dataAlignment : frameAlignment;
+
+	void *framePtr;
+	void *dataPtr;
+	size_t dataOffset;
+	size_t prevFrameOffset;
+	reserveFrame(sizeof(FrameData_t), maxAlignment, framePtr, dataPtr, prevFrameOffset);
+
+	VThreadStackFrame *frame = new (framePtr) VThreadStackFrame();
+	frame->prevFrameOffset = prevFrameOffset;
+
+	FrameData_t *frameData = new (dataPtr) FrameData_t(obj, method);
+	frame->taskDataOffset = reinterpret_cast<char *>(static_cast<VThreadTaskData *>(frameData)) - static_cast<char *>(_stackAlignedBase);
+
+#ifdef MTROPOLIS_DEBUG_VTHREAD_STACKS
+	frame->prevFrame = reinterpret_cast<VThreadStackFrame *>(static_cast<char *>(_stackAlignedBase) + prevFrameOffset);
+	frame->data = frameData;
+#endif
+
+	return &frameData->data;
+}
+
+template<typename TData>
+TData *VThread::pushTask(VThreadState (*func)(const TData &data)) {
+	typedef VThreadFunctionData<TData> FrameData_t;
+
+	const size_t frameAlignment = offsetof(VThreadAlignmentHelper<VThreadStackFrame>, item);
+	const size_t dataAlignment = offsetof(VThreadAlignmentHelper<FrameData_t>, item);
+	const size_t maxAlignment = (frameAlignment < dataAlignment) ? dataAlignment : frameAlignment;
+
+	void *framePtr;
+	void *dataPtr;
+	size_t prevFrameOffset;
+	reserveFrame(sizeof(FrameData_t), maxAlignment, framePtr, dataPtr, prevFrameOffset);
+
+	VThreadStackFrame *frame = new (framePtr) VThreadStackFrame();
+	frame->prevFrameOffset = prevFrameOffset;
+
+	FrameData_t *frameData = new (dataPtr) FrameData_t(func);
+	frame->taskDataOffset = reinterpret_cast<char *>(static_cast<VThreadTaskData *>(frameData)) - static_cast<char *>(_stackAlignedBase);
+
+#ifdef MTROPOLIS_DEBUG_VTHREAD_STACKS
+	frame->prevFrame = reinterpret_cast<VThreadStackFrame *>(static_cast<char *>(_stackAlignedBase) + prevFrameOffset);
+	frame->data = frameData;
+#endif
+
+	return &frameData->getData();
+}
+
+} // End of namespace MTropolis
+
+#endif


Commit: 59aea6e16a3b40f7acaf7183b180da9376480c3e
    https://github.com/scummvm/scummvm/commit/59aea6e16a3b40f7acaf7183b180da9376480c3e
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add behavior + modifier container loaders

Changed paths:
  A engines/mtropolis/modifier_factory.cpp
  A engines/mtropolis/modifier_factory.h
  A engines/mtropolis/modifiers.cpp
  A engines/mtropolis/modifiers.h
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/module.mk
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index ebf009e9358..cb2c1dd4ad0 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -248,6 +248,26 @@ DataReadErrorCode AssetCatalog::load(DataReader& reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode Unknown19::load(DataReader &reader) {
+	if (_revision != 0)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(persistFlags) || !reader.readU32(sizeIncludingTag) || !reader.readBytes(unknown1))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode GlobalObjectInfo::load(DataReader &reader) {
+	if (_revision != 0)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(persistFlags) || !reader.readU32(sizeIncludingTag) || !reader.readU16(numGlobalModifiers) || !reader.readBytes(unknown1))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode ProjectCatalog::load(DataReader &reader) {
 	if (_revision != 2 && _revision != 3) {
 		return kDataReadErrorUnsupportedRevision;
@@ -322,6 +342,25 @@ DataReadErrorCode StreamHeader::load(DataReader& reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode BehaviorModifier::load(DataReader& reader) {
+	if (_revision != 1)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(unknown1) || !reader.readU32(sizeIncludingTag)
+		|| !reader.readBytes(unknown2) || !reader.readU32(guid)
+		|| !reader.readU32(unknown4) || !reader.readU16(unknown5)
+		|| !reader.readU32(unknown6) || !editorLayoutPosition.load(reader)
+		|| !reader.readU16(lengthOfName) || !reader.readU16(numChildren))
+		return kDataReadErrorReadFailed;
+
+	if (lengthOfName > 0 && !reader.readTerminatedStr(name, lengthOfName))
+		return kDataReadErrorReadFailed;
+
+	if (!reader.readU32(flags) || !enableWhen.load(reader) || !disableWhen.load(reader) || !reader.readBytes(unknown7))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
 
 DataReadErrorCode loadDataObject(DataReader& reader, Common::SharedPtr<DataObject>& outObject) {
 	uint32 type;
@@ -347,6 +386,15 @@ DataReadErrorCode loadDataObject(DataReader& reader, Common::SharedPtr<DataObjec
 	case DataObjectTypes::kAssetCatalog:
 		dataObject = new AssetCatalog();
 		break;
+	case DataObjectTypes::kUnknown19:
+		dataObject = new Unknown19();
+		break;
+	case DataObjectTypes::kGlobalObjectInfo:
+		dataObject = new GlobalObjectInfo();
+		break;
+	case DataObjectTypes::kBehaviorModifier:
+		dataObject = new BehaviorModifier();
+		break;
 	default:
 		break;
 	}
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index 32139e28d78..39a8b92677e 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -58,6 +58,11 @@ enum DataObjectType {
 	kPresentationSettings = 0x3ec,
 
 	kAssetCatalog         = 0xd,
+    kGlobalObjectInfo     = 0x17,
+	kUnknown19            = 0x19,
+
+	
+	kBehaviorModifier     = 0x2c6,
 };
 
 } // End of namespace DataObjectTypes
@@ -152,21 +157,18 @@ protected:
 };
 
 struct PresentationSettings : public DataObject {
-
-public:
-	DataReadErrorCode load(DataReader &reader) override;
-
 	uint32 persistFlags;
 	uint32 sizeIncludingTag;
 	uint8 unknown1[2];
 	Point dimensions;
 	uint16 bitsPerPixel;
 	uint16 unknown4;
-};
 
-struct AssetCatalog : public DataObject {
+protected:
 	DataReadErrorCode load(DataReader &reader) override;
+};
 
+struct AssetCatalog : public DataObject {
 	enum {
 		kFlag1Deleted = 1,
 		kFlag1LimitOnePerSegment = 2,
@@ -188,6 +190,27 @@ struct AssetCatalog : public DataObject {
 	uint8 unknown1[4];
 	uint32 numAssets;
 	Common::Array<AssetInfo> assets;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+struct Unknown19 : public DataObject {
+	uint32 persistFlags;
+	uint32 sizeIncludingTag;
+	uint8 unknown1[2];
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+struct GlobalObjectInfo : public DataObject {
+	DataReadErrorCode load(DataReader &reader) override;
+
+	uint32 persistFlags;
+	uint32 sizeIncludingTag;
+	uint16 numGlobalModifiers;
+	uint8 unknown1[4];
 };
 
 class ProjectCatalog : public DataObject {
@@ -219,9 +242,8 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
-class StreamHeader : public DataObject {
+struct StreamHeader : public DataObject {
 
-public:
 	uint32 marker;
 	uint32 sizeIncludingTag;
 	char name[17];
@@ -233,6 +255,29 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct BehaviorModifier : public DataObject {
+
+	uint32 unknown1;
+	uint32 sizeIncludingTag;
+	uint8 unknown2[2];
+	uint32 guid;
+	uint32 unknown4;
+	uint16 unknown5;
+	uint32 unknown6;
+	Point editorLayoutPosition;
+	uint16 lengthOfName;
+	uint16 numChildren;
+	uint32 flags;
+	Event enableWhen;
+	Event disableWhen;
+	uint8 unknown7[2];
+
+	Common::String name;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 DataReadErrorCode loadDataObject(DataReader &reader, Common::SharedPtr<DataObject> &outObject);
 
 template<size_t TSize>
diff --git a/engines/mtropolis/modifier_factory.cpp b/engines/mtropolis/modifier_factory.cpp
new file mode 100644
index 00000000000..f38f838e5f7
--- /dev/null
+++ b/engines/mtropolis/modifier_factory.cpp
@@ -0,0 +1,65 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mtropolis/modifiers.h"
+#include "mtropolis/modifier_factory.h"
+
+namespace MTropolis {
+
+template<typename TModifier, typename TModifierData>
+class ModifierFactory : public IModifierFactory {
+public:
+	Common::SharedPtr<Modifier> createModifier(ModifierLoaderContext &context, const Data::DataObject &dataObject) override;
+	static IModifierFactory *getInstance();
+
+private:
+	static ModifierFactory<TModifier, TModifierData> _instance;
+};
+
+template<typename TModifier, typename TModifierData>
+Common::SharedPtr<Modifier> ModifierFactory<TModifier, TModifierData>::createModifier(ModifierLoaderContext &context, const Data::DataObject &dataObject) {
+	Common::SharedPtr<TModifier> modifier(new TModifier());
+
+	if (!modifier->load(context, static_cast<const TModifierData &>(dataObject)))
+		modifier.reset();
+
+	return Common::SharedPtr<Modifier>(modifier);
+}
+
+template<typename TModifier, typename TModifierData>
+IModifierFactory *ModifierFactory<TModifier, TModifierData>::getInstance() {
+	return &_instance;
+}
+
+template<typename TModifier, typename TModifierData>
+ModifierFactory<TModifier, TModifierData> ModifierFactory<TModifier, TModifierData>::_instance;
+
+
+IModifierFactory *getModifierFactoryForDataObjectType(const Data::DataObjectTypes::DataObjectType dataObjectType) {
+	switch (dataObjectType) {
+	case Data::DataObjectTypes::kBehaviorModifier:
+		return ModifierFactory<BehaviorModifier, Data::BehaviorModifier>::getInstance();
+	default:
+		return nullptr;
+	}
+}
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/modifier_factory.h b/engines/mtropolis/modifier_factory.h
new file mode 100644
index 00000000000..164a8054319
--- /dev/null
+++ b/engines/mtropolis/modifier_factory.h
@@ -0,0 +1,42 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_MODIFIER_FACTORY_H
+#define MTROPOLIS_MODIFIER_FACTORY_H
+
+#include "mtropolis/data.h"
+#include "mtropolis/runtime.h"
+
+namespace MTropolis {
+
+struct ModifierLoaderContext {
+	ChildLoaderStack *childLoaderStack;
+};
+
+struct IModifierFactory {
+	virtual Common::SharedPtr<Modifier> createModifier(ModifierLoaderContext &context, const Data::DataObject &dataObject) = 0;
+};
+
+IModifierFactory *getModifierFactoryForDataObjectType(Data::DataObjectTypes::DataObjectType dataObjectType);
+
+} // End of namespace MTropolis
+
+#endif
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
new file mode 100644
index 00000000000..6703edcd611
--- /dev/null
+++ b/engines/mtropolis/modifiers.cpp
@@ -0,0 +1,47 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mtropolis/modifiers.h"
+#include "mtropolis/modifier_factory.h"
+
+namespace MTropolis {
+
+bool BehaviorModifier::load(ModifierLoaderContext &context, const Data::BehaviorModifier &data) {
+	if (data.numChildren > 0) {
+		ChildLoaderContext loaderContext;
+		loaderContext.containerUnion.modifierContainer = this;
+		loaderContext.type = ChildLoaderContext::kTypeModifierList;
+		loaderContext.remainingCount = data.numChildren;
+
+		context.childLoaderStack->contexts.push_back(loaderContext);
+	}
+
+	if (!_enableWhen.load(data.enableWhen) || !_disableWhen.load(data.disableWhen))
+		return false;
+
+	return true;
+}
+
+Common::Array<Common::SharedPtr<Modifier> > &BehaviorModifier::getModifiers() {
+	return _children;
+}
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
new file mode 100644
index 00000000000..aea41a74aa5
--- /dev/null
+++ b/engines/mtropolis/modifiers.h
@@ -0,0 +1,47 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_MODIFIERS_H
+#define MTROPOLIS_MODIFIERS_H
+
+#include "mtropolis/runtime.h"
+#include "mtropolis/data.h"
+
+namespace MTropolis {
+
+struct ModifierLoaderContext;
+
+class BehaviorModifier : public Modifier, public IModifierContainer {
+public:
+	bool load(ModifierLoaderContext &context, const Data::BehaviorModifier &data);
+
+	Common::Array<Common::SharedPtr<Modifier> > &getModifiers() override;
+
+private:
+	Common::Array<Common::SharedPtr<Modifier> > _children;
+	Event _enableWhen;
+	Event _disableWhen;
+};
+
+
+}	// End of namespace MTropolis
+
+#endif
diff --git a/engines/mtropolis/module.mk b/engines/mtropolis/module.mk
index 69406ac5e00..3371453781a 100644
--- a/engines/mtropolis/module.mk
+++ b/engines/mtropolis/module.mk
@@ -5,6 +5,8 @@ MODULE_OBJS = \
 	data.o \
 	detection.o \
 	metaengine.o \
+	modifiers.o \
+	modifier_factory.o \
 	mtropolis.o \
 	runtime.o \
 	vthread.o
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 66a683cab6c..0be8fa9b199 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -22,6 +22,7 @@
 #include "mtropolis/runtime.h"
 #include "mtropolis/data.h"
 #include "mtropolis/vthread.h"
+#include "mtropolis/modifier_factory.h"
 
 #include "common/file.h"
 #include "common/substream.h"
@@ -29,6 +30,16 @@
 
 namespace MTropolis {
 
+Event::Event() : eventType(0), eventInfo(0) {
+}
+
+bool Event::load(const Data::Event& data) {
+	eventType = data.eventID;
+	eventInfo = data.eventInfo;
+
+	return true;
+}
+
 ProjectDescription::ProjectDescription() {
 }
 
@@ -47,6 +58,10 @@ const Common::Array<SegmentDescription> &ProjectDescription::getSegments() const
 	return _segments;
 }
 
+Common::Array<Common::SharedPtr<Modifier> >& SimpleModifierContainer::getModifiers() {
+	return _modifiers;
+}
+
 Structural::~Structural() {
 }
 
@@ -57,7 +72,7 @@ const Common::Array<Common::SharedPtr<Structural> > &Structural::getChildren() c
 	return _children;
 }
 
-const Common::Array<Common::SharedPtr<Modifier> > &Structural::getModifiers() const {
+Common::Array<Common::SharedPtr<Modifier> > &Structural::getModifiers() {
 	return _modifiers;
 }
 
@@ -96,9 +111,13 @@ void Runtime::addVolume(int volumeID, const char *name, bool isMounted) {
 	_volumes.push_back(volume);
 }
 
+const Common::Array<Common::SharedPtr<Modifier> > &IModifierContainer::getModifiers() const {
+	return const_cast<IModifierContainer &>(*this).getModifiers();
+};
+
 
 Project::Project()
-	: _projectFormat(Data::kProjectFormatUnknown), _isBigEndian(false) {
+	: _projectFormat(Data::kProjectFormatUnknown), _isBigEndian(false), _haveGlobalObjectInfo(false) {
 }
 
 Project::~Project() {
@@ -219,6 +238,8 @@ void Project::loadBootStream(size_t streamIndex) {
 	Common::SeekableSubReadStreamEndian stream(_segments[segmentIndex].stream.get(), streamDesc.pos, streamDesc.pos + streamDesc.size, _isBigEndian);
 	Data::DataReader reader(stream, _projectFormat);
 
+	ChildLoaderStack loaderStack;
+
 	for (;;) {
 		Common::SharedPtr<Data::DataObject> dataObject;
 		Data::loadDataObject(reader, dataObject);
@@ -227,18 +248,27 @@ void Project::loadBootStream(size_t streamIndex) {
 			error("Failed to load project boot data");
 		}
 
-		switch (dataObject->getType()) {
-		case Data::DataObjectTypes::kPresentationSettings:
-			loadPresentationSettings(*static_cast<const Data::PresentationSettings *>(dataObject.get()));
-			break;
-		case Data::DataObjectTypes::kAssetCatalog:
-			loadAssetCatalog(*static_cast<const Data::AssetCatalog *>(dataObject.get()));
-			break;
-		case Data::DataObjectTypes::kStreamHeader:
-			// Ignore
-			break;
-		default:
-			error("Unexpected object type in boot stream");
+		if (loaderStack.contexts.size() > 0) {
+			loadRuntimeContextualObject(loaderStack, *dataObject.get());
+		} else {
+			// Root-level objects
+			switch (dataObject->getType()) {
+			case Data::DataObjectTypes::kPresentationSettings:
+				loadPresentationSettings(*static_cast<const Data::PresentationSettings *>(dataObject.get()));
+				break;
+			case Data::DataObjectTypes::kAssetCatalog:
+				loadAssetCatalog(*static_cast<const Data::AssetCatalog *>(dataObject.get()));
+				break;
+			case Data::DataObjectTypes::kGlobalObjectInfo:
+				loadGlobalObjectInfo(loaderStack, *static_cast<const Data::GlobalObjectInfo *>(dataObject.get()));
+				break;
+			case Data::DataObjectTypes::kStreamHeader:
+			case Data::DataObjectTypes::kUnknown19:
+				// Ignore
+				break;
+			default:
+				error("Unexpected object type in boot stream");
+			}
 		}
 	}
 }
@@ -288,4 +318,58 @@ void Project::loadAssetCatalog(const Data::AssetCatalog &assetCatalog) {
 	}
 }
 
+void Project::loadGlobalObjectInfo(ChildLoaderStack& loaderStack, const Data::GlobalObjectInfo& globalObjectInfo) {
+	if (_haveGlobalObjectInfo)
+		error("Multiple global object infos");
+
+	_haveGlobalObjectInfo = true;
+
+	if (globalObjectInfo.numGlobalModifiers > 0) {
+		ChildLoaderContext loaderContext;
+		loaderContext.containerUnion.modifierContainer = &_globalModifiers;
+		loaderContext.remainingCount = globalObjectInfo.numGlobalModifiers;
+		loaderContext.type = ChildLoaderContext::kTypeModifierList;
+
+		loaderStack.contexts.push_back(loaderContext);
+	}
+}
+
+
+static Common::SharedPtr<Modifier> loadModifierObject(ModifierLoaderContext &loaderContext, const Data::DataObject &dataObject) {
+	IModifierFactory *factory = getModifierFactoryForDataObjectType(dataObject.getType());
+
+	if (!factory)
+		error("Unknown or unsupported modifier type, or non-modifier encountered where a modifier was expected");
+
+	Common::SharedPtr<Modifier> modifier = factory->createModifier(loaderContext, dataObject);
+	if (!modifier)
+		error("Modifier object failed to load");
+
+	return modifier;
+}
+
+void loadRuntimeContextualObject(ChildLoaderStack &stack, const Data::DataObject &dataObject) {
+	ChildLoaderContext &topContext = stack.contexts.back();
+
+	// The stack entry must always be popped before loading the object because the load process may descend into more children,
+	// such as when behaviors are nested.
+	switch (topContext.type) {
+	case ChildLoaderContext::kTypeModifierList: {
+			IModifierContainer *container = topContext.containerUnion.modifierContainer;
+
+			if ((--topContext.remainingCount) == 0)
+				stack.contexts.pop_back();
+
+			ModifierLoaderContext loaderContext;
+			loaderContext.childLoaderStack = &stack;
+
+			container->getModifiers().push_back(loadModifierObject(loaderContext, dataObject));
+		}
+		break;
+	}
+}
+
+Modifier::~Modifier() {
+}
+
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 4a09851e633..54c540a96e7 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -61,6 +61,15 @@ struct VolumeState {
 	bool isMounted;
 };
 
+struct Event {
+	Event();
+
+	uint32 eventType;
+	uint32 eventInfo;
+
+	bool load(const Data::Event &data);
+};
+
 class Runtime {
 public:
 	Runtime();
@@ -77,13 +86,27 @@ private:
 	Common::ScopedPtr<VThread> _vthread;
 };
 
-class Structural {
+struct IModifierContainer {
+	virtual Common::Array<Common::SharedPtr<Modifier> > &getModifiers() = 0;
+	const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const;
+};
+
+class SimpleModifierContainer : public IModifierContainer {
+
+	Common::Array<Common::SharedPtr<Modifier> > &getModifiers() override;
+
+private:
+	Common::Array<Common::SharedPtr<Modifier> > _modifiers;
+};
+
+class Structural : public IModifierContainer {
 
 public:
 	virtual ~Structural();
 
 	const Common::Array<Common::SharedPtr<Structural> > &getChildren() const;
-	const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const;
+
+	Common::Array<Common::SharedPtr<Modifier> > &getModifiers() override;
 
 private:
 	Common::Array<Common::SharedPtr<Structural> > _children;
@@ -111,6 +134,25 @@ private:
 	int _segmentIndex;
 };
 
+struct ChildLoaderContext {
+	enum Type {
+		kTypeModifierList,
+		kTypeStructuralList,
+	};
+
+	union ContainerUnion {
+		IModifierContainer *modifierContainer;
+	};
+
+	uint remainingCount;
+	Type type;
+	ContainerUnion containerUnion;
+};
+
+struct ChildLoaderStack {
+	Common::Array<ChildLoaderContext> contexts;
+};
+
 class Project : public Structural {
 
 public:
@@ -154,6 +196,7 @@ private:
 
 	void loadPresentationSettings(const Data::PresentationSettings &presentationSettings);
 	void loadAssetCatalog(const Data::AssetCatalog &assetCatalog);
+	void loadGlobalObjectInfo(ChildLoaderStack &loaderStack, const Data::GlobalObjectInfo &globalObjectInfo);
 
 	Common::Array<Segment> _segments;
 	Common::Array<StreamDesc> _streams;
@@ -166,6 +209,9 @@ private:
 	Common::HashMap<Common::String, size_t> _assetNameToID;
 
 	ProjectPresentationSettings _presentationSettings;
+
+	bool _haveGlobalObjectInfo;
+	SimpleModifierContainer _globalModifiers;
 };
 
 class Section : public Structural {
@@ -178,8 +224,12 @@ class Scene : public Structural {
 };
 
 class Modifier {
+public:
+	virtual ~Modifier();
 };
 
+void loadRuntimeContextualObject(ChildLoaderStack &stack, const Data::DataObject &dataObject);
+
 } // End of namespace MTropolis
 
 #endif


Commit: 4d4588e67501f3b7ca14f808ec615ca5cba7deff
    https://github.com/scummvm/scummvm/commit/4d4588e67501f3b7ca14f808ec615ca5cba7deff
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: More modifiers

Changed paths:
  A engines/mtropolis/alignment_helper.h
  A engines/mtropolis/miniscript.cpp
  A engines/mtropolis/miniscript.h
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/modifier_factory.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/module.mk
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h
    engines/mtropolis/vthread.cpp
    engines/mtropolis/vthread.h


diff --git a/engines/mtropolis/alignment_helper.h b/engines/mtropolis/alignment_helper.h
new file mode 100644
index 00000000000..bc46ac872e6
--- /dev/null
+++ b/engines/mtropolis/alignment_helper.h
@@ -0,0 +1,44 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_ALIGNMENT_HELPER_H
+#define MTROPOLIS_ALIGNMENT_HELPER_H
+
+#include <cstddef>
+
+namespace MTropolis {
+
+template<typename TClass>
+struct AlignmentHelper {
+	char prefix;
+	TClass item;
+
+	static size_t getAlignment();
+};
+
+template<typename TClass>
+inline size_t AlignmentHelper<TClass>::getAlignment() {
+	return offsetof(AlignmentHelper<TClass>, item);
+}
+
+} // End of namespace MTropolis
+
+#endif
diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index cb2c1dd4ad0..6ab1599904b 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -151,6 +151,10 @@ ProjectFormat DataReader::getProjectFormat() const {
 	return _projectFormat;
 }
 
+bool DataReader::isBigEndian() const {
+	return _stream.isBE();
+}
+
 bool Rect::load(DataReader &reader) {
 	if (reader.getProjectFormat() == kProjectFormatMacintosh)
 		return reader.readS16(top) && reader.readS16(left) && reader.readS16(bottom) && reader.readS16(right);
@@ -362,6 +366,120 @@ DataReadErrorCode BehaviorModifier::load(DataReader& reader) {
 	return kDataReadErrorNone;
 }
 
+bool MiniscriptProgram::load(DataReader &reader) {
+	projectFormat = reader.getProjectFormat();
+	isBigEndian = reader.isBigEndian();
+
+	if (!reader.readU32(unknown1) || !reader.readU32(sizeOfInstructions) || !reader.readU32(numOfInstructions) || !reader.readU32(numLocalRefs) || !reader.readU32(numAttributes))
+		return false;
+
+	if (sizeOfInstructions > 0) {
+		bytecode.resize(sizeOfInstructions);
+		if (!reader.read(&bytecode[0], sizeOfInstructions))
+			return false;
+	}
+
+	if (numLocalRefs > 0) {
+		localRefs.resize(numLocalRefs);
+		for (size_t i = 0; i < numLocalRefs; i++) {
+			LocalRef &localRef = localRefs[i];
+			if (!reader.readU32(localRef.guid) || !reader.readU8(localRef.lengthOfName) || !reader.readU8(localRef.unknown2))
+				return false;
+
+			if (localRef.lengthOfName > 0 && !reader.readTerminatedStr(localRef.name, localRef.lengthOfName))
+				return false;
+		}
+	}
+
+	if (numAttributes > 0) {
+		attributes.resize(numAttributes);
+		for (size_t i = 0; i < numAttributes; i++) {
+			Attribute &attrib = attributes[i];
+			if (!reader.readU8(attrib.lengthOfName) || !reader.readU8(attrib.unknown3))
+				return false;
+
+			if (attrib.lengthOfName > 0 && !reader.readTerminatedStr(attrib.name, attrib.lengthOfName))
+				return false;
+		}
+	}
+
+	return true;
+}
+
+DataReadErrorCode MiniscriptModifier::load(DataReader &reader) {
+	if (_revision != 0x3eb)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(unknown1) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid) || !reader.readBytes(unknown3) || !reader.readU32(unknown4) || !reader.readBytes(unknown5) || !reader.readU16(lengthOfName))
+		return kDataReadErrorReadFailed;
+
+	if (lengthOfName > 0 && !reader.readTerminatedStr(name, lengthOfName))
+		return kDataReadErrorReadFailed;
+
+	if (!enableWhen.load(reader) || !reader.readBytes(unknown6) || !reader.readU8(unknown7) || !program.load(reader))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode MessengerModifier::load(DataReader& reader) {
+	if (_revision != 0x3ea)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(unknown1) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid)
+		|| !reader.readBytes(unknown3) || !reader.readU32(unknown4) || !reader.readBytes(unknown5) || !reader.readU16(lengthOfName))
+		return kDataReadErrorReadFailed;
+
+	if (lengthOfName > 0 && !reader.readTerminatedStr(name, lengthOfName))
+		return kDataReadErrorReadFailed;
+
+	// Unlike most cases, the "when" event is split in half in this case
+	if (!reader.readU32(messageFlags) || !reader.readU32(when.eventID) || !send.load(reader) || !reader.readU16(unknown14) || !reader.readU32(destination)
+		|| !reader.readBytes(unknown11) || !reader.readU16(with) || !reader.readBytes(unknown15) || !reader.readU32(withSourceGUID)
+		|| !reader.readBytes(unknown12) || !reader.readU32(when.eventInfo) || !reader.readU8(withSourceLength) || !reader.readU8(unknown13))
+		return kDataReadErrorReadFailed;
+
+	if (withSourceLength > 0 && !reader.readNonTerminatedStr(withSourceName, withSourceLength))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode IfMessengerModifier::load(DataReader &reader) {
+	if (_revision != 0x3ea)
+		return kDataReadErrorReadFailed;
+
+	if (!reader.readU32(unknown1) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid)
+		|| !reader.readBytes(unknown3) || !reader.readU32(unknown4) || !reader.readBytes(unknown5) || !reader.readU16(lengthOfName))
+		return kDataReadErrorReadFailed;
+
+	if (lengthOfName > 0 && !reader.readTerminatedStr(name, lengthOfName))
+		return kDataReadErrorReadFailed;
+
+	if (!reader.readU32(messageFlags) || !when.load(reader) || !send.load(reader) ||
+		!reader.readU16(unknown6) || !reader.readU32(destination) || !reader.readBytes(unknown7) || !reader.readU16(with)
+		|| !reader.readBytes(unknown8) || !reader.readU32(withSourceGUID) || !reader.readBytes(unknown9) || !reader.readU8(withSourceLength) || !reader.readU8(unknown10))
+		return kDataReadErrorReadFailed;
+
+	if (withSourceLength > 0 && !reader.readNonTerminatedStr(withSource, withSourceLength))
+		return kDataReadErrorReadFailed;
+
+	if (!program.load(reader))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode Debris::load(DataReader &reader) {
+	if (_revision != 0)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(persistFlags) || !reader.readU32(sizeIncludingTag))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode loadDataObject(DataReader& reader, Common::SharedPtr<DataObject>& outObject) {
 	uint32 type;
 	uint16 revision;
@@ -395,6 +513,18 @@ DataReadErrorCode loadDataObject(DataReader& reader, Common::SharedPtr<DataObjec
 	case DataObjectTypes::kBehaviorModifier:
 		dataObject = new BehaviorModifier();
 		break;
+	case DataObjectTypes::kMiniscriptModifier:
+		dataObject = new MiniscriptModifier();
+		break;
+	case DataObjectTypes::kMessengerModifier:
+		dataObject = new MessengerModifier();
+		break;
+	case DataObjectTypes::kIfMessengerModifier:
+		dataObject = new IfMessengerModifier();
+		break;
+	case DataObjectTypes::kDebris:
+		dataObject = new Debris();
+		break;
 	default:
 		break;
 	}
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index 39a8b92677e..c598ae2d57f 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -60,9 +60,13 @@ enum DataObjectType {
 	kAssetCatalog         = 0xd,
     kGlobalObjectInfo     = 0x17,
 	kUnknown19            = 0x19,
-
 	
+	kIfMessengerModifier  = 0x2bc,
 	kBehaviorModifier     = 0x2c6,
+	kMessengerModifier    = 0x2da,
+	kMiniscriptModifier   = 0x3c0,
+
+	kDebris               = 0xfffffffe,	// Deleted object
 };
 
 } // End of namespace DataObjectTypes
@@ -98,6 +102,7 @@ public:
 	bool skip(size_t count);
 
 	ProjectFormat getProjectFormat() const;
+	bool isBigEndian() const;
 
 private:
 	Common::SeekableReadStreamEndian &_stream;
@@ -278,6 +283,130 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct MiniscriptProgram {
+	struct LocalRef {
+		uint32 guid;
+		uint8 lengthOfName;
+		uint8 unknown2;
+
+		Common::String name;
+	};
+
+	struct Attribute {
+		uint8 lengthOfName;
+		uint8 unknown3;
+
+		Common::String name;
+	};
+
+	uint32 unknown1;
+	uint32 sizeOfInstructions;
+	uint32 numOfInstructions;
+	uint32 numLocalRefs;
+	uint32 numAttributes;
+
+	Common::Array<uint8> bytecode;
+	Common::Array<LocalRef> localRefs;
+	Common::Array<Attribute> attributes;
+
+	ProjectFormat projectFormat;
+	bool isBigEndian;
+
+	bool load(DataReader &reader);
+};
+
+struct MiniscriptModifier : public DataObject {
+
+	uint32 unknown1;
+	uint32 sizeIncludingTag;
+	uint32 guid;
+	uint8 unknown3[6];
+	uint32 unknown4;
+	uint8 unknown5[4];
+	uint16 lengthOfName;
+	Event enableWhen;
+	uint8 unknown6[11];
+	uint8 unknown7;
+
+	Common::String name;
+
+	MiniscriptProgram program;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+enum MessageFlags {
+	kMessageFlagNoRelay = 0x20000000,
+	kMessageFlagNoCascade = 0x40000000,
+	kMessageFlagNoImmediate = 0x80000000,
+};
+
+struct MessengerModifier : public DataObject {
+	uint32 unknown1;
+	uint32 sizeIncludingTag;
+	uint32 guid;
+	uint8 unknown3[6];
+	uint32 unknown4;
+	uint8 unknown5[4];
+	uint16 lengthOfName;
+	uint32 messageFlags;
+	Event send;
+	Event when;
+	uint16 unknown14;
+	uint32 destination;
+	uint8 unknown11[10];
+	uint16 with;
+	uint8 unknown15[4];
+	uint32 withSourceGUID;
+	uint8 unknown12[36];
+	uint8 withSourceLength;
+	uint8 unknown13;
+
+	Common::String name;
+	Common::String withSourceName;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+struct IfMessengerModifier : public DataObject {
+	uint32 unknown1;
+	uint32 sizeIncludingTag;
+	uint32 guid;
+	uint8 unknown3[6];
+	uint32 unknown4;
+	uint8 unknown5[4];
+	uint16 lengthOfName;
+	uint32 messageFlags;
+	Event send;
+	Event when;
+	uint16 unknown6;
+	uint32 destination;
+	uint8 unknown7[10];
+	uint16 with;
+	uint8 unknown8[4];
+	uint32 withSourceGUID;
+	uint8 unknown9[46];
+	uint8 withSourceLength;
+	uint8 unknown10;
+	MiniscriptProgram program;
+
+	Common::String name;
+	Common::String withSource;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+struct Debris : public DataObject {
+	uint32 persistFlags;
+	uint32 sizeIncludingTag;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 DataReadErrorCode loadDataObject(DataReader &reader, Common::SharedPtr<DataObject> &outObject);
 
 template<size_t TSize>
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
new file mode 100644
index 00000000000..1faf3f5ee52
--- /dev/null
+++ b/engines/mtropolis/miniscript.cpp
@@ -0,0 +1,441 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mtropolis/miniscript.h"
+#include "mtropolis/alignment_helper.h"
+
+#include "common/memstream.h"
+
+#include <new>
+
+namespace MTropolis {
+
+MiniscriptInstruction::~MiniscriptInstruction() {
+}
+
+MiniscriptProgram::MiniscriptProgram(const Common::SharedPtr<Common::Array<uint8> > &programData, const Common::Array<MiniscriptInstruction *> &instructions,
+									 const Common::Array<LocalRef> &localRefs, const Common::Array<Attribute> &attributes)
+	: _programData(programData), _instructions(instructions), _localRefs(localRefs), _attributes(attributes) {
+}
+
+MiniscriptProgram::~MiniscriptProgram() {
+	// Destruct all instructions
+	for (Common::Array<MiniscriptInstruction *>::const_iterator it = _instructions.begin(), itEnd = _instructions.end(); it != itEnd; ++it)
+		(*it)->~MiniscriptInstruction();
+}
+
+
+template<class T>
+struct MiniscriptInstructionLoader {
+	static bool loadInstruction(void *dest, uint32 instrFlags, Data::DataReader &instrDataReader);
+};
+
+template<class T>
+bool MiniscriptInstructionLoader<T>::loadInstruction(void *dest, uint32 instrFlags, Data::DataReader &instrDataReader) {
+	// Default loader for simple instructions with no private data
+	new (dest) T();
+	return true;
+}
+
+template<>
+bool MiniscriptInstructionLoader<MiniscriptInstructions::Send>::loadInstruction(void *dest, uint32 instrFlags, Data::DataReader &instrDataReader) {
+	Data::Event dataEvent;
+	if (!dataEvent.load(instrDataReader))
+		return false;
+
+	Event evt;
+	if (!evt.load(dataEvent))
+		return false;
+
+	new (dest) MiniscriptInstructions::Send(evt);
+	return true;
+}
+
+template<>
+bool MiniscriptInstructionLoader<MiniscriptInstructions::BuiltinFunc>::loadInstruction(void *dest, uint32 instrFlags, Data::DataReader &instrDataReader) {
+	uint32 functionID;
+	if (!instrDataReader.readU32(functionID))
+		return false;
+
+	if (functionID < 1 || functionID > 20)
+		return false;	// Unknown function
+
+	new (dest) MiniscriptInstructions::BuiltinFunc(static_cast<MiniscriptInstructions::BuiltinFunc::BuiltinFunctionID>(functionID));
+	return true;
+}
+
+template<>
+bool MiniscriptInstructionLoader<MiniscriptInstructions::GetChild>::loadInstruction(void *dest, uint32 instrFlags, Data::DataReader &instrDataReader) {
+	uint32 childAttribute;
+	if (!instrDataReader.readU32(childAttribute))
+		return false;
+
+	new (dest) MiniscriptInstructions::GetChild(childAttribute);
+	return true;
+}
+
+template<>
+bool MiniscriptInstructionLoader<MiniscriptInstructions::PushGlobal>::loadInstruction(void *dest, uint32 instrFlags, Data::DataReader &instrDataReader) {
+	uint32 globalID;
+	if (!instrDataReader.readU32(globalID))
+		return false;
+
+	new (dest) MiniscriptInstructions::PushGlobal(globalID);
+	return true;
+}
+
+template<>
+bool MiniscriptInstructionLoader<MiniscriptInstructions::Jump>::loadInstruction(void *dest, uint32 instrFlags, Data::DataReader &instrDataReader) {
+	uint32 jumpFlags, unknown, instrOffset;
+	if (!instrDataReader.readU32(jumpFlags) || !instrDataReader.readU32(unknown) || !instrDataReader.readU32(instrOffset))
+		return false;
+
+	bool isConditional = (jumpFlags == 2);
+	if (jumpFlags != 1 && jumpFlags != 2)
+		return false;	// Don't recognize this flag combination
+
+	if (instrOffset == 0)
+		return false;	// Not valid
+
+	new (dest) MiniscriptInstructions::Jump(instrOffset, isConditional);
+	return true;
+}
+
+template<>
+bool MiniscriptInstructionLoader<MiniscriptInstructions::PushValue>::loadInstruction(void *dest, uint32 instrFlags, Data::DataReader &instrDataReader) {
+	uint16 dataType;
+	if (!instrDataReader.readU16(dataType))
+		return false;
+
+	if (dataType == 0)
+		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeNull, nullptr);
+	else if (dataType == 0x15) {
+		double d;
+		if (!instrDataReader.readF64(d))
+			return false;
+
+		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeDouble, &d);
+	} else if (dataType == 0x1a) {
+		uint8 boolValue;
+		if (!instrDataReader.readU8(boolValue))
+			return false;
+
+		bool b = (boolValue != 0);
+		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeBool, &b);
+	} else if (dataType == 0x1f9) {
+		uint32 refValue;
+		if (!instrDataReader.readU32(refValue))
+			return false;
+
+		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeLocalRef, &refValue);
+
+	} else if (dataType == 0x1fa) {
+		uint32 refValue;
+		if (!instrDataReader.readU32(refValue))
+			return false;
+
+		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeGlobalRef, &refValue);
+	} else
+		return false;
+
+	return true;
+}
+
+template<>
+bool MiniscriptInstructionLoader<MiniscriptInstructions::PushString>::loadInstruction(void *dest, uint32 instrFlags, Data::DataReader &instrDataReader) {
+	uint16 strLength;
+	if (!instrDataReader.readU16(strLength))
+		return false;
+
+	// Unlike most cases, in this case the string is null-terminated but the str length doesn't include the terminator
+	Common::String str;
+	if (!instrDataReader.readTerminatedStr(str, strLength + 1))
+		return false;
+
+	new (dest) MiniscriptInstructions::PushString(str);
+
+	return true;
+}
+
+struct IMiniscriptInstructionFactory {
+	virtual bool create(void *dest, uint32 instrFlags, Data::DataReader &instrDataReader, MiniscriptInstruction *&outMiniscriptInstructionPtr) const = 0;
+	virtual void getSizeAndAlignment(size_t &outSize, size_t &outAlignment) const = 0;
+};
+
+template<class T>
+class MiniscriptInstructionFactory : public IMiniscriptInstructionFactory {
+public:
+	bool create(void *dest, uint32 instrFlags, Data::DataReader &instrDataReader, MiniscriptInstruction *&outMiniscriptInstructionPtr) const override;
+	void getSizeAndAlignment(size_t &outSize, size_t &outAlignment) const override;
+
+	static IMiniscriptInstructionFactory *getInstance();
+
+private:
+	static MiniscriptInstructionFactory<T> _instance;
+};
+
+template<class T>
+bool MiniscriptInstructionFactory<T>::create(void *dest, uint32 instrFlags, Data::DataReader &instrDataReader, MiniscriptInstruction *&outMiniscriptInstructionPtr) const {
+	if (!MiniscriptInstructionLoader<T>::loadInstruction(dest, instrFlags, instrDataReader))
+		return false;
+
+	outMiniscriptInstructionPtr = static_cast<MiniscriptInstruction *>(static_cast<T *>(dest));
+	return true;
+}
+
+template<class T>
+void MiniscriptInstructionFactory<T>::getSizeAndAlignment(size_t& outSize, size_t& outAlignment) const {
+	outSize = sizeof(T);
+	outAlignment = AlignmentHelper<T>::getAlignment();
+}
+
+template<class T>
+inline IMiniscriptInstructionFactory* MiniscriptInstructionFactory<T>::getInstance() {
+	return &_instance;
+}
+
+template<class T>
+MiniscriptInstructionFactory<T> MiniscriptInstructionFactory<T>::_instance;
+
+
+Common::SharedPtr<MiniscriptProgram> MiniscriptParser::parse(const Data::MiniscriptProgram &program) {
+	Common::Array<MiniscriptProgram::LocalRef> localRefs;
+	Common::Array<MiniscriptProgram::Attribute> attributes;
+	Common::SharedPtr<Common::Array<uint8> > programDataPtr;
+	Common::Array<MiniscriptInstruction *> miniscriptInstructions;
+
+	// If the program is empty then just return an empty program
+	if (program.bytecode.size() == 0 || program.numOfInstructions == 0)
+		return Common::SharedPtr<MiniscriptProgram>(new MiniscriptProgram(programDataPtr, miniscriptInstructions, localRefs, attributes));
+
+	localRefs.resize(program.localRefs.size());
+	for (size_t i = 0; i < program.localRefs.size(); i++) {
+		localRefs[i].guid = program.localRefs[i].guid;
+		localRefs[i].name = program.localRefs[i].name;
+	}
+
+	attributes.resize(program.attributes.size());
+	for (size_t i = 0; i < program.attributes.size(); i++) {
+		attributes[i].name = program.attributes[i].name;
+	}
+
+	Common::MemoryReadStreamEndian stream(&program.bytecode[0], program.bytecode.size(), program.isBigEndian);
+	Data::DataReader reader(stream, program.projectFormat);
+
+	struct InstructionData {
+		uint16 opcode;
+		uint16 flags;
+		size_t pdPosition;
+		IMiniscriptInstructionFactory *instrFactory;
+		Common::Array<uint8> contents;
+	};
+
+	Common::Array<InstructionData> rawInstructions;
+	rawInstructions.resize(program.numOfInstructions);
+
+	for (size_t i = 0; i < program.numOfInstructions; i++) {
+		InstructionData &rawInstruction = rawInstructions[i];
+		uint16 instrSize;
+		if (!reader.readU16(rawInstruction.opcode) || !reader.readU16(rawInstruction.flags) || !reader.readU16(instrSize))
+			return nullptr;
+
+		if (instrSize < 6)
+			return nullptr;
+
+		if (instrSize > 6) {
+			rawInstruction.contents.resize(instrSize - 6);
+			if (!reader.read(&rawInstruction.contents[0], instrSize - 6))
+				return nullptr;
+		}
+	}
+
+	programDataPtr.reset(new Common::Array<uint8>());
+	Common::Array<uint8> &programData = *programDataPtr.get();
+
+	// Find out how much space we need and place instructions
+	size_t maxAlignment = 1;
+	size_t programDataSize = 0;
+	for (size_t i = 0; i < program.numOfInstructions; i++) {
+		InstructionData &rawInstruction = rawInstructions[i];
+
+		IMiniscriptInstructionFactory *factory = resolveOpcode(rawInstruction.opcode);
+		rawInstruction.instrFactory = factory;
+
+		if (!factory)
+			return nullptr;
+
+		size_t compiledSize = 0;
+		size_t compiledAlignment = 0;
+		factory->getSizeAndAlignment(compiledSize, compiledAlignment);
+
+		if (programDataSize % compiledAlignment != 0)
+			programDataSize += (compiledAlignment - (programDataSize % compiledAlignment));
+
+		rawInstruction.pdPosition = programDataSize;
+		programDataSize += compiledSize;
+
+		if (maxAlignment < compiledAlignment)
+			maxAlignment = compiledAlignment;
+	}
+
+	programData.resize(programDataSize + maxAlignment - 1);
+	uintptr_t programDataAddress = reinterpret_cast<uintptr_t>(&programData[0]);
+
+	size_t baseOffset = 0;
+	if (programDataAddress % maxAlignment != 0)
+		baseOffset = (maxAlignment - (programDataSize % maxAlignment));
+
+	miniscriptInstructions.resize(program.numOfInstructions);
+
+	// Create instructions
+	for (size_t i = 0; i < program.numOfInstructions; i++) {
+		const InstructionData &rawInstruction = rawInstructions[i];
+
+		const void *dataLoc = nullptr;
+		if (rawInstruction.contents.size() != 0)
+			dataLoc = &rawInstruction.contents[0];
+
+		Common::MemoryReadStreamEndian instrContentsStream(static_cast<const byte *>(dataLoc), rawInstruction.contents.size(), reader.isBigEndian());
+		Data::DataReader instrContentsReader(instrContentsStream, reader.getProjectFormat());
+
+		if (!rawInstruction.instrFactory->create(&programData[baseOffset + rawInstruction.pdPosition], rawInstruction.flags, instrContentsReader, miniscriptInstructions[i])) {
+			// Destroy any already-created instructions
+			for (size_t di = 0; di < i; di++) {
+				miniscriptInstructions[i - 1 - di]->~MiniscriptInstruction();
+			}
+
+			return nullptr;
+		}
+	}
+
+	// Done
+	return Common::SharedPtr<MiniscriptProgram>(new MiniscriptProgram(programDataPtr, miniscriptInstructions, localRefs, attributes));
+}
+
+IMiniscriptInstructionFactory* MiniscriptParser::resolveOpcode(uint16 opcode) {
+	switch (opcode) {
+	case 0x834:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::Set>::getInstance();
+	case 0x898:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::Send>::getInstance();
+	case 0xc9:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::Add>::getInstance();
+	case 0xca:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::Sub>::getInstance();
+	case 0xcb:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::Mul>::getInstance();
+	case 0xcc:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::Div>::getInstance();
+	case 0xcd:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::Pow>::getInstance();
+	case 0xce:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::And>::getInstance();
+	case 0xcf:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::Or>::getInstance();
+	case 0xd0:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::Neg>::getInstance();
+	case 0xd1:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::Not>::getInstance();
+	case 0xd2:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::CmpEqual>::getInstance();
+	case 0xd3:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::CmpNotEqual>::getInstance();
+	case 0xd4:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::CmpLessOrEqual>::getInstance();
+	case 0xd5:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::CmpLess>::getInstance();
+	case 0xd6:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::CmpGreaterOrEqual>::getInstance();
+	case 0xd7:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::CmpGreater>::getInstance();
+	case 0xd8:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::BuiltinFunc>::getInstance();
+	case 0xd9:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::DivInt>::getInstance();
+	case 0xda:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::Modulo>::getInstance();
+	case 0xdb:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::StrConcat>::getInstance();
+	case 0x12f:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::PointCreate>::getInstance();
+	case 0x130:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::RangeCreate>::getInstance();
+	case 0x131:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::VectorCreate>::getInstance();
+	case 0x135:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::GetChild>::getInstance();
+	case 0x136:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::ListAppend>::getInstance();
+	case 0x137:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::ListCreate>::getInstance();
+	case 0x191:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::PushValue>::getInstance();
+	case 0x192:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::PushGlobal>::getInstance();
+	case 0x193:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::PushString>::getInstance();
+	case 0x7d3:
+		return MiniscriptInstructionFactory<MiniscriptInstructions::Jump>::getInstance();
+	default:
+		return nullptr;
+	}
+}
+
+namespace MiniscriptInstructions {
+
+Send::Send(const Event &evt) : _evt(evt) {
+}
+
+BuiltinFunc::BuiltinFunc(BuiltinFunctionID bfid) : _funcID(bfid) {
+}
+
+GetChild::GetChild(uint32 attribute) : _attribute(attribute) {
+}
+
+PushValue::PushValue(DataType dataType, const void *value) {
+	switch (dataType) {
+	case DataType::kDataTypeBool:
+		_value.b = *static_cast<const bool *>(value);
+		break;
+	case DataType::kDataTypeDouble:
+		_value.d = *static_cast<const double *>(value);
+		break;
+	case DataType::kDataTypeLocalRef:
+	case DataType::kDataTypeGlobalRef:
+		_value.ref = *static_cast<const uint32 *>(value);
+		break;
+	default:
+		break;
+	}
+}
+
+PushGlobal::PushGlobal(uint32 globalID) : _globalID(globalID) {
+}
+
+PushString::PushString(const Common::String &str) : _str(str) {
+}
+
+Jump::Jump(uint32 instrOffset, bool isConditional) : _instrOffset(instrOffset), _isConditional(isConditional) {
+}
+
+} // End of namespace MiniscriptInstructions
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/miniscript.h b/engines/mtropolis/miniscript.h
new file mode 100644
index 00000000000..b6fb50aca1c
--- /dev/null
+++ b/engines/mtropolis/miniscript.h
@@ -0,0 +1,241 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_MINISCRIPT_H
+#define MTROPOLIS_MINISCRIPT_H
+
+#include "mtropolis/data.h"
+#include "mtropolis/runtime.h"
+
+namespace MTropolis {
+
+struct MiniscriptVM;
+struct IMiniscriptInstructionFactory;
+
+class MiniscriptInstruction {
+public:
+	virtual ~MiniscriptInstruction();
+};
+
+class MiniscriptProgram {
+public:
+	struct LocalRef {
+		uint32 guid;
+		Common::String name;
+	};
+
+	struct Attribute {
+		Common::String name;
+	};
+
+	MiniscriptProgram(const Common::SharedPtr<Common::Array<uint8> > &programData, const Common::Array<MiniscriptInstruction *> &instructions,
+		const Common::Array<LocalRef> &localRefs, const Common::Array<Attribute> &attributes);
+	~MiniscriptProgram();
+
+private:
+	Common::SharedPtr<Common::Array<uint8> > _programData;
+	Common::Array<MiniscriptInstruction *> _instructions;
+	Common::Array<LocalRef> _localRefs;
+	Common::Array<Attribute> _attributes;
+};
+
+class MiniscriptParser {
+public:
+	static Common::SharedPtr<MiniscriptProgram> parse(const Data::MiniscriptProgram &programData);
+
+	static IMiniscriptInstructionFactory *resolveOpcode(uint16 opcode);
+};
+
+namespace MiniscriptInstructions {
+	class UnimplementedInstruction : public MiniscriptInstruction {
+	};
+
+	class Set : public UnimplementedInstruction {
+	};
+
+	class Send : public UnimplementedInstruction {
+	public:
+		explicit Send(const Event &evt);
+
+	private:
+		Event _evt;
+	};
+
+	class Add : public UnimplementedInstruction {
+	};
+
+	class Sub : public UnimplementedInstruction {
+	};
+
+	class Mul : public UnimplementedInstruction {
+	};
+
+	class Div : public UnimplementedInstruction {
+	};
+
+	class Pow : public UnimplementedInstruction {
+	};
+
+	class And : public UnimplementedInstruction {
+	};
+
+	class Or : public UnimplementedInstruction {
+	};
+
+	class Neg : public UnimplementedInstruction {
+	};
+
+	class Not : public UnimplementedInstruction {
+	};
+
+	class CmpEqual : public UnimplementedInstruction {
+	};
+
+	class CmpNotEqual : public UnimplementedInstruction {
+	};
+
+	class CmpLessOrEqual : public UnimplementedInstruction {
+	};
+
+	class CmpLess : public UnimplementedInstruction {
+	};
+
+	class CmpGreaterOrEqual : public UnimplementedInstruction {
+	};
+
+	class CmpGreater : public UnimplementedInstruction {
+	};
+
+	class BuiltinFunc : public UnimplementedInstruction {
+	public:
+		enum BuiltinFunctionID {
+			kSin = 1,
+			kCos = 2,
+			kRandom = 3,
+			kSqrt = 4,
+			kTan = 5,
+			kAbs = 6,
+			kSign = 7,
+			kArctangent = 8,
+			kExp = 9,
+			kLn = 10,
+			kLog = 11,
+			kCosH = 12,
+			kSinH = 13,
+			kTanH = 14,
+			kRect2Polar = 15,
+			kPolar2Rect = 16,
+			kTrunc = 17,
+			kRound = 18,
+			kNum2Str = 19,
+			kStr2Num = 20,
+		};
+
+		explicit BuiltinFunc(BuiltinFunctionID bfid);
+
+	private:
+		BuiltinFunctionID _funcID;
+	};
+
+	class DivInt : public UnimplementedInstruction {
+	};
+
+	class Modulo : public UnimplementedInstruction {
+	};
+
+	class StrConcat : public UnimplementedInstruction {
+	};
+
+	class PointCreate : public UnimplementedInstruction {
+	};
+
+	class RangeCreate : public UnimplementedInstruction {
+	};
+
+	class VectorCreate : public UnimplementedInstruction {
+	};
+
+	class GetChild : public UnimplementedInstruction {
+	public:
+		explicit GetChild(uint32 attribute);
+
+	private:
+		uint32 _attribute;
+	};
+
+	class ListAppend : public UnimplementedInstruction {
+	};
+
+	class ListCreate : public UnimplementedInstruction {
+	};
+
+	class PushValue : public UnimplementedInstruction {
+	public:
+		enum DataType {
+			kDataTypeNull,
+			kDataTypeDouble,
+			kDataTypeBool,
+			kDataTypeLocalRef,
+			kDataTypeGlobalRef,
+		};
+
+		PushValue(DataType dataType, const void *value);
+
+	private:
+		union ValueUnion {
+			bool b;
+			double d;
+			uint32 ref;
+		};
+
+		DataType _dataType;
+		ValueUnion _value;
+	};
+
+	class PushGlobal : public UnimplementedInstruction {
+	public:
+		explicit PushGlobal(uint32 globalID);
+
+	private:
+		uint32 _globalID;
+	};
+
+	class PushString : public UnimplementedInstruction {
+	public:
+		explicit PushString(const Common::String &str);
+
+	private:
+		Common::String _str;
+	};
+
+	class Jump : public UnimplementedInstruction {
+	public:
+		Jump(uint32 instrOffset, bool isConditional);
+
+	private:
+		uint32 _instrOffset;
+		bool _isConditional;
+	};
+} // End of namespace MiniscriptInstructions
+
+} // End of namespace MTropolis
+
+#endif
diff --git a/engines/mtropolis/modifier_factory.cpp b/engines/mtropolis/modifier_factory.cpp
index f38f838e5f7..9c833dce9f7 100644
--- a/engines/mtropolis/modifier_factory.cpp
+++ b/engines/mtropolis/modifier_factory.cpp
@@ -57,6 +57,12 @@ IModifierFactory *getModifierFactoryForDataObjectType(const Data::DataObjectType
 	switch (dataObjectType) {
 	case Data::DataObjectTypes::kBehaviorModifier:
 		return ModifierFactory<BehaviorModifier, Data::BehaviorModifier>::getInstance();
+	case Data::DataObjectTypes::kMiniscriptModifier:
+		return ModifierFactory<MiniscriptModifier, Data::MiniscriptModifier>::getInstance();
+	case Data::DataObjectTypes::kIfMessengerModifier:
+		return ModifierFactory<IfMessengerModifier, Data::IfMessengerModifier>::getInstance();
+	case Data::DataObjectTypes::kMessengerModifier:
+		return ModifierFactory<MessengerModifier, Data::MessengerModifier>::getInstance();
 	default:
 		return nullptr;
 	}
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 6703edcd611..e2f944bb924 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -19,11 +19,22 @@
  *
  */
 
+#include "mtropolis/miniscript.h"
 #include "mtropolis/modifiers.h"
 #include "mtropolis/modifier_factory.h"
 
+#include "common/memstream.h"
+
 namespace MTropolis {
 
+static MessageFlags translateMessengerFlags(uint32 messengerFlags) {
+	MessageFlags messageFlags;
+	messageFlags.relay = ((messengerFlags & 0x20000000) == 0);
+	messageFlags.cascade = ((messengerFlags & 0x40000000) == 0);
+	messageFlags.immediate = ((messengerFlags & 0x80000000) == 0);
+	return messageFlags;
+}
+
 bool BehaviorModifier::load(ModifierLoaderContext &context, const Data::BehaviorModifier &data) {
 	if (data.numChildren > 0) {
 		ChildLoaderContext loaderContext;
@@ -37,11 +48,64 @@ bool BehaviorModifier::load(ModifierLoaderContext &context, const Data::Behavior
 	if (!_enableWhen.load(data.enableWhen) || !_disableWhen.load(data.disableWhen))
 		return false;
 
+	_guid = data.guid;
+	_name = data.name;
+
 	return true;
 }
 
-Common::Array<Common::SharedPtr<Modifier> > &BehaviorModifier::getModifiers() {
+const Common::Array<Common::SharedPtr<Modifier> > &BehaviorModifier::getModifiers() const {
 	return _children;
 }
 
+void BehaviorModifier::appendModifier(const Common::SharedPtr<Modifier> &modifier) {
+	_children.push_back(modifier);
+}
+
+
+// Miniscript modifier
+bool MiniscriptModifier::load(ModifierLoaderContext &context, const Data::MiniscriptModifier &data) {
+	if (!_enableWhen.load(data.enableWhen))
+		return false;
+
+	_program = MiniscriptParser::parse(data.program);
+	if (!_program)
+		return false;
+
+	return true;
+}
+
+bool MessengerModifier::load(ModifierLoaderContext &context, const Data::MessengerModifier &data) {
+	_guid = data.guid;
+	_name = data.name;
+
+	if (!_when.load(data.when) || !_send.load(data.send))
+		return false;
+
+	_messageFlags = translateMessengerFlags(data.messageFlags);
+	_messageWithType = static_cast<MessageWithType>(data.with);
+	_messageDestination = data.destination;
+
+	return true;
+}
+
+bool IfMessengerModifier::load(ModifierLoaderContext &context, const Data::IfMessengerModifier &data) {
+	_guid = data.guid;
+	_name = data.name;
+
+	if (!_when.load(data.when) || !_send.load(data.send))
+		return false;
+
+	_messageFlags = translateMessengerFlags(data.messageFlags);
+	_messageWithType = static_cast<MessageWithType>(data.with);
+	_messageDestination = data.destination;
+
+	_program = MiniscriptParser::parse(data.program);
+	if (!_program)
+		return false;
+
+	return true;
+}
+
+
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index aea41a74aa5..24967d3b6f8 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -28,12 +28,14 @@
 namespace MTropolis {
 
 struct ModifierLoaderContext;
+class MiniscriptProgram;
 
 class BehaviorModifier : public Modifier, public IModifierContainer {
 public:
 	bool load(ModifierLoaderContext &context, const Data::BehaviorModifier &data);
 
-	Common::Array<Common::SharedPtr<Modifier> > &getModifiers() override;
+	const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const override;
+	void appendModifier(const Common::SharedPtr<Modifier> &modifier) override;
 
 private:
 	Common::Array<Common::SharedPtr<Modifier> > _children;
@@ -41,6 +43,43 @@ private:
 	Event _disableWhen;
 };
 
+class MiniscriptModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::MiniscriptModifier &data);
+
+private:
+	Event _enableWhen;
+
+	Common::SharedPtr<MiniscriptProgram> _program;
+};
+
+class MessengerModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::MessengerModifier &data);
+
+private:
+	Event _when;
+	Event _send;
+
+	MessageFlags _messageFlags;
+	MessageWithType _messageWithType;
+	uint32 _messageDestination;	// May be a MessageDestination or GUID
+};
+
+class IfMessengerModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::IfMessengerModifier &data);
+
+private:
+	Event _when;
+	Event _send;
+
+	MessageFlags _messageFlags;
+	MessageWithType _messageWithType;
+	uint32 _messageDestination; // May be a MessageDestination or GUID
+
+	Common::SharedPtr<MiniscriptProgram> _program;
+};
 
 }	// End of namespace MTropolis
 
diff --git a/engines/mtropolis/module.mk b/engines/mtropolis/module.mk
index 3371453781a..f1c638093fe 100644
--- a/engines/mtropolis/module.mk
+++ b/engines/mtropolis/module.mk
@@ -5,6 +5,7 @@ MODULE_OBJS = \
 	data.o \
 	detection.o \
 	metaengine.o \
+	miniscript.o \
 	modifiers.o \
 	modifier_factory.o \
 	mtropolis.o \
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 0be8fa9b199..ccf92e897c3 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -30,6 +30,9 @@
 
 namespace MTropolis {
 
+MessageFlags::MessageFlags() : relay(true), cascade(true), immediate(true) {
+}
+
 Event::Event() : eventType(0), eventInfo(0) {
 }
 
@@ -58,10 +61,14 @@ const Common::Array<SegmentDescription> &ProjectDescription::getSegments() const
 	return _segments;
 }
 
-Common::Array<Common::SharedPtr<Modifier> >& SimpleModifierContainer::getModifiers() {
+const Common::Array<Common::SharedPtr<Modifier> >& SimpleModifierContainer::getModifiers() const {
 	return _modifiers;
 }
 
+void SimpleModifierContainer::appendModifier(const Common::SharedPtr<Modifier> &modifier) {
+	_modifiers.push_back(modifier);
+}
+
 Structural::~Structural() {
 }
 
@@ -72,10 +79,14 @@ const Common::Array<Common::SharedPtr<Structural> > &Structural::getChildren() c
 	return _children;
 }
 
-Common::Array<Common::SharedPtr<Modifier> > &Structural::getModifiers() {
+const Common::Array<Common::SharedPtr<Modifier> > &Structural::getModifiers() const {
 	return _modifiers;
 }
 
+void Structural::appendModifier(const Common::SharedPtr<Modifier> &modifier) {
+	_modifiers.push_back(modifier);
+}
+
 Runtime::Runtime() {
 	_vthread.reset(new VThread());
 }
@@ -336,6 +347,10 @@ void Project::loadGlobalObjectInfo(ChildLoaderStack& loaderStack, const Data::Gl
 
 
 static Common::SharedPtr<Modifier> loadModifierObject(ModifierLoaderContext &loaderContext, const Data::DataObject &dataObject) {
+	// Special case for debris
+	if (dataObject.getType() == Data::DataObjectTypes::kDebris)
+		return nullptr;
+
 	IModifierFactory *factory = getModifierFactoryForDataObjectType(dataObject.getType());
 
 	if (!factory)
@@ -363,12 +378,15 @@ void loadRuntimeContextualObject(ChildLoaderStack &stack, const Data::DataObject
 			ModifierLoaderContext loaderContext;
 			loaderContext.childLoaderStack = &stack;
 
-			container->getModifiers().push_back(loadModifierObject(loaderContext, dataObject));
+			container->appendModifier(loadModifierObject(loaderContext, dataObject));
 		}
 		break;
 	}
 }
 
+Modifier::Modifier() : _guid(0) {
+}
+
 Modifier::~Modifier() {
 }
 
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 54c540a96e7..3ce62399d02 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -37,6 +37,42 @@ namespace MTropolis {
 class Project;
 class Modifier;
 
+
+
+struct MessageFlags {
+	bool relay : 1;
+	bool cascade : 1;
+	bool immediate : 1;
+
+	MessageFlags();
+};
+
+enum MessageWithType {
+	kMessageWithNothing = 0,
+	kMessageWithIncomingData = 0x1b,
+	kMessageWithVariable = 0x1c,
+};
+
+enum MessageDestination {
+	kMessageDestSharedScene = 0x65,
+	kMessageDestScene = 0x66,
+	kMessageDestSection = 0x67,
+	kMessageDestProject = 0x68,
+	kMessageDestActiveScene = 0x69,
+	kMessageDestElementsParent = 0x6a,
+	kMessageDestChildren = 0x6b,
+	kMessageDestModifiersParent = 0x6c,
+	kMessageDestSubsection = 0x6d,
+
+	kMessageDestElement = 0xc9,
+	kMessageDestSourcesParent = 0xcf,
+
+	kMessageDestBehavior = 0xd4,
+	kMessageDestNextElement = 0xd1,
+	kMessageDestPrevElement = 0xd2,
+	kMessageDestBehaviorsParent = 0xd3,
+};
+
 struct SegmentDescription {
 	int volumeID;
 	Common::String filePath;
@@ -87,13 +123,14 @@ private:
 };
 
 struct IModifierContainer {
-	virtual Common::Array<Common::SharedPtr<Modifier> > &getModifiers() = 0;
-	const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const;
+	virtual const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const = 0;
+	virtual void appendModifier(const Common::SharedPtr<Modifier> &modifier) = 0;
 };
 
 class SimpleModifierContainer : public IModifierContainer {
 
-	Common::Array<Common::SharedPtr<Modifier> > &getModifiers() override;
+	const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const;
+	void appendModifier(const Common::SharedPtr<Modifier> &modifier) override;
 
 private:
 	Common::Array<Common::SharedPtr<Modifier> > _modifiers;
@@ -106,7 +143,8 @@ public:
 
 	const Common::Array<Common::SharedPtr<Structural> > &getChildren() const;
 
-	Common::Array<Common::SharedPtr<Modifier> > &getModifiers() override;
+	const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const override;
+	void appendModifier(const Common::SharedPtr<Modifier> &modifier) override;
 
 private:
 	Common::Array<Common::SharedPtr<Structural> > _children;
@@ -225,7 +263,12 @@ class Scene : public Structural {
 
 class Modifier {
 public:
+	Modifier();
 	virtual ~Modifier();
+
+protected:
+	uint32 _guid;
+	Common::String _name;
 };
 
 void loadRuntimeContextualObject(ChildLoaderStack &stack, const Data::DataObject &dataObject);
diff --git a/engines/mtropolis/vthread.cpp b/engines/mtropolis/vthread.cpp
index a6f6606927c..45a0761e6c0 100644
--- a/engines/mtropolis/vthread.cpp
+++ b/engines/mtropolis/vthread.cpp
@@ -56,7 +56,7 @@ VThreadState VThread::step() {
 }
 
 void VThread::reserveFrame(size_t size, size_t alignment, void *&outFramePtr, void *&outUnadjustedDataPtr, size_t &outPrevFrameOffset) {
-	const size_t frameAlignment = offsetof(VThreadAlignmentHelper<VThreadStackFrame>, item);
+	const size_t frameAlignment = AlignmentHelper<VThreadStackFrame>::getAlignment();
 	const size_t frameAlignmentMask = frameAlignment - 1;
 
 	size_t dataAlignmentMask = alignment - 1;
diff --git a/engines/mtropolis/vthread.h b/engines/mtropolis/vthread.h
index fd07ed5bb44..30973cc5be7 100644
--- a/engines/mtropolis/vthread.h
+++ b/engines/mtropolis/vthread.h
@@ -23,6 +23,7 @@
 #define MTROPOLIS_TASKSTACK_H
 
 #include <new>
+#include "mtropolis/alignment_helper.h"
 
 namespace MTropolis {
 
@@ -35,13 +36,6 @@ enum VThreadState {
 	kVThreadError,
 };
 
-template<typename TClass>
-struct VThreadAlignmentHelper {
-	char prefix;
-	TClass item;
-};
-
-
 class VThreadTaskData {
 
 public:
@@ -229,8 +223,8 @@ template<typename TClass, typename TData>
 TData *VThread::pushTask(TClass *obj, VThreadState (TClass::*method)(const TData &data)) {
 	typedef VThreadMethodData<TClass, TData> FrameData_t; 
 
-	const size_t frameAlignment = offsetof(VThreadAlignmentHelper<VThreadStackFrame>, item);
-	const size_t dataAlignment = offsetof(VThreadAlignmentHelper<FrameData_t>, item);
+	const size_t frameAlignment = AlignmentHelper<VThreadStackFrame>::getAlignment();
+	const size_t dataAlignment = AlignmentHelper<FrameData_t>::getAlignment();
 	const size_t maxAlignment = (frameAlignment < dataAlignment) ? dataAlignment : frameAlignment;
 
 	void *framePtr;
@@ -257,8 +251,8 @@ template<typename TData>
 TData *VThread::pushTask(VThreadState (*func)(const TData &data)) {
 	typedef VThreadFunctionData<TData> FrameData_t;
 
-	const size_t frameAlignment = offsetof(VThreadAlignmentHelper<VThreadStackFrame>, item);
-	const size_t dataAlignment = offsetof(VThreadAlignmentHelper<FrameData_t>, item);
+	const size_t frameAlignment = AlignmentHelper<VThreadStackFrame>::getAlignment();
+	const size_t dataAlignment = AlignmentHelper<FrameData_t>::getAlignment();
 	const size_t maxAlignment = (frameAlignment < dataAlignment) ? dataAlignment : frameAlignment;
 
 	void *framePtr;


Commit: 349bfa9694bfe4a1c449235009f6b2e0e231cd81
    https://github.com/scummvm/scummvm/commit/349bfa9694bfe4a1c449235009f6b2e0e231cd81
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Plug-in base work, add Mac Obsidian support

Changed paths:
  A engines/mtropolis/plugin_obsidian.cpp
  A engines/mtropolis/plugin_obsidian.h
  A engines/mtropolis/plugin_standard.cpp
  A engines/mtropolis/plugin_standard.h
  A engines/mtropolis/plugin_standard_data.cpp
  A engines/mtropolis/plugin_standard_data.h
  A engines/mtropolis/plugins.h
    common/stuffit.h
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/detection_tables.h
    engines/mtropolis/modifier_factory.cpp
    engines/mtropolis/modifier_factory.h
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/module.mk
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/common/stuffit.h b/common/stuffit.h
index 15ccd91944f..4350f4c69ba 100644
--- a/common/stuffit.h
+++ b/common/stuffit.h
@@ -25,6 +25,7 @@
  * - grim
  * - groovie
  * - kyra
+ * - mtropolis
  */
 
 #ifndef COMMON_STUFFIT_H
diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index 6ab1599904b..0396519bd09 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -20,6 +20,7 @@
  */
 
 #include "mtropolis/data.h"
+#include "common/debug.h"
 
 namespace MTropolis {
 
@@ -31,52 +32,52 @@ DataReader::DataReader(Common::SeekableReadStreamEndian &stream, ProjectFormat p
 
 bool DataReader::readU8(uint8 &value) {
 	value = _stream.readByte();
-	return !_stream.err();
+	return checkErrorAndReset();
 }
 
 bool DataReader::readU16(uint16 &value) {
 	value = _stream.readUint16();
-	return !_stream.err();
+	return checkErrorAndReset();
 }
 
 bool DataReader::readU32(uint32 &value) {
 	value = _stream.readUint32();
-	return !_stream.err();
+	return checkErrorAndReset();
 }
 
 bool DataReader::readU64(uint64 &value) {
 	value = _stream.readUint64();
-	return !_stream.err();
+	return checkErrorAndReset();
 }
 
 bool DataReader::readS8(int8 &value) {
 	value = _stream.readSByte();
-	return !_stream.err();
+	return checkErrorAndReset();
 }
 
 bool DataReader::readS16(int16 &value) {
 	value = _stream.readSint16();
-	return !_stream.err();
+	return checkErrorAndReset();
 }
 
 bool DataReader::readS32(int32 &value) {
 	value = _stream.readSint32();
-	return !_stream.err();
+	return checkErrorAndReset();
 }
 
 bool DataReader::readS64(int64 &value) {
 	value = _stream.readSint64();
-	return !_stream.err();
+	return checkErrorAndReset();
 }
 
 bool DataReader::readF32(float &value) {
 	value = _stream.readFloat();
-	return !_stream.err();
+	return checkErrorAndReset();
 }
 
 bool DataReader::readF64(double &value) {
 	value = _stream.readDouble();
-	return !_stream.err();
+	return checkErrorAndReset();
 }
 
 bool DataReader::read(void *dest, size_t size) {
@@ -86,8 +87,8 @@ bool DataReader::read(void *dest, size_t size) {
 			thisChunkSize = static_cast<uint32>(size);
 		}
 
-		_stream.read(dest, thisChunkSize);
-		if (_stream.err()) {
+		if (_stream.read(dest, thisChunkSize) != thisChunkSize) {
+			checkErrorAndReset();
 			return false;
 		}
 
@@ -139,6 +140,7 @@ bool DataReader::skip(size_t count) {
 		}
 
 		if (!_stream.seek(static_cast<int64>(count), SEEK_CUR)) {
+			checkErrorAndReset();
 			return false;
 		}
 
@@ -155,6 +157,17 @@ bool DataReader::isBigEndian() const {
 	return _stream.isBE();
 }
 
+bool DataReader::checkErrorAndReset() {
+	const bool isFault = _stream.err() || _stream.eos();
+	if (isFault) {
+		_stream.clearErr();
+		return false;
+	}
+
+	return true;
+}
+
+
 bool Rect::load(DataReader &reader) {
 	if (reader.getProjectFormat() == kProjectFormatMacintosh)
 		return reader.readS16(top) && reader.readS16(left) && reader.readS16(bottom) && reader.readS16(right);
@@ -297,13 +310,17 @@ DataReadErrorCode ProjectCatalog::load(DataReader &reader) {
 			return kDataReadErrorReadFailed;
 		}
 
-		if (_revision >= 3 && !reader.skip(8)) {
+		if (_revision >= 3 && reader.getProjectFormat() == Data::kProjectFormatWindows && !reader.skip(8)) {
 			return kDataReadErrorReadFailed;
 		}
 
 		if (!reader.readU32(streamDesc.pos) || !reader.readU32(streamDesc.size)) {
 			return kDataReadErrorReadFailed;
 		}
+
+		if (_revision >= 3 && reader.getProjectFormat() == Data::kProjectFormatMacintosh && !reader.skip(8)) {
+			return kDataReadErrorReadFailed;
+		}
 	}
 
 	uint32 unknownSegmentPrefix = 0;
@@ -406,17 +423,21 @@ bool MiniscriptProgram::load(DataReader &reader) {
 	return true;
 }
 
-DataReadErrorCode MiniscriptModifier::load(DataReader &reader) {
-	if (_revision != 0x3eb)
-		return kDataReadErrorUnsupportedRevision;
-
+bool TypicalModifierHeader::load(DataReader& reader) {
 	if (!reader.readU32(unknown1) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid) || !reader.readBytes(unknown3) || !reader.readU32(unknown4) || !reader.readBytes(unknown5) || !reader.readU16(lengthOfName))
-		return kDataReadErrorReadFailed;
+		return false;
 
 	if (lengthOfName > 0 && !reader.readTerminatedStr(name, lengthOfName))
-		return kDataReadErrorReadFailed;
+		return false;
+
+	return true;
+}
+
+DataReadErrorCode MiniscriptModifier::load(DataReader &reader) {
+	if (_revision != 0x3eb)
+		return kDataReadErrorUnsupportedRevision;
 
-	if (!enableWhen.load(reader) || !reader.readBytes(unknown6) || !reader.readU8(unknown7) || !program.load(reader))
+	if (!modHeader.load(reader) || !enableWhen.load(reader) || !reader.readBytes(unknown6) || !reader.readU8(unknown7) || !program.load(reader))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
@@ -426,11 +447,7 @@ DataReadErrorCode MessengerModifier::load(DataReader& reader) {
 	if (_revision != 0x3ea)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!reader.readU32(unknown1) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid)
-		|| !reader.readBytes(unknown3) || !reader.readU32(unknown4) || !reader.readBytes(unknown5) || !reader.readU16(lengthOfName))
-		return kDataReadErrorReadFailed;
-
-	if (lengthOfName > 0 && !reader.readTerminatedStr(name, lengthOfName))
+	if (!modHeader.load(reader))
 		return kDataReadErrorReadFailed;
 
 	// Unlike most cases, the "when" event is split in half in this case
@@ -449,14 +466,7 @@ DataReadErrorCode IfMessengerModifier::load(DataReader &reader) {
 	if (_revision != 0x3ea)
 		return kDataReadErrorReadFailed;
 
-	if (!reader.readU32(unknown1) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid)
-		|| !reader.readBytes(unknown3) || !reader.readU32(unknown4) || !reader.readBytes(unknown5) || !reader.readU16(lengthOfName))
-		return kDataReadErrorReadFailed;
-
-	if (lengthOfName > 0 && !reader.readTerminatedStr(name, lengthOfName))
-		return kDataReadErrorReadFailed;
-
-	if (!reader.readU32(messageFlags) || !when.load(reader) || !send.load(reader) ||
+	if (!modHeader.load(reader) || !reader.readU32(messageFlags) || !when.load(reader) || !send.load(reader) ||
 		!reader.readU16(unknown6) || !reader.readU32(destination) || !reader.readBytes(unknown7) || !reader.readU16(with)
 		|| !reader.readBytes(unknown8) || !reader.readU32(withSourceGUID) || !reader.readBytes(unknown9) || !reader.readU8(withSourceLength) || !reader.readU8(unknown10))
 		return kDataReadErrorReadFailed;
@@ -470,6 +480,63 @@ DataReadErrorCode IfMessengerModifier::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode BooleanVariableModifier::load(DataReader &reader) {
+	if (_revision != 0x3e8)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!modHeader.load(reader) || !reader.readU8(value) || !reader.readU8(unknown5))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode PointVariableModifier::load(DataReader &reader) {
+	if (_revision != 0x3e8)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!modHeader.load(reader) || !reader.readBytes(unknown5) || !value.load(reader))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
+PlugInModifierData::~PlugInModifierData() {
+}
+
+DataReadErrorCode PlugInModifier::load(DataReader &reader) {
+	if (_revision != 0x03e9)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(unknown1) || !reader.readU32(codedSize) || !reader.read(modifierName, 16)
+		|| !reader.readU32(guid) || !reader.readBytes(unknown2) || !reader.readU16(plugInRevision)
+		|| !reader.readU32(unknown4) || !reader.readBytes(unknown5) || !reader.readU16(lengthOfName))
+		return kDataReadErrorReadFailed;
+
+	if (lengthOfName > 0 && !reader.readTerminatedStr(name, lengthOfName))
+		return kDataReadErrorReadFailed;
+
+	// Terminate modifier name safely
+	modifierName[16] = 0;
+
+	subObjectSize = codedSize;
+	if (reader.getProjectFormat() == kProjectFormatWindows) {
+		// This makes no sense but it's how it's stored...
+		if (subObjectSize < lengthOfName * 256u)
+			return kDataReadErrorReadFailed;
+		subObjectSize -= lengthOfName * 256u;
+	} else {
+		if (subObjectSize < lengthOfName)
+			return kDataReadErrorReadFailed;
+		subObjectSize -= lengthOfName;
+	}
+
+	if (subObjectSize < 52)
+		return kDataReadErrorReadFailed;
+	subObjectSize -= 52;
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode Debris::load(DataReader &reader) {
 	if (_revision != 0)
 		return kDataReadErrorUnsupportedRevision;
@@ -480,7 +547,18 @@ DataReadErrorCode Debris::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
-DataReadErrorCode loadDataObject(DataReader& reader, Common::SharedPtr<DataObject>& outObject) {
+const IPlugInModifierDataFactory *PlugInModifierRegistry::findLoader(const char *modifierName) const {
+	Common::HashMap<Common::String, const IPlugInModifierDataFactory *>::const_iterator it = _loaders.find(modifierName);
+	if (it == _loaders.end())
+		return nullptr;
+	return it->_value;
+}
+
+void PlugInModifierRegistry::registerLoader(const char *modifierName, const IPlugInModifierDataFactory *loader) {
+	_loaders[modifierName] = loader;
+}
+
+DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataReader &reader, Common::SharedPtr<DataObject> &outObject) {
 	uint32 type;
 	uint16 revision;
 	if (!reader.readU32(type) || !reader.readU16(revision)) {
@@ -522,10 +600,20 @@ DataReadErrorCode loadDataObject(DataReader& reader, Common::SharedPtr<DataObjec
 	case DataObjectTypes::kIfMessengerModifier:
 		dataObject = new IfMessengerModifier();
 		break;
+	case DataObjectTypes::kBooleanVariableModifier:
+		dataObject = new BooleanVariableModifier();
+		break;
+	case DataObjectTypes::kPointVariableModifier:
+		dataObject = new PointVariableModifier();
+		break;
 	case DataObjectTypes::kDebris:
 		dataObject = new Debris();
 		break;
+	case DataObjectTypes::kPlugInModifier:
+		dataObject = new PlugInModifier();
+		break;
 	default:
+		debug(1, "Unrecognized data object type %x", static_cast<int>(type));
 		break;
 	}
 
@@ -540,6 +628,24 @@ DataReadErrorCode loadDataObject(DataReader& reader, Common::SharedPtr<DataObjec
 		return errorCode;
 	}
 
+	if (type == DataObjectTypes::kPlugInModifier) {
+		const IPlugInModifierDataFactory *plugInLoader = registry.findLoader(static_cast<const PlugInModifier *>(dataObject)->modifierName);
+		if (!plugInLoader) {
+			debug(1, "Unrecognized plug-in modifier type %s", static_cast<const PlugInModifier *>(dataObject)->modifierName);
+			outObject.reset();
+			return kDataReadErrorPlugInNotFound;
+		}
+
+		Common::SharedPtr<PlugInModifierData> plugInModifierData(plugInLoader->createModifierData());
+		errorCode = plugInModifierData->load(*static_cast<const PlugInModifier *>(dataObject), reader);
+		if (errorCode != kDataReadErrorNone) {
+			outObject.reset();
+			return errorCode;
+		}
+
+		static_cast<PlugInModifier *>(dataObject)->plugInData = plugInModifierData;
+	}
+
 	outObject = sharedPtr;
 	return errorCode;
 }
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index c598ae2d57f..7bf1cec0d42 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -24,6 +24,8 @@
 
 #include "common/array.h"
 #include "common/error.h"
+#include "common/hashmap.h"
+#include "common/hash-str.h"
 #include "common/ptr.h"
 #include "common/stream.h"
 
@@ -31,6 +33,9 @@ namespace MTropolis {
 
 namespace Data {
 
+struct PlugInModifier;
+struct PlugInModifierData;
+
 enum ProjectFormat {
 	kProjectFormatUnknown,
 
@@ -45,28 +50,81 @@ enum DataReadErrorCode {
 	kDataReadErrorUnsupportedRevision,
 	kDataReadErrorReadFailed,
 	kDataReadErrorUnrecognized,
+	kDataReadErrorPlugInNotFound,
 };
 
 namespace DataObjectTypes {
 
 enum DataObjectType {
-	kUnknown = 0,
-
-	kProjectCatalog       = 0x3e8,
-	kStreamHeader         = 0x3e9,
-	kProjectHeader        = 0x3ea,
-	kPresentationSettings = 0x3ec,
-
-	kAssetCatalog         = 0xd,
-    kGlobalObjectInfo     = 0x17,
-	kUnknown19            = 0x19,
-	
-	kIfMessengerModifier  = 0x2bc,
-	kBehaviorModifier     = 0x2c6,
-	kMessengerModifier    = 0x2da,
-	kMiniscriptModifier   = 0x3c0,
-
-	kDebris               = 0xfffffffe,	// Deleted object
+	kUnknown                             = 0,
+
+	kProjectCatalog                      = 0x3e8,
+	kStreamHeader                        = 0x3e9,
+	kProjectHeader                       = 0x3ea,
+	kPresentationSettings                = 0x3ec,
+
+	kAssetCatalog                        = 0xd,
+    kGlobalObjectInfo                    = 0x17,
+	kUnknown19                           = 0x19,
+	kProjectLabelMap                     = 0x22,	// NYI
+
+	kProjectStructuralDef                = 0x2,		// NYI
+	kSectionStructuralDef                = 0x3,		// NYI
+	kSceneStructuralDef                  = 0x8,
+	kSubsectionStructuralDef             = 0x21,	// NYI
+	kMovieStructuralDef                  = 0x5,		// NYI
+	kMToonStructuralDef                  = 0x6,		// NYI
+	kGraphicStructuralDef                = 0x7,		// NYI
+	kSoundStructuralDef                  = 0xa,		// NYI
+
+	kTextLabelElement                    = 0x15,	// NYI
+
+	kAlias                               = 0x27,	// NYI
+
+	kMovieAsset                          = 0x10,	// NYI
+	kSoundAsset                          = 0x11,	// NYI
+	kColorTableAsset                     = 0x1e,	// NYI
+	kImageAsset                          = 0xe,		// NYI
+	kMToonAsset                          = 0xf,		// NYI
+
+	kSoundEffectModifier                 = 0x1a4,	// NYI
+	kChangeSceneModifier                 = 0x136,	// NYI
+	kReturnModifier                      = 0x140,	// NYI
+	kDragMotionModifier                  = 0x208,	// NYI
+	kVectorMotionModifier                = 0x226,	// NYI
+	kPathMotionModifierV1                = 0x21c,	// NYI - Obsolete version
+	kPathMotionModifierV2                = 0x21b,	// NYI
+	kSceneTransitionModifier             = 0x26c,	// NYI
+	kElementTransitionModifier           = 0x276,	// NYI
+	kSharedSceneModifier                 = 0x29a,	// NYI
+	kIfMessengerModifier                 = 0x2bc,
+	kBehaviorModifier                    = 0x2c6,
+	kMessengerModifier                   = 0x2da,
+	kSetModifier                         = 0x2df,	// NYI
+	kCollisionDetectionMessengerMOdifier = 0x2ee,	// NYI
+	kBoundaryDetectionMessengerModifier  = 0x2f8,	// NYI
+	kKeyboardMessengerModifier           = 0x302,	// NYI
+	kTextStyleModifier                   = 0x32a,	// NYI
+	kGraphicModifier                     = 0x334,	// NYI
+	kImageEffectModifier                 = 0x384,	// NYI
+	kMiniscriptModifier                  = 0x3c0,
+	kCursorModifierV1                    = 0x3ca,	// NYI - Obsolete version
+	kGradientModifier                    = 0x4b0,	// NYI
+	kColorTableModifier                  = 0x4c4,	// NYI
+	kSaveAndRestoreModifier              = 0x4d8,	// NYI
+
+	kCompoundVariableModifier            = 0x2c7,	// NYI
+	kBooleanVariableModifier             = 0x321,
+	kIntegerVariableModifier             = 0x322,	// NYI
+	kIntegerRangeVariableModifier        = 0x324,	// NYI
+	kVectorVariableModifier              = 0x327,	// NYI
+	kFloatingPointVariableModifier       = 0x328,	// NYI
+	kPointVariableModifier               = 0x326,
+	kStringVariableModifier              = 0x329,	// NYI
+
+	kDebris                              = 0xfffffffe,	// Deleted object
+	kPlugInModifier                      = 0xffffffff,
+	kAssetDataChunk                      = 0xffff,
 };
 
 } // End of namespace DataObjectTypes
@@ -105,6 +163,8 @@ public:
 	bool isBigEndian() const;
 
 private:
+	bool checkErrorAndReset();
+
 	Common::SeekableReadStreamEndian &_stream;
 	ProjectFormat _projectFormat;
 };
@@ -315,8 +375,8 @@ struct MiniscriptProgram {
 	bool load(DataReader &reader);
 };
 
-struct MiniscriptModifier : public DataObject {
-
+// Header used for most modifiers, but not all
+struct TypicalModifierHeader {
 	uint32 unknown1;
 	uint32 sizeIncludingTag;
 	uint32 guid;
@@ -324,12 +384,19 @@ struct MiniscriptModifier : public DataObject {
 	uint32 unknown4;
 	uint8 unknown5[4];
 	uint16 lengthOfName;
+
+	Common::String name;
+
+	bool load(DataReader &reader);
+};
+
+struct MiniscriptModifier : public DataObject {
+
+	TypicalModifierHeader modHeader;
 	Event enableWhen;
 	uint8 unknown6[11];
 	uint8 unknown7;
 
-	Common::String name;
-
 	MiniscriptProgram program;
 
 protected:
@@ -343,13 +410,8 @@ enum MessageFlags {
 };
 
 struct MessengerModifier : public DataObject {
-	uint32 unknown1;
-	uint32 sizeIncludingTag;
-	uint32 guid;
-	uint8 unknown3[6];
-	uint32 unknown4;
-	uint8 unknown5[4];
-	uint16 lengthOfName;
+	TypicalModifierHeader modHeader;
+
 	uint32 messageFlags;
 	Event send;
 	Event when;
@@ -363,7 +425,6 @@ struct MessengerModifier : public DataObject {
 	uint8 withSourceLength;
 	uint8 unknown13;
 
-	Common::String name;
 	Common::String withSourceName;
 
 protected:
@@ -371,13 +432,8 @@ protected:
 };
 
 struct IfMessengerModifier : public DataObject {
-	uint32 unknown1;
-	uint32 sizeIncludingTag;
-	uint32 guid;
-	uint8 unknown3[6];
-	uint32 unknown4;
-	uint8 unknown5[4];
-	uint16 lengthOfName;
+	TypicalModifierHeader modHeader;
+
 	uint32 messageFlags;
 	Event send;
 	Event when;
@@ -392,13 +448,57 @@ struct IfMessengerModifier : public DataObject {
 	uint8 unknown10;
 	MiniscriptProgram program;
 
-	Common::String name;
 	Common::String withSource;
 
 protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct BooleanVariableModifier final : public DataObject {
+	TypicalModifierHeader modHeader;
+	uint8_t value;
+	uint8_t unknown5;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+struct PointVariableModifier final : public DataObject {
+	TypicalModifierHeader modHeader;
+
+	uint8_t unknown5[4];
+	Point value;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+struct PlugInModifierData {
+	virtual ~PlugInModifierData();
+	virtual DataReadErrorCode load(const PlugInModifier &prefix, DataReader &reader) = 0;
+};
+
+struct PlugInModifier : public DataObject {
+	uint32 unknown1;
+	uint32 codedSize;	// Total size on Mac but (size + (name length * 255)) on Windows for some reason
+	char modifierName[17];
+	uint32 guid;
+	uint8 unknown2[6];
+	uint16 plugInRevision;
+	uint32 unknown4;
+	uint8 unknown5[4];
+	uint16 lengthOfName;
+
+	Common::String name;
+
+	uint32 subObjectSize;
+
+	Common::SharedPtr<PlugInModifierData> plugInData;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 struct Debris : public DataObject {
 	uint32 persistFlags;
 	uint32 sizeIncludingTag;
@@ -407,7 +507,20 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
-DataReadErrorCode loadDataObject(DataReader &reader, Common::SharedPtr<DataObject> &outObject);
+struct IPlugInModifierDataFactory {
+	virtual Common::SharedPtr<Data::PlugInModifierData> createModifierData() const = 0;
+};
+
+class PlugInModifierRegistry {
+public:
+	const IPlugInModifierDataFactory *findLoader(const char *modifierName) const;
+	void registerLoader(const char *modifierName, const IPlugInModifierDataFactory *loader);
+
+private:
+	Common::HashMap<Common::String, const IPlugInModifierDataFactory *> _loaders;
+};
+
+DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataReader &reader, Common::SharedPtr<DataObject> &outObject);
 
 template<size_t TSize>
 inline bool DataReader::readBytes(uint8(&arr)[TSize]) {
diff --git a/engines/mtropolis/detection_tables.h b/engines/mtropolis/detection_tables.h
index 21f06f2580f..f7b0a735133 100644
--- a/engines/mtropolis/detection_tables.h
+++ b/engines/mtropolis/detection_tables.h
@@ -27,20 +27,18 @@
 namespace MTropolis {
 
 static const MTropolisGameDescription gameDescriptions[] = {
-#if 0
 	{
 		// Obsidian Macintosh
 		{
 			"obsidian",
 			"V1.0, 1/13/97, installed, CD",
 			{
-				//{ "Obsidian Data 0", 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -1 },  // Only used for launch loading screen
-				{ "Obsidian Data 1", 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -1 },
-				{ "Obsidian Data 2", 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -1 },
-				{ "Obsidian Data 3", 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -1 },
-				{ "Obsidian Data 4", 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -1 },
-				{ "Obsidian Data 5", 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -1 },
-				{ "Obsidian Data 6", 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -1 },
+				{ "Obsidian Installer", 0, "1c272c23dc50b771970cabe8410c9349", -1 },
+				{ "Obsidian Data 2", 0, "1e590e3154c1af09efb951a07abc48b8", -1 },
+				{ "Obsidian Data 3", 0, "48e514a594b7a7ad190351d6d32d5d33", -1 },
+				{ "Obsidian Data 4", 0, "8dfa726c675aae3778951ddd18e4484c", -1 },
+				{ "Obsidian Data 5", 0, "6f085578b13b3db99543b969c9009b17", -1 },
+				{ "Obsidian Data 6", 0, "120ddcb1780be0f6380d708041733406", -1 },
 				AD_LISTEND
 			},
 			Common::EN_ANY,
@@ -52,7 +50,6 @@ static const MTropolisGameDescription gameDescriptions[] = {
 		0,
 		0,
 	},
-#endif
 	{
 		// Obsidian Windows, installed
 		{
diff --git a/engines/mtropolis/modifier_factory.cpp b/engines/mtropolis/modifier_factory.cpp
index 9c833dce9f7..b79b1497c62 100644
--- a/engines/mtropolis/modifier_factory.cpp
+++ b/engines/mtropolis/modifier_factory.cpp
@@ -24,10 +24,16 @@
 
 namespace MTropolis {
 
+ModifierLoaderContext::ModifierLoaderContext(ChildLoaderStack *childLoaderStack) : childLoaderStack(childLoaderStack) {}
+
+PlugInModifierLoaderContext::PlugInModifierLoaderContext(ModifierLoaderContext &modifierLoaderContext, const Data::PlugInModifier &plugInModifierData, PlugIn *plugIn)
+	: modifierLoaderContext(modifierLoaderContext), plugInModifierData(plugInModifierData), plugIn(plugIn) {
+}
+
 template<typename TModifier, typename TModifierData>
 class ModifierFactory : public IModifierFactory {
 public:
-	Common::SharedPtr<Modifier> createModifier(ModifierLoaderContext &context, const Data::DataObject &dataObject) override;
+	Common::SharedPtr<Modifier> createModifier(ModifierLoaderContext &context, const Data::DataObject &dataObject) const override;
 	static IModifierFactory *getInstance();
 
 private:
@@ -35,7 +41,7 @@ private:
 };
 
 template<typename TModifier, typename TModifierData>
-Common::SharedPtr<Modifier> ModifierFactory<TModifier, TModifierData>::createModifier(ModifierLoaderContext &context, const Data::DataObject &dataObject) {
+Common::SharedPtr<Modifier> ModifierFactory<TModifier, TModifierData>::createModifier(ModifierLoaderContext &context, const Data::DataObject &dataObject) const {
 	Common::SharedPtr<TModifier> modifier(new TModifier());
 
 	if (!modifier->load(context, static_cast<const TModifierData &>(dataObject)))
@@ -63,6 +69,10 @@ IModifierFactory *getModifierFactoryForDataObjectType(const Data::DataObjectType
 		return ModifierFactory<IfMessengerModifier, Data::IfMessengerModifier>::getInstance();
 	case Data::DataObjectTypes::kMessengerModifier:
 		return ModifierFactory<MessengerModifier, Data::MessengerModifier>::getInstance();
+	case Data::DataObjectTypes::kBooleanVariableModifier:
+		return ModifierFactory<BooleanVariableModifier, Data::BooleanVariableModifier>::getInstance();
+	case Data::DataObjectTypes::kPointVariableModifier:
+		return ModifierFactory<PointVariableModifier, Data::PointVariableModifier>::getInstance();
 	default:
 		return nullptr;
 	}
diff --git a/engines/mtropolis/modifier_factory.h b/engines/mtropolis/modifier_factory.h
index 164a8054319..fc7fb1b9dfe 100644
--- a/engines/mtropolis/modifier_factory.h
+++ b/engines/mtropolis/modifier_factory.h
@@ -28,13 +28,64 @@
 namespace MTropolis {
 
 struct ModifierLoaderContext {
+	explicit ModifierLoaderContext(ChildLoaderStack *childLoaderStack);
+
 	ChildLoaderStack *childLoaderStack;
 };
 
 struct IModifierFactory {
-	virtual Common::SharedPtr<Modifier> createModifier(ModifierLoaderContext &context, const Data::DataObject &dataObject) = 0;
+	virtual Common::SharedPtr<Modifier> createModifier(ModifierLoaderContext &context, const Data::DataObject &dataObject) const = 0;
+};
+
+struct IPlugInModifierFactory {
+	virtual Common::SharedPtr<Modifier> createModifier(ModifierLoaderContext &context, const Data::PlugInModifier &plugInModifierData) const = 0;
+};
+
+struct IPlugInModifierFactoryAndDataFactory : public IPlugInModifierFactory, public Data::IPlugInModifierDataFactory {
+};
+
+// Helper classes for plug-in modifier loaders
+struct PlugInModifierLoaderContext {
+	PlugInModifierLoaderContext(ModifierLoaderContext &modifierLoaderContext, const Data::PlugInModifier &plugInModifierData, PlugIn *plugIn);
+
+	ModifierLoaderContext &modifierLoaderContext;
+	const Data::PlugInModifier &plugInModifierData;
+	PlugIn *plugIn;
+};
+
+template<typename TModifier, typename TModifierData>
+class PlugInModifierFactory : public IPlugInModifierFactoryAndDataFactory {
+public:
+	explicit PlugInModifierFactory(PlugIn *plugIn);
+
+	Common::SharedPtr<Modifier> createModifier(ModifierLoaderContext &context, const Data::PlugInModifier &plugInModifierData) const override;
+	Common::SharedPtr<Data::PlugInModifierData> createModifierData() const override;
+
+private:
+	PlugIn &_plugIn;
 };
 
+template<typename TModifier, typename TModifierData>
+PlugInModifierFactory<TModifier, TModifierData>::PlugInModifierFactory(PlugIn *plugIn) : _plugIn(*plugIn) {
+}
+
+template<typename TModifier, typename TModifierData>
+Common::SharedPtr<Modifier> PlugInModifierFactory<TModifier, TModifierData>::createModifier(ModifierLoaderContext &context, const Data::PlugInModifier &plugInModifierData) const {
+	Common::SharedPtr<TModifier> modifier(new TModifier());
+
+	PlugInModifierLoaderContext plugInContext(context, plugInModifierData, &_plugIn);
+
+	if (!modifier->load(plugInContext, static_cast<const TModifierData &>(*plugInModifierData.plugInData.get())))
+		modifier.reset();
+
+	return Common::SharedPtr<Modifier>(modifier);
+}
+
+template<typename TModifier, typename TModifierData>
+Common::SharedPtr<Data::PlugInModifierData> PlugInModifierFactory<TModifier, TModifierData>::createModifierData() const {
+	return Common::SharedPtr<Data::PlugInModifierData>(new TModifierData());
+}
+
 IModifierFactory *getModifierFactoryForDataObjectType(Data::DataObjectTypes::DataObjectType dataObjectType);
 
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index e2f944bb924..3de435de6f2 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -76,8 +76,8 @@ bool MiniscriptModifier::load(ModifierLoaderContext &context, const Data::Minisc
 }
 
 bool MessengerModifier::load(ModifierLoaderContext &context, const Data::MessengerModifier &data) {
-	_guid = data.guid;
-	_name = data.name;
+	_guid = data.modHeader.guid;
+	_name = data.modHeader.name;
 
 	if (!_when.load(data.when) || !_send.load(data.send))
 		return false;
@@ -90,8 +90,8 @@ bool MessengerModifier::load(ModifierLoaderContext &context, const Data::Messeng
 }
 
 bool IfMessengerModifier::load(ModifierLoaderContext &context, const Data::IfMessengerModifier &data) {
-	_guid = data.guid;
-	_name = data.name;
+	_guid = data.modHeader.guid;
+	_name = data.modHeader.name;
 
 	if (!_when.load(data.when) || !_send.load(data.send))
 		return false;
@@ -107,5 +107,23 @@ bool IfMessengerModifier::load(ModifierLoaderContext &context, const Data::IfMes
 	return true;
 }
 
+bool BooleanVariableModifier::load(ModifierLoaderContext &context, const Data::BooleanVariableModifier &data) {
+	_guid = data.modHeader.guid;
+	_name = data.modHeader.name;
+
+	_value = (data.value != 0);
+
+	return true;
+}
+
+bool PointVariableModifier::load(ModifierLoaderContext &context, const Data::PointVariableModifier &data) {
+	_guid = data.modHeader.guid;
+	_name = data.modHeader.name;
+
+	_value.x = data.value.x;
+	_value.y = data.value.y;
+
+	return true;
+}
 
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 24967d3b6f8..68d00ad629d 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -81,6 +81,22 @@ private:
 	Common::SharedPtr<MiniscriptProgram> _program;
 };
 
+class BooleanVariableModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::BooleanVariableModifier &data);
+
+private:
+	bool _value;
+};
+
+class PointVariableModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::PointVariableModifier &data);
+
+private:
+	Point16 _value;
+};
+
 }	// End of namespace MTropolis
 
 #endif
diff --git a/engines/mtropolis/module.mk b/engines/mtropolis/module.mk
index f1c638093fe..1f32883ee6b 100644
--- a/engines/mtropolis/module.mk
+++ b/engines/mtropolis/module.mk
@@ -9,6 +9,8 @@ MODULE_OBJS = \
 	modifiers.o \
 	modifier_factory.o \
 	mtropolis.o \
+	plugin_standard.o \
+	plugin_standard_data.o \
 	runtime.o \
 	vthread.o
 
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index 459f0ebd76c..27f57ebbc1f 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -23,13 +23,83 @@
 #include "mtropolis/console.h"
 #include "mtropolis/runtime.h"
 
+#include "mtropolis/plugins.h"
+
 #include "common/config-manager.h"
+#include "common/macresman.h"
+#include "common/ptr.h"
+#include "common/stuffit.h"
 
 namespace MTropolis {
 
+
+struct MacObsidianResources : public ProjectResources {
+	MacObsidianResources();
+	~MacObsidianResources();
+
+	void setup();
+	Common::SeekableReadStream *getSegmentStream(int index) const;
+
+private:
+	Common::MacResManager _installerResMan;
+	Common::MacResManager _dataFileResMan[5];
+
+	Common::SeekableReadStream *_installerDataForkStream;
+	Common::Archive *_installerArchive;
+	Common::SeekableReadStream *_segmentStreams[6];
+};
+
+MacObsidianResources::MacObsidianResources() : _installerArchive(nullptr), _installerDataForkStream(nullptr) {
+	for (int i = 0; i < 6; i++)
+		_segmentStreams[i] = nullptr;
+}
+
+void MacObsidianResources::setup() {
+	if (!_installerResMan.open("Obsidian Installer"))
+		error("Failed to open Obsidian Installer");
+
+	if (!_installerResMan.hasDataFork())
+		error("Obsidian Installer has no data fork");
+
+	_installerDataForkStream = _installerResMan.getDataFork();
+
+	_installerArchive = Common::createStuffItArchive(_installerDataForkStream);
+	if (!_installerArchive)
+		error("Failed to open Obsidian Installer archive");
+
+	_segmentStreams[0] = _installerArchive->createReadStreamForMember("Obsidian Data 1");
+
+	for (int i = 0; i < 5; i++) {
+		char fileName[32];
+		sprintf(fileName, "Obsidian Data %i", (i + 2));
+
+		Common::MacResManager &resMan = _dataFileResMan[i];
+		if (!resMan.open(fileName))
+			error("Failed to open data file %s", fileName);
+
+		if (!resMan.hasDataFork())
+			error("Data fork in %s is missing", fileName);
+
+		_segmentStreams[1 + i] = resMan.getDataFork();
+	}
+}
+
+Common::SeekableReadStream *MacObsidianResources::getSegmentStream(int index) const {
+	return _segmentStreams[index];
+}
+
+MacObsidianResources::~MacObsidianResources() {
+	for (int i = 0; i < 6; i++)
+		delete _segmentStreams[i];
+
+	delete _installerArchive;
+	delete _installerDataForkStream;
+}
+
+
 MTropolisEngine::MTropolisEngine(OSystem *syst, const MTropolisGameDescription *gameDesc) : Engine(syst), _gameDescription(gameDesc) {
 
-	if (gameDesc->gameID == GID_OBSIDIAN) {
+	if (gameDesc->gameID == GID_OBSIDIAN && _gameDescription->desc.platform == Common::kPlatformWindows) {
 		const Common::FSNode gameDataDir(ConfMan.get("path"));
 		SearchMan.addSubDirectoryMatching(gameDataDir, "Obsidian");
 		SearchMan.addSubDirectoryMatching(gameDataDir, "Obsidian/RESOURCE");
@@ -64,6 +134,33 @@ Common::Error MTropolisEngine::run() {
 		desc->addSegment(4, "Obsidian Data 5.MPX");
 		desc->addSegment(5, "Obsidian Data 6.MPX");
 
+		desc->addPlugIn(PlugIns::createStandard());
+		desc->addPlugIn(PlugIns::createObsidian());
+
+		_runtime->queueProject(desc);
+	} else if (_gameDescription->gameID == GID_OBSIDIAN && _gameDescription->desc.platform == Common::kPlatformMacintosh) {
+		MacObsidianResources *resources = new MacObsidianResources();
+		Common::SharedPtr<ProjectResources> resPtr(resources);
+
+		resources->setup();
+
+		_runtime->addVolume(0, "Installed", true);
+		_runtime->addVolume(1, "OBSIDIAN1", true);
+		_runtime->addVolume(2, "OBSIDIAN2", true);
+		_runtime->addVolume(3, "OBSIDIAN3", true);
+		_runtime->addVolume(4, "OBSIDIAN4", true);
+		_runtime->addVolume(5, "OBSIDIAN5", true);
+
+		Common::SharedPtr<ProjectDescription> desc(new ProjectDescription());
+
+		for (int i = 0; i < 6; i++)
+			desc->addSegment(i, resources->getSegmentStream(i));
+
+		desc->addPlugIn(PlugIns::createStandard());
+		desc->addPlugIn(PlugIns::createObsidian());
+
+		desc->setResources(resPtr);
+
 		_runtime->queueProject(desc);
 	}
 
diff --git a/engines/mtropolis/plugin_obsidian.cpp b/engines/mtropolis/plugin_obsidian.cpp
new file mode 100644
index 00000000000..76a15a02ae0
--- /dev/null
+++ b/engines/mtropolis/plugin_obsidian.cpp
@@ -0,0 +1,45 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mtropolis/plugin_obsidian.h"
+#include "mtropolis/plugins.h"
+
+namespace MTropolis {
+
+namespace Obsidian {
+
+ObsidianPlugIn::ObsidianPlugIn() {
+}
+
+void ObsidianPlugIn::registerModifiers(IPlugInModifierRegistrar *registrar) const {
+}
+
+} // End of namespace ObsidianPlugIn
+
+namespace PlugIns {
+
+Common::SharedPtr<PlugIn> createObsidian() {
+	return Common::SharedPtr<PlugIn>(new Obsidian::ObsidianPlugIn());
+}
+
+} // End of namespace PlugIns
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/plugin_obsidian.h b/engines/mtropolis/plugin_obsidian.h
new file mode 100644
index 00000000000..9b0e948b520
--- /dev/null
+++ b/engines/mtropolis/plugin_obsidian.h
@@ -0,0 +1,42 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_PLUGIN_OBSIDIAN_H
+#define MTROPOLIS_PLUGIN_OBSIDIAN_H
+
+#include "mtropolis/runtime.h"
+
+namespace MTropolis {
+
+namespace Obsidian {
+
+class ObsidianPlugIn : public MTropolis::PlugIn {
+public:
+	ObsidianPlugIn();
+
+	void registerModifiers(IPlugInModifierRegistrar *registrar) const override;
+};
+
+} // End of namespace Obsidian
+
+} // End of namespace MTropolis
+
+#endif
diff --git a/engines/mtropolis/plugin_standard.cpp b/engines/mtropolis/plugin_standard.cpp
new file mode 100644
index 00000000000..ec87a129098
--- /dev/null
+++ b/engines/mtropolis/plugin_standard.cpp
@@ -0,0 +1,55 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mtropolis/plugin_standard.h"
+#include "mtropolis/plugins.h"
+
+
+namespace MTropolis {
+
+namespace Standard {
+
+bool CursorModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::CursorModifier &data) {
+	if (!_applyWhen.load(data.applyWhen) || !_removeWhen.load(data.removeWhen))
+		return false;
+	_cursorID = data.cursorID;
+
+	return true;
+}
+
+StandardPlugIn::StandardPlugIn() : _cursorModifierFactory(this) {
+}
+
+void StandardPlugIn::registerModifiers(IPlugInModifierRegistrar *registrar) const {
+	registrar->registerPlugInModifier("CursorMod", &_cursorModifierFactory);
+}
+
+} // End of namespace Standard
+
+namespace PlugIns {
+
+Common::SharedPtr<PlugIn> createStandard() {
+	return Common::SharedPtr<PlugIn>(new Standard::StandardPlugIn());
+}
+
+} // End of namespace MTropolis
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/plugin_standard.h b/engines/mtropolis/plugin_standard.h
new file mode 100644
index 00000000000..e78b017b2d5
--- /dev/null
+++ b/engines/mtropolis/plugin_standard.h
@@ -0,0 +1,59 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_PLUGIN_STANDARD_H
+#define MTROPOLIS_PLUGIN_STANDARD_H
+
+#include "mtropolis/modifier_factory.h"
+#include "mtropolis/runtime.h"
+#include "mtropolis/plugin_standard_data.h"
+
+namespace MTropolis {
+
+namespace Standard {
+
+class StandardPlugIn;
+
+class CursorModifier : public Modifier {
+public:
+	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::CursorModifier &data);
+
+private:
+	Event _applyWhen;
+	Event _removeWhen;
+	uint32 _cursorID;
+};
+
+class StandardPlugIn : public MTropolis::PlugIn {
+public:
+	StandardPlugIn();
+
+	void registerModifiers(IPlugInModifierRegistrar *registrar) const override;
+
+private:
+	PlugInModifierFactory<CursorModifier, Data::Standard::CursorModifier> _cursorModifierFactory;
+};
+
+} // End of namespace Standard
+
+} // End of namespace MTropolis
+
+#endif
diff --git a/engines/mtropolis/plugin_standard_data.cpp b/engines/mtropolis/plugin_standard_data.cpp
new file mode 100644
index 00000000000..c69e73692ee
--- /dev/null
+++ b/engines/mtropolis/plugin_standard_data.cpp
@@ -0,0 +1,45 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mtropolis/plugin_standard_data.h"
+
+namespace MTropolis {
+
+namespace Data {
+
+namespace Standard {
+
+DataReadErrorCode CursorModifier::load(const PlugInModifier &prefix, DataReader &reader) {
+	if (prefix.plugInRevision != 1)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU16(unknown1) || !applyWhen.load(reader) || !reader.readU16(unknown2)
+		|| !removeWhen.load(reader) || !reader.readU16(unknown3) || !reader.readU32(cursorID) || !reader.readBytes(unknown4))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
+} // End of namespace Standard
+
+} // End of namespace Data
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/plugin_standard_data.h b/engines/mtropolis/plugin_standard_data.h
new file mode 100644
index 00000000000..04330ffce16
--- /dev/null
+++ b/engines/mtropolis/plugin_standard_data.h
@@ -0,0 +1,52 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_PLUGIN_STANDARD_DATA_H
+#define MTROPOLIS_PLUGIN_STANDARD_DATA_H
+
+#include "mtropolis/data.h"
+
+namespace MTropolis {
+
+namespace Data {
+
+namespace Standard {
+
+struct CursorModifier : public PlugInModifierData {
+	uint16 unknown1;
+	Event applyWhen;
+	uint16 unknown2;
+	Event removeWhen;
+	uint16 unknown3;
+	uint32 cursorID;
+	uint8 unknown4[4];
+
+protected:
+	DataReadErrorCode load(const PlugInModifier &prefix, DataReader &reader) override;
+};
+
+} // End of namespace Standard
+
+} // End of namespace Data
+
+} // End of namespace MTropolis
+
+#endif
diff --git a/engines/mtropolis/plugins.h b/engines/mtropolis/plugins.h
new file mode 100644
index 00000000000..fc08a508135
--- /dev/null
+++ b/engines/mtropolis/plugins.h
@@ -0,0 +1,40 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_PLUGINS_H
+#define MTROPOLIS_PLUGINS_H
+
+#include "common/ptr.h"
+
+namespace MTropolis {
+
+class PlugIn;
+
+namespace PlugIns {
+
+Common::SharedPtr<PlugIn> createStandard();
+Common::SharedPtr<PlugIn> createObsidian();
+
+} // End of namespace PlugIns
+
+} // End of namespace MTropolis
+
+#endif
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index ccf92e897c3..f4d1a1341c6 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -36,13 +36,23 @@ MessageFlags::MessageFlags() : relay(true), cascade(true), immediate(true) {
 Event::Event() : eventType(0), eventInfo(0) {
 }
 
-bool Event::load(const Data::Event& data) {
+bool Event::load(const Data::Event &data) {
 	eventType = data.eventID;
 	eventInfo = data.eventInfo;
 
 	return true;
 }
 
+void IPlugInModifierRegistrar::registerPlugInModifier(const char *name, const IPlugInModifierFactoryAndDataFactory *loaderFactory) {
+	return this->registerPlugInModifier(name, loaderFactory, loaderFactory);
+}
+
+PlugIn::~PlugIn() {
+}
+
+ProjectResources::~ProjectResources() {
+}
+
 ProjectDescription::ProjectDescription() {
 }
 
@@ -53,6 +63,15 @@ void ProjectDescription::addSegment(int volumeID, const char *filePath) {
 	SegmentDescription desc;
 	desc.volumeID = volumeID;
 	desc.filePath = filePath;
+	desc.stream = nullptr;
+
+	_segments.push_back(desc);
+}
+
+void ProjectDescription::addSegment(int volumeID, Common::SeekableReadStream *stream) {
+	SegmentDescription desc;
+	desc.volumeID = volumeID;
+	desc.stream = stream;
 
 	_segments.push_back(desc);
 }
@@ -61,6 +80,23 @@ const Common::Array<SegmentDescription> &ProjectDescription::getSegments() const
 	return _segments;
 }
 
+void ProjectDescription::addPlugIn(const Common::SharedPtr<PlugIn>& plugIn) {
+	_plugIns.push_back(plugIn);
+}
+
+const Common::Array<Common::SharedPtr<PlugIn> >& ProjectDescription::getPlugIns() const {
+	return _plugIns;
+}
+
+void ProjectDescription::setResources(const Common::SharedPtr<ProjectResources> &resources) {
+	_resources = resources;
+}
+
+const Common::SharedPtr<ProjectResources> &ProjectDescription::getResources() const {
+	return _resources;
+}
+
+
 const Common::Array<Common::SharedPtr<Modifier> >& SimpleModifierContainer::getModifiers() const {
 	return _modifiers;
 }
@@ -126,6 +162,24 @@ const Common::Array<Common::SharedPtr<Modifier> > &IModifierContainer::getModifi
 	return const_cast<IModifierContainer &>(*this).getModifiers();
 };
 
+ProjectPlugInRegistry::ProjectPlugInRegistry() {
+}
+
+void ProjectPlugInRegistry::registerPlugInModifier(const char *name, const Data::IPlugInModifierDataFactory *loader, const IPlugInModifierFactory *factory) {
+	_dataLoaderRegistry.registerLoader(name, loader);
+	_factoryRegistry[name] = factory;
+}
+
+const Data::PlugInModifierRegistry& ProjectPlugInRegistry::getDataLoaderRegistry() const {
+	return _dataLoaderRegistry;
+}
+
+const IPlugInModifierFactory *ProjectPlugInRegistry::findPlugInModifierFactory(const char *name) const {
+	Common::HashMap<Common::String, const IPlugInModifierFactory *>::const_iterator it = _factoryRegistry.find(name);
+	if (it == _factoryRegistry.end())
+		return nullptr;
+	return it->_value;
+}
 
 Project::Project()
 	: _projectFormat(Data::kProjectFormatUnknown), _isBigEndian(false), _haveGlobalObjectInfo(false) {
@@ -135,6 +189,20 @@ Project::~Project() {
 }
 
 void Project::loadFromDescription(const ProjectDescription& desc) {
+	_resources = desc.getResources();
+
+	const Common::Array<Common::SharedPtr<PlugIn> > &plugIns = desc.getPlugIns();
+
+	for (Common::Array<Common::SharedPtr<PlugIn> >::const_iterator it = plugIns.begin(), itEnd = plugIns.end(); it != itEnd; ++it) {
+		Common::SharedPtr<PlugIn> plugIn = (*it);
+
+		_plugIns.push_back(plugIn);
+
+		plugIn->registerModifiers(&_plugInRegistry);
+	}
+
+	const Data::PlugInModifierRegistry &plugInDataLoaderRegistry = _plugInRegistry.getDataLoaderRegistry();
+
 	size_t numSegments = desc.getSegments().size();
 	_segments.resize(numSegments);
 
@@ -145,7 +213,7 @@ void Project::loadFromDescription(const ProjectDescription& desc) {
 	// Try to open the first segment
 	openSegmentStream(0);
 
-	Common::SeekableReadStream *baseStream = _segments[0].stream.get();
+	Common::SeekableReadStream *baseStream = _segments[0].weakStream;
 	uint16_t startValue = baseStream->readUint16LE();
 
 	if (startValue == 1) {
@@ -168,14 +236,14 @@ void Project::loadFromDescription(const ProjectDescription& desc) {
 	Data::DataReader reader(stream, _projectFormat);
 
 	Common::SharedPtr<Data::DataObject> dataObject;
-	Data::loadDataObject(reader, dataObject);
+	Data::loadDataObject(_plugInRegistry.getDataLoaderRegistry(), reader, dataObject);
 
 	if (!dataObject || dataObject->getType() != Data::DataObjectTypes::kProjectHeader) {
 		error("Expected project header but found something else");
 
 	}
 
-	Data::loadDataObject(reader, dataObject);
+	Data::loadDataObject(plugInDataLoaderRegistry, reader, dataObject);
 	if (!dataObject || dataObject->getType() != Data::DataObjectTypes::kProjectCatalog) {
 		error("Expected project catalog but found something else");
 	}
@@ -229,14 +297,20 @@ void Project::openSegmentStream(int segmentIndex) {
 
 	Segment &segment = _segments[segmentIndex];
 
-	if (segment.stream)
+	if (segment.weakStream)
 		return;
 
-	Common::File *f = new Common::File();
-	segment.stream.reset(f);
+	if (segment.desc.stream) {
+		segment.rcStream.reset();
+		segment.weakStream = segment.desc.stream;
+	} else {
+		Common::File *f = new Common::File();
+		segment.rcStream.reset(f);
+		segment.weakStream = f;
 
-	if (!f->open(segment.desc.filePath)) {
-		error("Failed to open segment file %s", segment.desc.filePath.c_str());
+		if (!f->open(segment.desc.filePath)) {
+			error("Failed to open segment file %s", segment.desc.filePath.c_str());
+		}
 	}
 }
 
@@ -246,21 +320,26 @@ void Project::loadBootStream(size_t streamIndex) {
 	size_t segmentIndex = streamDesc.segmentIndex;
 	openSegmentStream(segmentIndex);
 
-	Common::SeekableSubReadStreamEndian stream(_segments[segmentIndex].stream.get(), streamDesc.pos, streamDesc.pos + streamDesc.size, _isBigEndian);
+	Common::SeekableSubReadStreamEndian stream(_segments[segmentIndex].weakStream, streamDesc.pos, streamDesc.pos + streamDesc.size, _isBigEndian);
 	Data::DataReader reader(stream, _projectFormat);
 
 	ChildLoaderStack loaderStack;
 
+	const Data::PlugInModifierRegistry &plugInDataLoaderRegistry = _plugInRegistry.getDataLoaderRegistry();
+
+	size_t numObjectsLoaded = 0;
 	for (;;) {
 		Common::SharedPtr<Data::DataObject> dataObject;
-		Data::loadDataObject(reader, dataObject);
+		Data::loadDataObject(plugInDataLoaderRegistry, reader, dataObject);
+
+		numObjectsLoaded++;
 
 		if (!dataObject) {
 			error("Failed to load project boot data");
 		}
 
 		if (loaderStack.contexts.size() > 0) {
-			loadRuntimeContextualObject(loaderStack, *dataObject.get());
+			loadContextualObject(loaderStack, *dataObject.get());
 		} else {
 			// Root-level objects
 			switch (dataObject->getType()) {
@@ -345,25 +424,36 @@ void Project::loadGlobalObjectInfo(ChildLoaderStack& loaderStack, const Data::Gl
 	}
 }
 
-
-static Common::SharedPtr<Modifier> loadModifierObject(ModifierLoaderContext &loaderContext, const Data::DataObject &dataObject) {
+Common::SharedPtr<Modifier> Project::loadModifierObject(ModifierLoaderContext &loaderContext, const Data::DataObject &dataObject) {
 	// Special case for debris
 	if (dataObject.getType() == Data::DataObjectTypes::kDebris)
 		return nullptr;
 
-	IModifierFactory *factory = getModifierFactoryForDataObjectType(dataObject.getType());
+	Common::SharedPtr<Modifier> modifier;
+
+	// Special case for plug-ins
+	if (dataObject.getType() == Data::DataObjectTypes::kPlugInModifier) {
+		const Data::PlugInModifier &plugInData = static_cast<const Data::PlugInModifier &>(dataObject);
+		const IPlugInModifierFactory *factory = _plugInRegistry.findPlugInModifierFactory(plugInData.modifierName);
+		if (!factory)
+			error("Unknown or unsupported plug-in modifier type");
 
-	if (!factory)
-		error("Unknown or unsupported modifier type, or non-modifier encountered where a modifier was expected");
+		modifier = factory->createModifier(loaderContext, plugInData);
+	} else {
+		IModifierFactory *factory = getModifierFactoryForDataObjectType(dataObject.getType());
 
-	Common::SharedPtr<Modifier> modifier = factory->createModifier(loaderContext, dataObject);
+		if (!factory)
+			error("Unknown or unsupported modifier type, or non-modifier encountered where a modifier was expected");
+
+		modifier = factory->createModifier(loaderContext, dataObject);
+	}
 	if (!modifier)
 		error("Modifier object failed to load");
 
 	return modifier;
 }
 
-void loadRuntimeContextualObject(ChildLoaderStack &stack, const Data::DataObject &dataObject) {
+void Project::loadContextualObject(ChildLoaderStack &stack, const Data::DataObject &dataObject) {
 	ChildLoaderContext &topContext = stack.contexts.back();
 
 	// The stack entry must always be popped before loading the object because the load process may descend into more children,
@@ -375,8 +465,7 @@ void loadRuntimeContextualObject(ChildLoaderStack &stack, const Data::DataObject
 			if ((--topContext.remainingCount) == 0)
 				stack.contexts.pop_back();
 
-			ModifierLoaderContext loaderContext;
-			loaderContext.childLoaderStack = &stack;
+			ModifierLoaderContext loaderContext(&stack);
 
 			container->appendModifier(loadModifierObject(loaderContext, dataObject));
 		}
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 3ce62399d02..2d333b11367 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -35,9 +35,17 @@
 namespace MTropolis {
 
 class Project;
+class PlugIn;
 class Modifier;
-
-
+struct IModifierFactory;
+struct IPlugInModifierFactory;
+struct IPlugInModifierFactoryAndDataFactory;
+struct ModifierLoaderContext;
+
+struct Point16 {
+	int16 x;
+	int16 y;
+};
 
 struct MessageFlags {
 	bool relay : 1;
@@ -76,6 +84,23 @@ enum MessageDestination {
 struct SegmentDescription {
 	int volumeID;
 	Common::String filePath;
+	Common::SeekableReadStream *stream;
+};
+
+struct IPlugInModifierRegistrar {
+	virtual void registerPlugInModifier(const char *name, const Data::IPlugInModifierDataFactory *loader, const IPlugInModifierFactory *factory) = 0;
+	void registerPlugInModifier(const char *name, const IPlugInModifierFactoryAndDataFactory *loaderFactory);
+};
+
+class PlugIn {
+public:
+	virtual ~PlugIn();
+
+	virtual void registerModifiers(IPlugInModifierRegistrar *registrar) const = 0;
+};
+
+struct ProjectResources {
+	virtual ~ProjectResources();
 };
 
 class ProjectDescription {
@@ -85,10 +110,19 @@ public:
 	~ProjectDescription();
 
 	void addSegment(int volumeID, const char *filePath);
+	void addSegment(int volumeID, Common::SeekableReadStream *stream);
 	const Common::Array<SegmentDescription> &getSegments() const;
 
+	void addPlugIn(const Common::SharedPtr<PlugIn> &plugIn);
+	const Common::Array<Common::SharedPtr<PlugIn> > &getPlugIns() const;
+
+	void setResources(const Common::SharedPtr<ProjectResources> &resources);
+	const Common::SharedPtr<ProjectResources> &getResources() const;
+
 private:
 	Common::Array<SegmentDescription> _segments;
+	Common::Array<Common::SharedPtr<PlugIn> > _plugIns;
+	Common::SharedPtr<ProjectResources> _resources;
 };
 
 struct VolumeState {
@@ -191,6 +225,20 @@ struct ChildLoaderStack {
 	Common::Array<ChildLoaderContext> contexts;
 };
 
+class ProjectPlugInRegistry : public IPlugInModifierRegistrar  {
+public:
+	ProjectPlugInRegistry();
+
+	void registerPlugInModifier(const char *name, const Data::IPlugInModifierDataFactory *dataFactory, const IPlugInModifierFactory *factory) override;
+
+	const Data::PlugInModifierRegistry &getDataLoaderRegistry() const;
+	const IPlugInModifierFactory *findPlugInModifierFactory(const char *name) const;
+
+private:
+	Data::PlugInModifierRegistry _dataLoaderRegistry;
+	Common::HashMap<Common::String, const IPlugInModifierFactory *> _factoryRegistry;
+};
+
 class Project : public Structural {
 
 public:
@@ -202,7 +250,8 @@ public:
 private:
 	struct Segment {
 		SegmentDescription desc;
-		Common::SharedPtr<Common::SeekableReadStream> stream;
+		Common::SharedPtr<Common::SeekableReadStream> rcStream;
+		Common::SeekableReadStream *weakStream;
 	};
 
 	enum StreamType {
@@ -235,6 +284,8 @@ private:
 	void loadPresentationSettings(const Data::PresentationSettings &presentationSettings);
 	void loadAssetCatalog(const Data::AssetCatalog &assetCatalog);
 	void loadGlobalObjectInfo(ChildLoaderStack &loaderStack, const Data::GlobalObjectInfo &globalObjectInfo);
+	void loadContextualObject(ChildLoaderStack &stack, const Data::DataObject &dataObject);
+	Common::SharedPtr<Modifier> loadModifierObject(ModifierLoaderContext &loaderContext, const Data::DataObject &dataObject);
 
 	Common::Array<Segment> _segments;
 	Common::Array<StreamDesc> _streams;
@@ -250,6 +301,11 @@ private:
 
 	bool _haveGlobalObjectInfo;
 	SimpleModifierContainer _globalModifiers;
+
+	ProjectPlugInRegistry _plugInRegistry;
+
+	Common::Array<Common::SharedPtr<PlugIn> > _plugIns;
+	Common::SharedPtr<ProjectResources> _resources;
 };
 
 class Section : public Structural {
@@ -271,8 +327,6 @@ protected:
 	Common::String _name;
 };
 
-void loadRuntimeContextualObject(ChildLoaderStack &stack, const Data::DataObject &dataObject);
-
 } // End of namespace MTropolis
 
 #endif


Commit: 48c3a890255f625a6bff77ae7d49af631a98551d
    https://github.com/scummvm/scummvm/commit/48c3a890255f625a6bff77ae7d49af631a98551d
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add Obsidian plug-in stubs and STransCt modifier stub

Changed paths:
  A engines/mtropolis/plugin_obsidian_data.cpp
  A engines/mtropolis/plugin_obsidian_data.h
    engines/mtropolis/module.mk
    engines/mtropolis/plugin_standard.cpp
    engines/mtropolis/plugin_standard.h
    engines/mtropolis/plugin_standard_data.cpp
    engines/mtropolis/plugin_standard_data.h


diff --git a/engines/mtropolis/module.mk b/engines/mtropolis/module.mk
index 1f32883ee6b..873333d02f6 100644
--- a/engines/mtropolis/module.mk
+++ b/engines/mtropolis/module.mk
@@ -9,6 +9,8 @@ MODULE_OBJS = \
 	modifiers.o \
 	modifier_factory.o \
 	mtropolis.o \
+	plugin_obsidian.o \
+	plugin_obsidian_data.o \
 	plugin_standard.o \
 	plugin_standard_data.o \
 	runtime.o \
diff --git a/engines/mtropolis/plugin_obsidian_data.cpp b/engines/mtropolis/plugin_obsidian_data.cpp
new file mode 100644
index 00000000000..2983fe838b4
--- /dev/null
+++ b/engines/mtropolis/plugin_obsidian_data.cpp
@@ -0,0 +1,35 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mtropolis/plugin_obsidian_data.h"
+
+namespace MTropolis {
+
+namespace Data {
+
+namespace Obsidian {
+
+
+} // End of namespace Obsidian
+
+} // End of namespace Data
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/plugin_obsidian_data.h b/engines/mtropolis/plugin_obsidian_data.h
new file mode 100644
index 00000000000..6c5ed92db44
--- /dev/null
+++ b/engines/mtropolis/plugin_obsidian_data.h
@@ -0,0 +1,40 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_PLUGIN_OBSIDIAN_DATA_H
+#define MTROPOLIS_PLUGIN_OBSIDIAN_DATA_H
+
+#include "mtropolis/data.h"
+
+namespace MTropolis {
+
+namespace Data {
+
+namespace Obsidian {
+
+
+} // End of namespace Obsidian
+
+} // End of namespace Data
+
+} // End of namespace MTropolis
+
+#endif
diff --git a/engines/mtropolis/plugin_standard.cpp b/engines/mtropolis/plugin_standard.cpp
index ec87a129098..8fe4adc57e1 100644
--- a/engines/mtropolis/plugin_standard.cpp
+++ b/engines/mtropolis/plugin_standard.cpp
@@ -35,11 +35,16 @@ bool CursorModifier::load(const PlugInModifierLoaderContext &context, const Data
 	return true;
 }
 
-StandardPlugIn::StandardPlugIn() : _cursorModifierFactory(this) {
+bool STransCtModifier::load(const PlugInModifierLoaderContext& context, const Data::Standard::STransCtModifier& data) {
+	return true;
+}
+
+StandardPlugIn::StandardPlugIn() : _cursorModifierFactory(this), _sTransCtModifierFactory(this) {
 }
 
 void StandardPlugIn::registerModifiers(IPlugInModifierRegistrar *registrar) const {
 	registrar->registerPlugInModifier("CursorMod", &_cursorModifierFactory);
+	registrar->registerPlugInModifier("STransCt", &_sTransCtModifierFactory);
 }
 
 } // End of namespace Standard
diff --git a/engines/mtropolis/plugin_standard.h b/engines/mtropolis/plugin_standard.h
index e78b017b2d5..ab1d9ade6e0 100644
--- a/engines/mtropolis/plugin_standard.h
+++ b/engines/mtropolis/plugin_standard.h
@@ -42,6 +42,12 @@ private:
 	uint32 _cursorID;
 };
 
+// Some sort of scene transition modifier
+class STransCtModifier : public Modifier {
+public:
+	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::STransCtModifier &data);
+};
+
 class StandardPlugIn : public MTropolis::PlugIn {
 public:
 	StandardPlugIn();
@@ -50,6 +56,7 @@ public:
 
 private:
 	PlugInModifierFactory<CursorModifier, Data::Standard::CursorModifier> _cursorModifierFactory;
+	PlugInModifierFactory<STransCtModifier, Data::Standard::STransCtModifier> _sTransCtModifierFactory;
 };
 
 } // End of namespace Standard
diff --git a/engines/mtropolis/plugin_standard_data.cpp b/engines/mtropolis/plugin_standard_data.cpp
index c69e73692ee..56b299c2494 100644
--- a/engines/mtropolis/plugin_standard_data.cpp
+++ b/engines/mtropolis/plugin_standard_data.cpp
@@ -38,6 +38,19 @@ DataReadErrorCode CursorModifier::load(const PlugInModifier &prefix, DataReader
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode STransCtModifier::load(const PlugInModifier &prefix, DataReader &reader) {
+	if (prefix.plugInRevision != 0)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU16(unknown1) || !unknown2.load(reader) || !reader.readU16(unknown3) || !unknown4.load(reader)
+		|| !reader.readU16(unknown5) || !reader.readU32(unknown6) || !reader.readU16(unknown7) || !reader.readU32(unknown8)
+		|| !reader.readU16(unknown9) || !reader.readU32(unknown10) || !reader.readU16(unknown11) || !reader.readU32(unknown12)
+		|| !reader.readU16(unknown13) || !reader.readU32(unknown14) || !reader.readU16(unknown15) || !reader.readBytes(unknown16))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 } // End of namespace Standard
 
 } // End of namespace Data
diff --git a/engines/mtropolis/plugin_standard_data.h b/engines/mtropolis/plugin_standard_data.h
index 04330ffce16..38a029514fa 100644
--- a/engines/mtropolis/plugin_standard_data.h
+++ b/engines/mtropolis/plugin_standard_data.h
@@ -43,6 +43,28 @@ protected:
 	DataReadErrorCode load(const PlugInModifier &prefix, DataReader &reader) override;
 };
 
+struct STransCtModifier : public PlugInModifierData {
+	uint16 unknown1;	// Type tag? (0x17)
+	Event unknown2;		// Probably "apply when"
+	uint16 unknown3;	// Type tag? (0x17)
+	Event unknown4;		// Probably "remove when"
+	uint16 unknown5;	// Type tag? (1)
+	uint32 unknown6;
+	uint16 unknown7;	// Type tag? (1)
+	uint32 unknown8;
+	uint16 unknown9;	// Type tag? (1)
+	uint32 unknown10;
+	uint16 unknown11;	// Type tag? (1)
+	uint32 unknown12;
+	uint16 unknown13;	// Type tag? (1)
+	uint32 unknown14;
+	uint16 unknown15;	// Type tag? (0x14)
+	uint8 unknown16[2];
+
+protected:
+	DataReadErrorCode load(const PlugInModifier &prefix, DataReader &reader) override;
+};
+
 } // End of namespace Standard
 
 } // End of namespace Data


Commit: 6f16b72d07d36288c4722b6984a6c503b33d9dec
    https://github.com/scummvm/scummvm/commit/6f16b72d07d36288c4722b6984a6c503b33d9dec
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Convert Mac version 47e15 floats to IEEE double

Changed paths:
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/miniscript.cpp


diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index 0396519bd09..01bacd400b3 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -22,6 +22,8 @@
 #include "mtropolis/data.h"
 #include "common/debug.h"
 
+#include <float.h>
+
 namespace MTropolis {
 
 namespace Data {
@@ -80,6 +82,47 @@ bool DataReader::readF64(double &value) {
 	return checkErrorAndReset();
 }
 
+bool DataReader::readTruncated8087XPDouble(double &value) {
+	uint64 u64 = _stream.readUint64();
+	if (!checkErrorAndReset())
+		return false;
+	
+	uint8_t sign = static_cast<uint16_t>((u64 >> 63) & 1);
+	int16_t exponent = static_cast<int16_t>((u64 >> 48) & 0x7fff);
+	uint64_t mantissa = (u64 & 0x7fffffffffffULL);
+
+	// Adjust exponent
+	exponent = exponent - 15360;
+	if (exponent > 2047) {
+		// Too big
+		if (sign)
+			value = -DBL_MAX;
+		else
+			value = DBL_MAX;
+	} else if (exponent > 0) {
+		// Normal number
+		uint64_t recombined = (static_cast<uint64_t>(sign) << 63) | (static_cast<uint64_t>(exponent) << 52) | (static_cast<uint64_t>(mantissa) << 5);
+		memcpy(&value, &recombined, 8);
+	} else {
+		// Subnormal number
+		mantissa |= 0x800000000000ULL;
+		mantissa <<= 5;
+		if (exponent <= -52) {
+			mantissa = 0;
+			exponent = 0;
+		} else {
+			mantissa >>= (-exponent);
+			exponent = 0;
+		}
+
+		uint64_t recombined = (static_cast<uint64_t>(sign) << 63) | (static_cast<uint64_t>(exponent) << 52) | (static_cast<uint64_t>(mantissa) << 5);
+		memcpy(&value, &recombined, 8);
+	}
+
+	return true;
+}
+
+
 bool DataReader::read(void *dest, size_t size) {
 	while (size > 0) {
 		uint32 thisChunkSize = UINT32_MAX;
@@ -613,7 +656,7 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 		dataObject = new PlugInModifier();
 		break;
 	default:
-		debug(1, "Unrecognized data object type %x", static_cast<int>(type));
+		warning("Unrecognized data object type %x", static_cast<int>(type));
 		break;
 	}
 
@@ -631,7 +674,7 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 	if (type == DataObjectTypes::kPlugInModifier) {
 		const IPlugInModifierDataFactory *plugInLoader = registry.findLoader(static_cast<const PlugInModifier *>(dataObject)->modifierName);
 		if (!plugInLoader) {
-			debug(1, "Unrecognized plug-in modifier type %s", static_cast<const PlugInModifier *>(dataObject)->modifierName);
+			warning("Unrecognized plug-in modifier type %s", static_cast<const PlugInModifier *>(dataObject)->modifierName);
 			outObject.reset();
 			return kDataReadErrorPlugInNotFound;
 		}
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index 7bf1cec0d42..8283bfdf494 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -144,6 +144,7 @@ public:
 	bool readS64(int64 &value);
 	bool readF32(float &value);
 	bool readF64(double &value);
+	bool readTruncated8087XPDouble(double &value);
 	bool read(void *dest, size_t size);
 
 	// Reads a terminated string where "length" is the number of characters including a null terminator
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 1faf3f5ee52..89e2fbf8231 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -129,8 +129,15 @@ bool MiniscriptInstructionLoader<MiniscriptInstructions::PushValue>::loadInstruc
 		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeNull, nullptr);
 	else if (dataType == 0x15) {
 		double d;
-		if (!instrDataReader.readF64(d))
+		if (instrDataReader.getProjectFormat() == Data::kProjectFormatMacintosh) {
+			if (!instrDataReader.readTruncated8087XPDouble(d))
+				return false;
+		} else if (instrDataReader.getProjectFormat() == Data::kProjectFormatWindows) {
+			if (!instrDataReader.readF64(d))
+				return false;
+		} else {
 			return false;
+		}
 
 		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeDouble, &d);
 	} else if (dataType == 0x1a) {


Commit: 257ce80381b8cca149058516098868feffe5c2bd
    https://github.com/scummvm/scummvm/commit/257ce80381b8cca149058516098868feffe5c2bd
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Parse entire float80 values

Changed paths:
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/miniscript.cpp


diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index 01bacd400b3..34abc44dd52 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -82,47 +82,50 @@ bool DataReader::readF64(double &value) {
 	return checkErrorAndReset();
 }
 
-bool DataReader::readTruncated8087XPDouble(double &value) {
-	uint64 u64 = _stream.readUint64();
-	if (!checkErrorAndReset())
-		return false;
-	
-	uint8_t sign = static_cast<uint16_t>((u64 >> 63) & 1);
-	int16_t exponent = static_cast<int16_t>((u64 >> 48) & 0x7fff);
-	uint64_t mantissa = (u64 & 0x7fffffffffffULL);
-
-	// Adjust exponent
-	exponent = exponent - 15360;
-	if (exponent > 2047) {
-		// Too big
-		if (sign)
-			value = -DBL_MAX;
-		else
-			value = DBL_MAX;
-	} else if (exponent > 0) {
-		// Normal number
-		uint64_t recombined = (static_cast<uint64_t>(sign) << 63) | (static_cast<uint64_t>(exponent) << 52) | (static_cast<uint64_t>(mantissa) << 5);
-		memcpy(&value, &recombined, 8);
+bool DataReader::readF80(double &value) {
+	uint16_t signAndExponent;
+	uint64_t mantissa;
+	if (isBigEndian()) {
+		if (!readU16(signAndExponent) || !readU64(mantissa))
+			return false;
 	} else {
-		// Subnormal number
-		mantissa |= 0x800000000000ULL;
-		mantissa <<= 5;
-		if (exponent <= -52) {
-			mantissa = 0;
-			exponent = 0;
-		} else {
-			mantissa >>= (-exponent);
-			exponent = 0;
-		}
+		if (!readU64(mantissa) || !readU16(signAndExponent))
+			return false;
+	}
 
-		uint64_t recombined = (static_cast<uint64_t>(sign) << 63) | (static_cast<uint64_t>(exponent) << 52) | (static_cast<uint64_t>(mantissa) << 5);
-		memcpy(&value, &recombined, 8);
+	uint8_t sign = (signAndExponent >> 15) & 1;
+	int16_t exponent = signAndExponent & 0x7fff;
+
+	// Eliminate implicit 1 and truncate from 63 to 47 bits
+	mantissa &= 0x7fffffffffffffffULL;
+	mantissa >>= 16;
+
+	if (mantissa != 0 || exponent != 0) {
+		// Adjust exponent
+		exponent = exponent - 15360;
+		if (exponent > 2046) {
+			// Too big, set to largest finite magnitude
+			exponent = 2046;
+			mantissa = 0xFFFFFFFFFFFFFUL;
+		} else if (exponent < 0) {
+			// Subnormal number
+			mantissa |= 0x1000000000000ULL;
+			if (exponent <= -48) {
+				mantissa = 0;
+				exponent = 0;
+			} else {
+				mantissa >>= (-exponent);
+				exponent = 0;
+			}
+		}
 	}
 
+	uint64_t recombined = (static_cast<uint64_t>(sign) << 63) | (static_cast<uint64_t>(exponent) << 52) | (static_cast<uint64_t>(mantissa) << 5);
+	memcpy(&value, &recombined, 8);
+
 	return true;
 }
 
-
 bool DataReader::read(void *dest, size_t size) {
 	while (size > 0) {
 		uint32 thisChunkSize = UINT32_MAX;
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index 8283bfdf494..8a01b5d8549 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -144,7 +144,8 @@ public:
 	bool readS64(int64 &value);
 	bool readF32(float &value);
 	bool readF64(double &value);
-	bool readTruncated8087XPDouble(double &value);
+	bool readF80(double &value);
+
 	bool read(void *dest, size_t size);
 
 	// Reads a terminated string where "length" is the number of characters including a null terminator
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 89e2fbf8231..dc9a036c994 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -130,7 +130,7 @@ bool MiniscriptInstructionLoader<MiniscriptInstructions::PushValue>::loadInstruc
 	else if (dataType == 0x15) {
 		double d;
 		if (instrDataReader.getProjectFormat() == Data::kProjectFormatMacintosh) {
-			if (!instrDataReader.readTruncated8087XPDouble(d))
+			if (!instrDataReader.readF80(d))
 				return false;
 		} else if (instrDataReader.getProjectFormat() == Data::kProjectFormatWindows) {
 			if (!instrDataReader.readF64(d))


Commit: a888c537fa763ba1d8494647ffc8bf0262cd59d0
    https://github.com/scummvm/scummvm/commit/a888c537fa763ba1d8494647ffc8bf0262cd59d0
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add keyboard messenger modifier loader and merge some messenger functionality

Changed paths:
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/modifier_factory.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h


diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index 34abc44dd52..34f6246c039 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -526,6 +526,24 @@ DataReadErrorCode IfMessengerModifier::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode KeyboardMessengerModifier::load(DataReader &reader) {
+	if (_revision != 0x3eb)
+		return kDataReadErrorReadFailed;
+
+	if (!modHeader.load(reader) || !reader.readU32(messageFlagsAndKeyStates) || !reader.readU16(unknown2)
+		|| !reader.readU16(keyModifiers) || !reader.readU8(keycode) || !reader.readBytes(unknown4)
+		|| !message.load(reader) || !reader.readU16(unknown7) || !reader.readU32(destination)
+		|| !reader.readBytes(unknown9) || !reader.readU16(with) || !reader.readBytes(unknown11)
+		|| !reader.readU32(withSourceGUID) || !reader.readBytes(unknown13) || !reader.readU8(withSourceLength)
+		|| !reader.readU8(unknown14))
+		return kDataReadErrorReadFailed;
+
+	if (withSourceLength > 0 && !reader.readNonTerminatedStr(withSource, withSourceLength))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode BooleanVariableModifier::load(DataReader &reader) {
 	if (_revision != 0x3e8)
 		return kDataReadErrorUnsupportedRevision;
@@ -658,6 +676,9 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 	case DataObjectTypes::kPlugInModifier:
 		dataObject = new PlugInModifier();
 		break;
+	case DataObjectTypes::kKeyboardMessengerModifier:
+		dataObject = new KeyboardMessengerModifier();
+		break;
 	default:
 		warning("Unrecognized data object type %x", static_cast<int>(type));
 		break;
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index 8a01b5d8549..c38432679ed 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -103,7 +103,7 @@ enum DataObjectType {
 	kSetModifier                         = 0x2df,	// NYI
 	kCollisionDetectionMessengerMOdifier = 0x2ee,	// NYI
 	kBoundaryDetectionMessengerModifier  = 0x2f8,	// NYI
-	kKeyboardMessengerModifier           = 0x302,	// NYI
+	kKeyboardMessengerModifier           = 0x302,
 	kTextStyleModifier                   = 0x32a,	// NYI
 	kGraphicModifier                     = 0x334,	// NYI
 	kImageEffectModifier                 = 0x384,	// NYI
@@ -456,6 +456,63 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct KeyboardMessengerModifier : public DataObject {
+	enum KeyStateFlags {
+		kOnDown = 0x10000000,
+		kOnUp = 0x4000000,
+		kOnRepeat = 0x8000000,
+
+		kKeyStateMask = (kOnDown | kOnUp | kOnRepeat),
+	};
+
+	enum KeyModifiers {
+		kControl = 0x1000,
+		kCommand = 0x0100,
+		kOption = 0x0800,
+	};
+
+	enum KeyCodes {
+		kAny = 0x00,
+		kHome = 0x01,
+		kEnter = 0x03,
+		kEnd = 0x04,
+		kHelp = 0x05,
+		kBackspace = 0x08,
+		kTab = 0x09,
+		kPageUp = 0x0b,
+		kPageDown = 0x0c,
+		kReturn = 0x0d,
+		kEscape = 0x1b,
+		kArrowLeft = 0x1c,
+		kArrowRight = 0x1d,
+		kArrowUp = 0x1e,
+		kArrowDown = 0x1f,
+		kDelete = 0x7f,
+	};
+
+	TypicalModifierHeader modHeader;
+	uint32 messageFlagsAndKeyStates;
+	uint16 unknown2;
+	uint16 keyModifiers;
+	uint8 keycode;
+	uint8 unknown4[7];
+	Event message;
+	uint16 unknown7;
+	uint32 destination;
+	uint8 unknown9[10];
+	uint16 with;
+	uint8 unknown11[4];
+	uint32 withSourceGUID;
+	uint8 unknown13[36];
+	uint8 withSourceLength;
+	uint8 unknown14;
+
+	Common::String withSource;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 struct BooleanVariableModifier final : public DataObject {
 	TypicalModifierHeader modHeader;
 	uint8_t value;
diff --git a/engines/mtropolis/modifier_factory.cpp b/engines/mtropolis/modifier_factory.cpp
index b79b1497c62..498b21ae80b 100644
--- a/engines/mtropolis/modifier_factory.cpp
+++ b/engines/mtropolis/modifier_factory.cpp
@@ -67,6 +67,8 @@ IModifierFactory *getModifierFactoryForDataObjectType(const Data::DataObjectType
 		return ModifierFactory<MiniscriptModifier, Data::MiniscriptModifier>::getInstance();
 	case Data::DataObjectTypes::kIfMessengerModifier:
 		return ModifierFactory<IfMessengerModifier, Data::IfMessengerModifier>::getInstance();
+	case Data::DataObjectTypes::kKeyboardMessengerModifier:
+		return ModifierFactory<KeyboardMessengerModifier, Data::KeyboardMessengerModifier>::getInstance();
 	case Data::DataObjectTypes::kMessengerModifier:
 		return ModifierFactory<MessengerModifier, Data::MessengerModifier>::getInstance();
 	case Data::DataObjectTypes::kBooleanVariableModifier:
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 3de435de6f2..48b7861fcca 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -27,14 +27,6 @@
 
 namespace MTropolis {
 
-static MessageFlags translateMessengerFlags(uint32 messengerFlags) {
-	MessageFlags messageFlags;
-	messageFlags.relay = ((messengerFlags & 0x20000000) == 0);
-	messageFlags.cascade = ((messengerFlags & 0x40000000) == 0);
-	messageFlags.immediate = ((messengerFlags & 0x80000000) == 0);
-	return messageFlags;
-}
-
 bool BehaviorModifier::load(ModifierLoaderContext &context, const Data::BehaviorModifier &data) {
 	if (data.numChildren > 0) {
 		ChildLoaderContext loaderContext;
@@ -75,17 +67,31 @@ bool MiniscriptModifier::load(ModifierLoaderContext &context, const Data::Minisc
 	return true;
 }
 
+MessengerSendSpec::MessengerSendSpec() : withType(kMessageWithNothing), withSourceGUID(0), destination(0) {
+}
+
+bool MessengerSendSpec::load(const Data::Event& dataEvent, uint32 dataMessageFlags, uint16 dataWith, uint32 dataWithSourceGUID, uint32 dataDestination) {
+	messageFlags.relay = ((dataMessageFlags & 0x20000000) == 0);
+	messageFlags.cascade = ((dataMessageFlags & 0x40000000) == 0);
+	messageFlags.immediate = ((dataMessageFlags & 0x80000000) == 0);
+
+	if (!this->send.load(dataEvent))
+		return false;
+
+	this->destination = dataDestination;
+	this->withSourceGUID = dataWithSourceGUID;
+	this->withType = static_cast<MessageWithType>(dataWith);
+
+	return true;
+}
+
 bool MessengerModifier::load(ModifierLoaderContext &context, const Data::MessengerModifier &data) {
 	_guid = data.modHeader.guid;
 	_name = data.modHeader.name;
 
-	if (!_when.load(data.when) || !_send.load(data.send))
+	if (!_when.load(data.when) || !_sendSpec.load(data.send, data.messageFlags, data.with, data.withSourceGUID, data.destination))
 		return false;
 
-	_messageFlags = translateMessengerFlags(data.messageFlags);
-	_messageWithType = static_cast<MessageWithType>(data.with);
-	_messageDestination = data.destination;
-
 	return true;
 }
 
@@ -93,13 +99,9 @@ bool IfMessengerModifier::load(ModifierLoaderContext &context, const Data::IfMes
 	_guid = data.modHeader.guid;
 	_name = data.modHeader.name;
 
-	if (!_when.load(data.when) || !_send.load(data.send))
+	if (!_when.load(data.when) || !_sendSpec.load(data.send, data.messageFlags, data.with, data.withSourceGUID, data.destination))
 		return false;
 
-	_messageFlags = translateMessengerFlags(data.messageFlags);
-	_messageWithType = static_cast<MessageWithType>(data.with);
-	_messageDestination = data.destination;
-
 	_program = MiniscriptParser::parse(data.program);
 	if (!_program)
 		return false;
@@ -107,6 +109,49 @@ bool IfMessengerModifier::load(ModifierLoaderContext &context, const Data::IfMes
 	return true;
 }
 
+bool KeyboardMessengerModifier::load(ModifierLoaderContext &context, const Data::KeyboardMessengerModifier &data) {
+	_guid = data.modHeader.guid;
+	_name = data.modHeader.name;
+
+	_onDown = ((data.messageFlagsAndKeyStates & Data::KeyboardMessengerModifier::kOnDown) != 0);
+	_onUp = ((data.messageFlagsAndKeyStates & Data::KeyboardMessengerModifier::kOnUp) != 0);
+	_onRepeat = ((data.messageFlagsAndKeyStates & Data::KeyboardMessengerModifier::kOnRepeat) != 0);
+	_keyModControl = ((data.keyModifiers & Data::KeyboardMessengerModifier::kControl) != 0);
+	_keyModCommand = ((data.keyModifiers & Data::KeyboardMessengerModifier::kCommand) != 0);
+	_keyModOption = ((data.keyModifiers & Data::KeyboardMessengerModifier::kOption) != 0);
+
+	switch (data.keycode) {
+	case KeyCodeType::kAny:
+	case KeyCodeType::kHome:
+	case KeyCodeType::kEnter:
+	case KeyCodeType::kEnd:
+	case KeyCodeType::kHelp:
+	case KeyCodeType::kBackspace:
+	case KeyCodeType::kTab:
+	case KeyCodeType::kPageUp:
+	case KeyCodeType::kPageDown:
+	case KeyCodeType::kReturn:
+	case KeyCodeType::kEscape:
+	case KeyCodeType::kArrowLeft:
+	case KeyCodeType::kArrowRight:
+	case KeyCodeType::kArrowUp:
+	case KeyCodeType::kArrowDown:
+	case KeyCodeType::kDelete:
+		_keyCodeType = static_cast<KeyCodeType>(data.keycode);
+		_macRomanChar = 0;
+		break;
+	default:
+		_keyCodeType = kMacRomanChar;
+		_macRomanChar = data.keycode;
+		break;
+	}
+
+	if (!_sendSpec.load(data.message, data.messageFlagsAndKeyStates, data.with, data.withSourceGUID, data.destination))
+		return false;
+
+	return true;
+}
+
 bool BooleanVariableModifier::load(ModifierLoaderContext &context, const Data::BooleanVariableModifier &data) {
 	_guid = data.modHeader.guid;
 	_name = data.modHeader.name;
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 68d00ad629d..4440eccdb60 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -53,17 +53,24 @@ private:
 	Common::SharedPtr<MiniscriptProgram> _program;
 };
 
+struct MessengerSendSpec {
+	MessengerSendSpec();
+	bool load(const Data::Event &dataEvent, uint32 dataMessageFlags, uint16 dataWith, uint32 dataWithSourceGUID, uint32 dataDestination);
+
+	Event send;
+	MessageFlags messageFlags;
+	MessageWithType withType;
+	uint32 withSourceGUID;
+	uint32 destination; // May be a MessageDestination or GUID
+};
+
 class MessengerModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::MessengerModifier &data);
 
 private:
 	Event _when;
-	Event _send;
-
-	MessageFlags _messageFlags;
-	MessageWithType _messageWithType;
-	uint32 _messageDestination;	// May be a MessageDestination or GUID
+	MessengerSendSpec _sendSpec;
 };
 
 class IfMessengerModifier : public Modifier {
@@ -72,14 +79,48 @@ public:
 
 private:
 	Event _when;
-	Event _send;
-
-	MessageFlags _messageFlags;
-	MessageWithType _messageWithType;
-	uint32 _messageDestination; // May be a MessageDestination or GUID
+	MessengerSendSpec _sendSpec;
 
 	Common::SharedPtr<MiniscriptProgram> _program;
 };
+class KeyboardMessengerModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::KeyboardMessengerModifier &data);
+
+private:
+	Event _send;
+
+	enum KeyCodeType {
+		kAny = 0x00,
+		kHome = 0x01,
+		kEnter = 0x03,
+		kEnd = 0x04,
+		kHelp = 0x05,
+		kBackspace = 0x08,
+		kTab = 0x09,
+		kPageUp = 0x0b,
+		kPageDown = 0x0c,
+		kReturn = 0x0d,
+		kEscape = 0x1b,
+		kArrowLeft = 0x1c,
+		kArrowRight = 0x1d,
+		kArrowUp = 0x1e,
+		kArrowDown = 0x1f,
+		kDelete = 0x7f,
+		kMacRomanChar = 0xff,
+	};
+
+	bool _onDown : 1;
+	bool _onUp : 1;
+	bool _onRepeat : 1;
+	bool _keyModControl : 1;
+	bool _keyModCommand : 1;
+	bool _keyModOption : 1;
+	KeyCodeType _keyCodeType;
+	uint8_t _macRomanChar;
+
+	MessengerSendSpec _sendSpec;
+};
 
 class BooleanVariableModifier : public Modifier {
 public:


Commit: 1cdd7682d28a0505bfa4f6776a4e94909c74d190
    https://github.com/scummvm/scummvm/commit/1cdd7682d28a0505bfa4f6776a4e94909c74d190
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Recognize modifier flags and editor layout position fields

Changed paths:
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index 34f6246c039..97ade7da870 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -413,7 +413,7 @@ DataReadErrorCode BehaviorModifier::load(DataReader& reader) {
 	if (_revision != 1)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!reader.readU32(unknown1) || !reader.readU32(sizeIncludingTag)
+	if (!reader.readU32(modifierFlags) || !reader.readU32(sizeIncludingTag)
 		|| !reader.readBytes(unknown2) || !reader.readU32(guid)
 		|| !reader.readU32(unknown4) || !reader.readU16(unknown5)
 		|| !reader.readU32(unknown6) || !editorLayoutPosition.load(reader)
@@ -470,7 +470,9 @@ bool MiniscriptProgram::load(DataReader &reader) {
 }
 
 bool TypicalModifierHeader::load(DataReader& reader) {
-	if (!reader.readU32(unknown1) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid) || !reader.readBytes(unknown3) || !reader.readU32(unknown4) || !reader.readBytes(unknown5) || !reader.readU16(lengthOfName))
+	if (!reader.readU32(modifierFlags) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid)
+		|| !reader.readBytes(unknown3) || !reader.readU32(unknown4) || !editorLayoutPosition.load(reader)
+		|| !reader.readU16(lengthOfName))
 		return false;
 
 	if (lengthOfName > 0 && !reader.readTerminatedStr(name, lengthOfName))
@@ -571,9 +573,9 @@ DataReadErrorCode PlugInModifier::load(DataReader &reader) {
 	if (_revision != 0x03e9)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!reader.readU32(unknown1) || !reader.readU32(codedSize) || !reader.read(modifierName, 16)
+	if (!reader.readU32(modifierFlags) || !reader.readU32(codedSize) || !reader.read(modifierName, 16)
 		|| !reader.readU32(guid) || !reader.readBytes(unknown2) || !reader.readU16(plugInRevision)
-		|| !reader.readU32(unknown4) || !reader.readBytes(unknown5) || !reader.readU16(lengthOfName))
+		|| !reader.readU32(unknown4) || !editorLayoutPosition.load(reader) || !reader.readU16(lengthOfName))
 		return kDataReadErrorReadFailed;
 
 	if (lengthOfName > 0 && !reader.readTerminatedStr(name, lengthOfName))
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index c38432679ed..35c6b37960b 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -29,6 +29,7 @@
 #include "common/ptr.h"
 #include "common/stream.h"
 
+// This contains defs related to parsing of mTropolis stored data into structured data objects.
 namespace MTropolis {
 
 namespace Data {
@@ -53,6 +54,11 @@ enum DataReadErrorCode {
 	kDataReadErrorPlugInNotFound,
 };
 
+enum ModifierFlags {
+	kModifierFlagLast = 0x2,
+};
+
+
 namespace DataObjectTypes {
 
 enum DataObjectType {
@@ -101,7 +107,7 @@ enum DataObjectType {
 	kBehaviorModifier                    = 0x2c6,
 	kMessengerModifier                   = 0x2da,
 	kSetModifier                         = 0x2df,	// NYI
-	kCollisionDetectionMessengerMOdifier = 0x2ee,	// NYI
+	kCollisionDetectionMessengerModifier = 0x2ee,	// NYI
 	kBoundaryDetectionMessengerModifier  = 0x2f8,	// NYI
 	kKeyboardMessengerModifier           = 0x302,
 	kTextStyleModifier                   = 0x32a,	// NYI
@@ -324,7 +330,7 @@ protected:
 
 struct BehaviorModifier : public DataObject {
 
-	uint32 unknown1;
+	uint32 modifierFlags;
 	uint32 sizeIncludingTag;
 	uint8 unknown2[2];
 	uint32 guid;
@@ -379,12 +385,12 @@ struct MiniscriptProgram {
 
 // Header used for most modifiers, but not all
 struct TypicalModifierHeader {
-	uint32 unknown1;
+	uint32 modifierFlags;
 	uint32 sizeIncludingTag;
 	uint32 guid;
 	uint8 unknown3[6];
 	uint32 unknown4;
-	uint8 unknown5[4];
+	Point editorLayoutPosition;
 	uint16 lengthOfName;
 
 	Common::String name;
@@ -538,14 +544,14 @@ struct PlugInModifierData {
 };
 
 struct PlugInModifier : public DataObject {
-	uint32 unknown1;
+	uint32 modifierFlags;
 	uint32 codedSize;	// Total size on Mac but (size + (name length * 255)) on Windows for some reason
 	char modifierName[17];
 	uint32 guid;
 	uint8 unknown2[6];
 	uint16 plugInRevision;
 	uint32 unknown4;
-	uint8 unknown5[4];
+	Point editorLayoutPosition;
 	uint16 lengthOfName;
 
 	Common::String name;
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 48b7861fcca..0861c1143cc 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -42,6 +42,7 @@ bool BehaviorModifier::load(ModifierLoaderContext &context, const Data::Behavior
 
 	_guid = data.guid;
 	_name = data.name;
+	_modifierFlags.load(data.modifierFlags);
 
 	return true;
 }
@@ -86,8 +87,8 @@ bool MessengerSendSpec::load(const Data::Event& dataEvent, uint32 dataMessageFla
 }
 
 bool MessengerModifier::load(ModifierLoaderContext &context, const Data::MessengerModifier &data) {
-	_guid = data.modHeader.guid;
-	_name = data.modHeader.name;
+	if (!loadTypicalHeader(data.modHeader))
+		return false;
 
 	if (!_when.load(data.when) || !_sendSpec.load(data.send, data.messageFlags, data.with, data.withSourceGUID, data.destination))
 		return false;
@@ -96,8 +97,8 @@ bool MessengerModifier::load(ModifierLoaderContext &context, const Data::Messeng
 }
 
 bool IfMessengerModifier::load(ModifierLoaderContext &context, const Data::IfMessengerModifier &data) {
-	_guid = data.modHeader.guid;
-	_name = data.modHeader.name;
+	if (!loadTypicalHeader(data.modHeader))
+		return false;
 
 	if (!_when.load(data.when) || !_sendSpec.load(data.send, data.messageFlags, data.with, data.withSourceGUID, data.destination))
 		return false;
@@ -110,8 +111,8 @@ bool IfMessengerModifier::load(ModifierLoaderContext &context, const Data::IfMes
 }
 
 bool KeyboardMessengerModifier::load(ModifierLoaderContext &context, const Data::KeyboardMessengerModifier &data) {
-	_guid = data.modHeader.guid;
-	_name = data.modHeader.name;
+	if (!loadTypicalHeader(data.modHeader))
+		return false;
 
 	_onDown = ((data.messageFlagsAndKeyStates & Data::KeyboardMessengerModifier::kOnDown) != 0);
 	_onUp = ((data.messageFlagsAndKeyStates & Data::KeyboardMessengerModifier::kOnUp) != 0);
@@ -153,8 +154,8 @@ bool KeyboardMessengerModifier::load(ModifierLoaderContext &context, const Data:
 }
 
 bool BooleanVariableModifier::load(ModifierLoaderContext &context, const Data::BooleanVariableModifier &data) {
-	_guid = data.modHeader.guid;
-	_name = data.modHeader.name;
+	if (!loadTypicalHeader(data.modHeader))
+		return false;
 
 	_value = (data.value != 0);
 
@@ -162,8 +163,8 @@ bool BooleanVariableModifier::load(ModifierLoaderContext &context, const Data::B
 }
 
 bool PointVariableModifier::load(ModifierLoaderContext &context, const Data::PointVariableModifier &data) {
-	_guid = data.modHeader.guid;
-	_name = data.modHeader.name;
+	if (!loadTypicalHeader(data.modHeader))
+		return false;
 
 	_value.x = data.value.x;
 	_value.y = data.value.y;
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index f4d1a1341c6..aa9d8d3f8ff 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -473,10 +473,25 @@ void Project::loadContextualObject(ChildLoaderStack &stack, const Data::DataObje
 	}
 }
 
+ModifierFlags::ModifierFlags() : isLastModifier(false) {
+}
+
+bool ModifierFlags::load(const uint32 dataModifierFlags) {
+	isLastModifier = ((dataModifierFlags & 0x2) != 0);
+	return true;
+}
+
 Modifier::Modifier() : _guid(0) {
 }
 
 Modifier::~Modifier() {
 }
 
+bool Modifier::loadTypicalHeader(const Data::TypicalModifierHeader& typicalHeader) {
+	if (!_modifierFlags.load(typicalHeader.modifierFlags))
+		return false;
+
+	return true;
+}
+
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 2d333b11367..e463bf8f927 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -317,6 +317,13 @@ class Subsection : public Structural {
 class Scene : public Structural {
 };
 
+struct ModifierFlags {
+	ModifierFlags();
+	bool load(const uint32 dataModifierFlags);
+
+	bool isLastModifier : 1;
+};
+
 class Modifier {
 public:
 	Modifier();
@@ -325,6 +332,9 @@ public:
 protected:
 	uint32 _guid;
 	Common::String _name;
+	ModifierFlags _modifierFlags;
+
+	bool loadTypicalHeader(const Data::TypicalModifierHeader &typicalHeader);
 };
 
 } // End of namespace MTropolis


Commit: a8fe7160444617d1b76778b8b121219666b0fa61
    https://github.com/scummvm/scummvm/commit/a8fe7160444617d1b76778b8b121219666b0fa61
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add graphic modifier loader

Changed paths:
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/modifier_factory.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index 97ade7da870..b04b58f3b88 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -236,6 +236,26 @@ bool Event::load(DataReader& reader) {
 	return reader.readU32(eventID) && reader.readU32(eventInfo);
 }
 
+
+bool ColorRGB16::load(DataReader& reader) {
+
+	if (reader.getProjectFormat() == kProjectFormatMacintosh)
+		return reader.readU16(red) && reader.readU16(green) && reader.readU16(blue); 
+	else if (reader.getProjectFormat() == kProjectFormatWindows) {
+		uint8 bgra[4];
+		if (!reader.readBytes(bgra))
+			return false;
+
+		red = bgra[2] * 0x101;
+		green = bgra[1] * 0x101;
+		blue = bgra[0] * 0x101;
+
+		return true;
+	}
+	else
+		return false;
+}
+
 DataObject::DataObject() : _type(DataObjectTypes::kUnknown), _revision(0) {
 }
 
@@ -512,7 +532,7 @@ DataReadErrorCode MessengerModifier::load(DataReader& reader) {
 
 DataReadErrorCode IfMessengerModifier::load(DataReader &reader) {
 	if (_revision != 0x3ea)
-		return kDataReadErrorReadFailed;
+		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader) || !reader.readU32(messageFlags) || !when.load(reader) || !send.load(reader) ||
 		!reader.readU16(unknown6) || !reader.readU32(destination) || !reader.readBytes(unknown7) || !reader.readU16(with)
@@ -530,7 +550,7 @@ DataReadErrorCode IfMessengerModifier::load(DataReader &reader) {
 
 DataReadErrorCode KeyboardMessengerModifier::load(DataReader &reader) {
 	if (_revision != 0x3eb)
-		return kDataReadErrorReadFailed;
+		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader) || !reader.readU32(messageFlagsAndKeyStates) || !reader.readU16(unknown2)
 		|| !reader.readU16(keyModifiers) || !reader.readU8(keycode) || !reader.readBytes(unknown4)
@@ -546,6 +566,46 @@ DataReadErrorCode KeyboardMessengerModifier::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+
+DataReadErrorCode GraphicModifier::load(DataReader &reader) {
+	if (_revision != 0x3e9)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!modHeader.load(reader) || !reader.readU16(unknown1) || !applyWhen.load(reader)
+		|| !removeWhen.load(reader) || !reader.readBytes(unknown2) || !reader.readU16(inkMode)
+		|| !reader.readU16(shape))
+		return kDataReadErrorReadFailed;
+
+	if (reader.getProjectFormat() == kProjectFormatMacintosh) {
+		haveMacPart = true;
+		if (!reader.readBytes(platform.mac.unknown4_1) || !backColor.load(reader) || !foreColor.load(reader)
+			|| !reader.readU16(borderSize) || !borderColor.load(reader) || !reader.readU16(shadowSize)
+			|| !shadowColor.load(reader) || !reader.readBytes(platform.mac.unknown4_2))
+			return kDataReadErrorReadFailed;
+	} else
+		haveMacPart = false;
+
+	if (reader.getProjectFormat() == kProjectFormatWindows) {
+		haveWinPart = true;
+		if (!reader.readBytes(platform.win.unknown5_1) || !backColor.load(reader) || !foreColor.load(reader)
+			|| !reader.readU16(borderSize) || !borderColor.load(reader) || !reader.readU16(shadowSize)
+			|| !shadowColor.load(reader) || !reader.readBytes(platform.win.unknown5_2))
+			return kDataReadErrorReadFailed;
+	} else
+		haveWinPart = false;
+
+	if (!reader.readU16(numPolygonPoints) || !reader.readBytes(unknown6))
+		return kDataReadErrorReadFailed;
+
+	polyPoints.resize(numPolygonPoints);
+	for (size_t i = 0; i < numPolygonPoints; i++) {
+		if (!polyPoints[i].load(reader))
+			return kDataReadErrorReadFailed;
+	}
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode BooleanVariableModifier::load(DataReader &reader) {
 	if (_revision != 0x3e8)
 		return kDataReadErrorUnsupportedRevision;
@@ -681,6 +741,9 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 	case DataObjectTypes::kKeyboardMessengerModifier:
 		dataObject = new KeyboardMessengerModifier();
 		break;
+	case DataObjectTypes::kGraphicModifier:
+		dataObject = new GraphicModifier();
+		break;
 	default:
 		warning("Unrecognized data object type %x", static_cast<int>(type));
 		break;
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index 35c6b37960b..2f88f1b571a 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -111,7 +111,7 @@ enum DataObjectType {
 	kBoundaryDetectionMessengerModifier  = 0x2f8,	// NYI
 	kKeyboardMessengerModifier           = 0x302,
 	kTextStyleModifier                   = 0x32a,	// NYI
-	kGraphicModifier                     = 0x334,	// NYI
+	kGraphicModifier                     = 0x334,
 	kImageEffectModifier                 = 0x384,	// NYI
 	kMiniscriptModifier                  = 0x3c0,
 	kCursorModifierV1                    = 0x3ca,	// NYI - Obsolete version
@@ -200,6 +200,14 @@ struct Event {
 	uint32 eventInfo;
 };
 
+struct ColorRGB16 {
+	bool load(DataReader &reader);
+
+	uint16 red;
+	uint16 green;
+	uint16 blue;
+};
+
 class DataObject : public Common::NonCopyable {
 
 public:
@@ -519,7 +527,52 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
-struct BooleanVariableModifier final : public DataObject {
+struct GraphicModifier : public DataObject {
+	TypicalModifierHeader modHeader;
+
+	uint16 unknown1;
+	Event applyWhen;
+	Event removeWhen;
+	uint8 unknown2[2];
+	uint16 inkMode;
+	uint16 shape;
+
+	struct MacPart {
+		uint8 unknown4_1[6];
+		uint8 unknown4_2[26];
+	};
+
+	struct WinPart {
+		uint8 unknown5_1[4];
+		uint8 unknown5_2[22];
+	};
+
+	union PlatformPart {
+		MacPart mac;
+		WinPart win;
+	};
+
+	bool haveMacPart;
+	bool haveWinPart;
+	PlatformPart platform;
+
+	ColorRGB16 foreColor;
+	ColorRGB16 backColor;
+	uint16 borderSize;
+	ColorRGB16 borderColor;
+	uint16 shadowSize;
+	ColorRGB16 shadowColor;
+
+	uint16 numPolygonPoints;
+	uint8 unknown6[8];
+
+	Common::Array<Point> polyPoints;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+struct BooleanVariableModifier : public DataObject {
 	TypicalModifierHeader modHeader;
 	uint8_t value;
 	uint8_t unknown5;
@@ -528,7 +581,7 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
-struct PointVariableModifier final : public DataObject {
+struct PointVariableModifier : public DataObject {
 	TypicalModifierHeader modHeader;
 
 	uint8_t unknown5[4];
diff --git a/engines/mtropolis/modifier_factory.cpp b/engines/mtropolis/modifier_factory.cpp
index 498b21ae80b..8751282e99b 100644
--- a/engines/mtropolis/modifier_factory.cpp
+++ b/engines/mtropolis/modifier_factory.cpp
@@ -69,6 +69,8 @@ IModifierFactory *getModifierFactoryForDataObjectType(const Data::DataObjectType
 		return ModifierFactory<IfMessengerModifier, Data::IfMessengerModifier>::getInstance();
 	case Data::DataObjectTypes::kKeyboardMessengerModifier:
 		return ModifierFactory<KeyboardMessengerModifier, Data::KeyboardMessengerModifier>::getInstance();
+	case Data::DataObjectTypes::kGraphicModifier:
+		return ModifierFactory<GraphicModifier, Data::GraphicModifier>::getInstance();
 	case Data::DataObjectTypes::kMessengerModifier:
 		return ModifierFactory<MessengerModifier, Data::MessengerModifier>::getInstance();
 	case Data::DataObjectTypes::kBooleanVariableModifier:
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 0861c1143cc..a3729cb3720 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -153,6 +153,28 @@ bool KeyboardMessengerModifier::load(ModifierLoaderContext &context, const Data:
 	return true;
 }
 
+bool GraphicModifier::load(ModifierLoaderContext& context, const Data::GraphicModifier& data) {
+	if (!loadTypicalHeader(data.modHeader) || !_applyWhen.load(data.applyWhen) || !_removeWhen.load(data.removeWhen)
+		|| !_foreColor.load(data.foreColor) || !_backColor.load(data.backColor)
+		|| !_borderColor.load(data.borderColor) || !_shadowColor.load(data.shadowColor))
+		return false;
+
+	// We need the poly points even if this isn't a poly shape since I think it's possible to change the shape type at runtime
+	_polyPoints.resize(data.polyPoints.size());
+	for (size_t i = 0; i < data.polyPoints.size(); i++) {
+		_polyPoints[i].x = data.polyPoints[i].x;
+		_polyPoints[i].y = data.polyPoints[i].y;
+	}
+
+	_inkMode = static_cast<InkMode>(data.inkMode);
+	_shape = static_cast<Shape>(data.shape);
+
+	_borderSize = data.borderSize;
+	_shadowSize = data.shadowSize;
+
+	return true;
+}
+
 bool BooleanVariableModifier::load(ModifierLoaderContext &context, const Data::BooleanVariableModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 4440eccdb60..3dda36721c2 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -83,6 +83,7 @@ private:
 
 	Common::SharedPtr<MiniscriptProgram> _program;
 };
+
 class KeyboardMessengerModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::KeyboardMessengerModifier &data);
@@ -122,6 +123,49 @@ private:
 	MessengerSendSpec _sendSpec;
 };
 
+class GraphicModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::GraphicModifier &data);
+
+	enum InkMode {
+		kInkModeCopy = 0x0,
+		kInkModeTransparent = 0x1,				// src*dest
+		kInkModeGhost = 0x3,					// (1-src)+dest
+		kInkModeReverseCopy = 0x4,				// 1-src
+		kInkModeReverseGhost = 0x7,				// src+dest
+		kInkModeReverseTransparent = 0x9,		// (1-src)*dest
+		kInkModeBlend = 0x20,					// (src*bgcolor)+(dest*(1-bgcolor)
+		kInkModeBackgroundTransparent = 0x24,	// BG color is transparent
+		kInkModeChameleonDark = 0x25,			// src+dest
+		kInkModeChameleonLight = 0x27,			// src*dest
+		kInkModeBackgroundMatte = 0x224,		// BG color is transparent and non-interactive
+		kInkModeInvisible = 0xffff,				// Not drawn, but interactive
+	};
+
+	enum Shape {
+		kShapeRect = 0x1,
+		kShapeRoundedRect = 0x2,
+		kShapeOval = 0x3,
+		kShapePolygon = 0x9,
+		kShapeStar = 0xb,	// 5-point star, horizontal arms are at (top+bottom*2)/3
+	};
+
+private:
+	Event _applyWhen;
+	Event _removeWhen;
+	InkMode _inkMode;
+	Shape _shape;
+
+	ColorRGB8 _foreColor;
+	ColorRGB8 _backColor;
+	uint16 _borderSize;
+	ColorRGB8 _borderColor;
+	uint16 _shadowSize;
+	ColorRGB8 _shadowColor;
+
+	Common::Array<Point16> _polyPoints;
+};
+
 class BooleanVariableModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::BooleanVariableModifier &data);
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index aa9d8d3f8ff..03f1ddb7a9f 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -30,6 +30,13 @@
 
 namespace MTropolis {
 
+bool ColorRGB8::load(const Data::ColorRGB16& color) {
+	this->r = (color.red * 510 + 1) / 131070;
+	this->g = (color.green * 510 + 1) / 131070;
+	this->b = (color.blue * 510 + 1) / 131070;
+	return true;
+}
+
 MessageFlags::MessageFlags() : relay(true), cascade(true), immediate(true) {
 }
 
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index e463bf8f927..86ef937f209 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -47,6 +47,14 @@ struct Point16 {
 	int16 y;
 };
 
+struct ColorRGB8 {
+	uint16 r;
+	uint16 g;
+	uint16 b;
+
+	bool load(const Data::ColorRGB16 &color);
+};
+
 struct MessageFlags {
 	bool relay : 1;
 	bool cascade : 1;


Commit: 9c0337294fe2db3fe927a986f4dd44c5d67d1f2a
    https://github.com/scummvm/scummvm/commit/9c0337294fe2db3fe927a986f4dd44c5d67d1f2a
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add text style modifier

Changed paths:
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/modifier_factory.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index b04b58f3b88..e2a33135e6a 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -566,6 +566,19 @@ DataReadErrorCode KeyboardMessengerModifier::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode TextStyleModifier::load(DataReader &reader) {
+	if (_revision != 0x3e8)
+		return kDataReadErrorUnsupportedRevision;
+	
+	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !reader.readU16(macFontID)
+		|| !reader.readU8(flags) || !reader.readU8(unknown2) || !reader.readU16(size)
+		|| !textColor.load(reader) || !backgroundColor.load(reader) || !reader.readU16(alignment)
+		|| !reader.readU16(unknown3) || !applyWhen.load(reader) || !removeWhen.load(reader)
+		|| !reader.readU16(lengthOfFontFamilyName) || !reader.readNonTerminatedStr(fontFamilyName, lengthOfFontFamilyName))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
 
 DataReadErrorCode GraphicModifier::load(DataReader &reader) {
 	if (_revision != 0x3e9)
@@ -741,6 +754,9 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 	case DataObjectTypes::kKeyboardMessengerModifier:
 		dataObject = new KeyboardMessengerModifier();
 		break;
+	case DataObjectTypes::kTextStyleModifier:
+		dataObject = new TextStyleModifier();
+		break;
 	case DataObjectTypes::kGraphicModifier:
 		dataObject = new GraphicModifier();
 		break;
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index 2f88f1b571a..16d4b1d4f89 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -110,7 +110,7 @@ enum DataObjectType {
 	kCollisionDetectionMessengerModifier = 0x2ee,	// NYI
 	kBoundaryDetectionMessengerModifier  = 0x2f8,	// NYI
 	kKeyboardMessengerModifier           = 0x302,
-	kTextStyleModifier                   = 0x32a,	// NYI
+	kTextStyleModifier                   = 0x32a,
 	kGraphicModifier                     = 0x334,
 	kImageEffectModifier                 = 0x384,	// NYI
 	kMiniscriptModifier                  = 0x3c0,
@@ -527,6 +527,29 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct TextStyleModifier : public DataObject {
+	TypicalModifierHeader modHeader;
+
+	TypicalModifierHeader m_modHeader;
+	uint8 unknown1[4];
+	uint16 macFontID;
+	uint8 flags;
+	uint8 unknown2;
+	uint16 size;
+	ColorRGB16 textColor;		// Appears to not actually be used
+	ColorRGB16 backgroundColor; // Appears to not actually be used
+	uint16 alignment;
+	uint16 unknown3;
+	Event applyWhen;
+	Event removeWhen;
+	uint16_t lengthOfFontFamilyName;
+
+	Common::String fontFamilyName;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 struct GraphicModifier : public DataObject {
 	TypicalModifierHeader modHeader;
 
diff --git a/engines/mtropolis/modifier_factory.cpp b/engines/mtropolis/modifier_factory.cpp
index 8751282e99b..469a6b330a5 100644
--- a/engines/mtropolis/modifier_factory.cpp
+++ b/engines/mtropolis/modifier_factory.cpp
@@ -69,6 +69,8 @@ IModifierFactory *getModifierFactoryForDataObjectType(const Data::DataObjectType
 		return ModifierFactory<IfMessengerModifier, Data::IfMessengerModifier>::getInstance();
 	case Data::DataObjectTypes::kKeyboardMessengerModifier:
 		return ModifierFactory<KeyboardMessengerModifier, Data::KeyboardMessengerModifier>::getInstance();
+	case Data::DataObjectTypes::kTextStyleModifier:
+		return ModifierFactory<TextStyleModifier, Data::TextStyleModifier>::getInstance();
 	case Data::DataObjectTypes::kGraphicModifier:
 		return ModifierFactory<GraphicModifier, Data::GraphicModifier>::getInstance();
 	case Data::DataObjectTypes::kMessengerModifier:
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index a3729cb3720..35767e07555 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -153,6 +153,35 @@ bool KeyboardMessengerModifier::load(ModifierLoaderContext &context, const Data:
 	return true;
 }
 
+TextStyleModifier::StyleFlags::StyleFlags() : bold(false), italic(false), underline(false), outline(false), shadow(false), condensed(false), expanded(false) {
+}
+
+bool TextStyleModifier::StyleFlags::load(uint8 dataStyleFlags) {
+	bold = ((dataStyleFlags & 0x01) != 0);
+	italic = ((dataStyleFlags & 0x02) != 0);
+	underline = ((dataStyleFlags & 0x03) != 0);
+	outline = ((dataStyleFlags & 0x04) != 0);
+	shadow = ((dataStyleFlags & 0x10) != 0);
+	condensed = ((dataStyleFlags & 0x20) != 0);
+	expanded = ((dataStyleFlags & 0x40) != 0);
+	return true;
+}
+
+bool TextStyleModifier::load(ModifierLoaderContext &context, const Data::TextStyleModifier &data) {
+	if (!loadTypicalHeader(data.modHeader))
+		return false;
+
+	if (!_textColor.load(data.textColor) || !_backgroundColor.load(data.backgroundColor) || !_applyWhen.load(data.applyWhen) || !_removeWhen.load(data.removeWhen))
+		return false;
+
+	_macFontID = data.macFontID;
+	_size = data.size;
+	_alignment = static_cast<Alignment>(data.alignment);
+	_fontFamilyName = data.fontFamilyName;
+
+	return true;
+}
+
 bool GraphicModifier::load(ModifierLoaderContext& context, const Data::GraphicModifier& data) {
 	if (!loadTypicalHeader(data.modHeader) || !_applyWhen.load(data.applyWhen) || !_removeWhen.load(data.removeWhen)
 		|| !_foreColor.load(data.foreColor) || !_backColor.load(data.backColor)
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 3dda36721c2..c5a7ef4a4d3 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -123,6 +123,39 @@ private:
 	MessengerSendSpec _sendSpec;
 };
 
+class TextStyleModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::TextStyleModifier &data);
+
+	enum Alignment {
+		kAlignmentLeft = 0,
+		kAlignmentCenter = 1,
+		kAlignmentRight = 0xffff,
+	};
+
+	struct StyleFlags {
+		bool bold : 1;
+		bool italic : 1;
+		bool underline : 1;
+		bool outline : 1;
+		bool shadow : 1;
+		bool condensed : 1;
+		bool expanded : 1;
+
+		StyleFlags();
+		bool load(uint8 dataStyleFlags);
+	};
+
+	uint16 _macFontID;
+	uint16 _size;
+	ColorRGB8 _textColor;
+	ColorRGB8 _backgroundColor;
+	Alignment _alignment;
+	Event _applyWhen;
+	Event _removeWhen;
+	Common::String _fontFamilyName;
+};
+
 class GraphicModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::GraphicModifier &data);
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 86ef937f209..70c7f00c242 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -48,9 +48,9 @@ struct Point16 {
 };
 
 struct ColorRGB8 {
-	uint16 r;
-	uint16 g;
-	uint16 b;
+	uint8 r;
+	uint8 g;
+	uint8 b;
 
 	bool load(const Data::ColorRGB16 &color);
 };


Commit: 3779d4acaef04306ca4645195d659267e5bf96d4
    https://github.com/scummvm/scummvm/commit/3779d4acaef04306ca4645195d659267e5bf96d4
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add set modifier loader

Changed paths:
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/modifier_factory.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/runtime.cpp


diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index e2a33135e6a..57e8fe87b71 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -511,7 +511,14 @@ DataReadErrorCode MiniscriptModifier::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
-DataReadErrorCode MessengerModifier::load(DataReader& reader) {
+bool MessageDataLocator::load(DataReader& reader) {
+	if (!reader.readU16(locationType) || !reader.readBytes(unknown1) || !reader.readU32(guid) || !reader.readBytes(unknown2))
+		return false;
+
+	return true;
+}
+
+DataReadErrorCode MessengerModifier::load(DataReader &reader) {
 	if (_revision != 0x3ea)
 		return kDataReadErrorUnsupportedRevision;
 
@@ -520,8 +527,7 @@ DataReadErrorCode MessengerModifier::load(DataReader& reader) {
 
 	// Unlike most cases, the "when" event is split in half in this case
 	if (!reader.readU32(messageFlags) || !reader.readU32(when.eventID) || !send.load(reader) || !reader.readU16(unknown14) || !reader.readU32(destination)
-		|| !reader.readBytes(unknown11) || !reader.readU16(with) || !reader.readBytes(unknown15) || !reader.readU32(withSourceGUID)
-		|| !reader.readBytes(unknown12) || !reader.readU32(when.eventInfo) || !reader.readU8(withSourceLength) || !reader.readU8(unknown13))
+		|| !reader.readBytes(unknown11) || !with.load(reader) || !reader.readU32(when.eventInfo) || !reader.readU8(withSourceLength) || !reader.readU8(unknown13))
 		return kDataReadErrorReadFailed;
 
 	if (withSourceLength > 0 && !reader.readNonTerminatedStr(withSourceName, withSourceLength))
@@ -530,6 +536,18 @@ DataReadErrorCode MessengerModifier::load(DataReader& reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode SetModifier::load(DataReader &reader) {
+	if (_revision != 0x3e8)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !executeWhen.load(reader) || !sourceLocator.load(reader) || !targetLocator.load(reader)
+		|| !reader.readU8(unknown3) || !reader.readU8(sourceNameLength) || !reader.readU8(targetNameLength) || !reader.readBytes(unknown4)
+		|| !reader.readNonTerminatedStr(sourceName, sourceNameLength) || !reader.readNonTerminatedStr(targetName, targetNameLength))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode IfMessengerModifier::load(DataReader &reader) {
 	if (_revision != 0x3ea)
 		return kDataReadErrorUnsupportedRevision;
@@ -736,6 +754,9 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 	case DataObjectTypes::kMessengerModifier:
 		dataObject = new MessengerModifier();
 		break;
+	case DataObjectTypes::kSetModifier:
+		dataObject = new SetModifier();
+		break;
 	case DataObjectTypes::kIfMessengerModifier:
 		dataObject = new IfMessengerModifier();
 		break;
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index 16d4b1d4f89..51e69fc2b1a 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -106,7 +106,7 @@ enum DataObjectType {
 	kIfMessengerModifier                 = 0x2bc,
 	kBehaviorModifier                    = 0x2c6,
 	kMessengerModifier                   = 0x2da,
-	kSetModifier                         = 0x2df,	// NYI
+	kSetModifier                         = 0x2df,
 	kCollisionDetectionMessengerModifier = 0x2ee,	// NYI
 	kBoundaryDetectionMessengerModifier  = 0x2f8,	// NYI
 	kKeyboardMessengerModifier           = 0x302,
@@ -425,6 +425,15 @@ enum MessageFlags {
 	kMessageFlagNoImmediate = 0x80000000,
 };
 
+struct MessageDataLocator {
+	uint16 locationType;
+	uint8 unknown1[4];
+	uint32 guid;
+	uint8 unknown2[36];
+
+	bool load(DataReader &reader);
+};
+
 struct MessengerModifier : public DataObject {
 	TypicalModifierHeader modHeader;
 
@@ -434,10 +443,7 @@ struct MessengerModifier : public DataObject {
 	uint16 unknown14;
 	uint32 destination;
 	uint8 unknown11[10];
-	uint16 with;
-	uint8 unknown15[4];
-	uint32 withSourceGUID;
-	uint8 unknown12[36];
+	MessageDataLocator with;
 	uint8 withSourceLength;
 	uint8 unknown13;
 
@@ -447,6 +453,25 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct SetModifier : public DataObject {
+	TypicalModifierHeader modHeader;
+
+	uint8 unknown1[4];
+	Event executeWhen;
+	MessageDataLocator sourceLocator;
+	MessageDataLocator targetLocator;
+	uint8 unknown3;
+	uint8 sourceNameLength;
+	uint8 targetNameLength;
+	uint8 unknown4[3];
+
+	Common::String sourceName;
+	Common::String targetName;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 struct IfMessengerModifier : public DataObject {
 	TypicalModifierHeader modHeader;
 
diff --git a/engines/mtropolis/modifier_factory.cpp b/engines/mtropolis/modifier_factory.cpp
index 469a6b330a5..df31ba634be 100644
--- a/engines/mtropolis/modifier_factory.cpp
+++ b/engines/mtropolis/modifier_factory.cpp
@@ -75,6 +75,8 @@ IModifierFactory *getModifierFactoryForDataObjectType(const Data::DataObjectType
 		return ModifierFactory<GraphicModifier, Data::GraphicModifier>::getInstance();
 	case Data::DataObjectTypes::kMessengerModifier:
 		return ModifierFactory<MessengerModifier, Data::MessengerModifier>::getInstance();
+	case Data::DataObjectTypes::kSetModifier:
+		return ModifierFactory<SetModifier, Data::SetModifier>::getInstance();
 	case Data::DataObjectTypes::kBooleanVariableModifier:
 		return ModifierFactory<BooleanVariableModifier, Data::BooleanVariableModifier>::getInstance();
 	case Data::DataObjectTypes::kPointVariableModifier:
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 35767e07555..d7d94534780 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -90,9 +90,28 @@ bool MessengerModifier::load(ModifierLoaderContext &context, const Data::Messeng
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
 
-	if (!_when.load(data.when) || !_sendSpec.load(data.send, data.messageFlags, data.with, data.withSourceGUID, data.destination))
+	if (!_when.load(data.when) || !_sendSpec.load(data.send, data.messageFlags, data.with.locationType, data.with.guid, data.destination))
+		return false;
+
+	return true;
+}
+
+bool SetModifier::load(ModifierLoaderContext &context, const Data::SetModifier &data) {
+	if (!loadTypicalHeader(data.modHeader))
+		return false;
+
+	if (!_executeWhen.load(data.executeWhen))
 		return false;
 
+	_sourceLocType = static_cast<MessageWithType>(data.sourceLocator.locationType);
+	_targetLocType = static_cast<MessageWithType>(data.targetLocator.locationType);
+
+	_sourceGUID = data.sourceLocator.guid;
+	_targetGUID = data.targetLocator.guid;
+
+	_sourceName = data.sourceName;
+	_targetName = data.targetName;
+
 	return true;
 }
 
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index c5a7ef4a4d3..809fae818b9 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -73,6 +73,20 @@ private:
 	MessengerSendSpec _sendSpec;
 };
 
+class SetModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::SetModifier &data);
+
+private:
+	Event _executeWhen;
+	MessageWithType _sourceLocType;
+	MessageWithType _targetLocType;
+	uint32 _sourceGUID;
+	uint32 _targetGUID;
+	Common::String _sourceName;
+	Common::String _targetName;
+};
+
 class IfMessengerModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::IfMessengerModifier &data);
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 03f1ddb7a9f..a46bc863eda 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -339,8 +339,6 @@ void Project::loadBootStream(size_t streamIndex) {
 		Common::SharedPtr<Data::DataObject> dataObject;
 		Data::loadDataObject(plugInDataLoaderRegistry, reader, dataObject);
 
-		numObjectsLoaded++;
-
 		if (!dataObject) {
 			error("Failed to load project boot data");
 		}
@@ -367,6 +365,8 @@ void Project::loadBootStream(size_t streamIndex) {
 				error("Unexpected object type in boot stream");
 			}
 		}
+
+		numObjectsLoaded++;
 	}
 }
 


Commit: 4bcdf6785fe6a65d5bb4d0cd93816554b05744d3
    https://github.com/scummvm/scummvm/commit/4bcdf6785fe6a65d5bb4d0cd93816554b05744d3
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Move plug-ins to new dir, add timer messenger and collision detection messenger modifiers

Changed paths:
  A engines/mtropolis/plugin/obsidian.cpp
  A engines/mtropolis/plugin/obsidian.h
  A engines/mtropolis/plugin/obsidian_data.cpp
  A engines/mtropolis/plugin/obsidian_data.h
  A engines/mtropolis/plugin/standard.cpp
  A engines/mtropolis/plugin/standard.h
  A engines/mtropolis/plugin/standard_data.cpp
  A engines/mtropolis/plugin/standard_data.h
  R engines/mtropolis/plugin_obsidian.cpp
  R engines/mtropolis/plugin_obsidian.h
  R engines/mtropolis/plugin_obsidian_data.cpp
  R engines/mtropolis/plugin_obsidian_data.h
  R engines/mtropolis/plugin_standard.cpp
  R engines/mtropolis/plugin_standard.h
  R engines/mtropolis/plugin_standard_data.cpp
  R engines/mtropolis/plugin_standard_data.h
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/modifier_factory.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/module.mk


diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index 57e8fe87b71..6176fd0fd72 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -96,21 +96,21 @@ bool DataReader::readF80(double &value) {
 	uint8_t sign = (signAndExponent >> 15) & 1;
 	int16_t exponent = signAndExponent & 0x7fff;
 
-	// Eliminate implicit 1 and truncate from 63 to 47 bits
-	mantissa &= 0x7fffffffffffffffULL;
-	mantissa >>= 16;
+	// Eliminate implicit 1 and truncate from 63 to 52 bits
+	mantissa &= (((static_cast<uint64_t>(1) << 52) - 1u) << 11);
+	mantissa >>= 11;
 
 	if (mantissa != 0 || exponent != 0) {
 		// Adjust exponent
-		exponent = exponent - 15360;
+		exponent -= 15360;
 		if (exponent > 2046) {
 			// Too big, set to largest finite magnitude
 			exponent = 2046;
-			mantissa = 0xFFFFFFFFFFFFFUL;
+			mantissa = (static_cast<uint64_t>(1) << 52) - 1u;
 		} else if (exponent < 0) {
 			// Subnormal number
-			mantissa |= 0x1000000000000ULL;
-			if (exponent <= -48) {
+			mantissa |= (static_cast<uint64_t>(1) << 52);
+			if (exponent < -52) {
 				mantissa = 0;
 				exponent = 0;
 			} else {
@@ -120,7 +120,7 @@ bool DataReader::readF80(double &value) {
 		}
 	}
 
-	uint64_t recombined = (static_cast<uint64_t>(sign) << 63) | (static_cast<uint64_t>(exponent) << 52) | (static_cast<uint64_t>(mantissa) << 5);
+	uint64_t recombined = (static_cast<uint64_t>(sign) << 63) | (static_cast<uint64_t>(exponent) << 52) | static_cast<uint64_t>(mantissa);
 	memcpy(&value, &recombined, 8);
 
 	return true;
@@ -566,6 +566,40 @@ DataReadErrorCode IfMessengerModifier::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode TimerMessengerModifier::load(DataReader &reader) {
+	if (_revision != 0x3ea)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!modHeader.load(reader))
+		return kDataReadErrorReadFailed;
+
+	if (!reader.readU32(messageAndTimerFlags) || !executeWhen.load(reader) || !send.load(reader)
+		|| !terminateWhen.load(reader) || !reader.readU16(unknown2) || !reader.readU32(destination)
+		|| !reader.readBytes(unknown4) || !with.load(reader) || !reader.readU8(unknown5)
+		|| !reader.readU8(minutes) || !reader.readU8(seconds) || !reader.readU8(hundredthsOfSeconds)
+		|| !reader.readU32(unknown6) || !reader.readU32(unknown7) || !reader.readBytes(unknown8)
+		|| !reader.readU8(withSourceLength) || !reader.readU8(unknown9) || !reader.readNonTerminatedStr(withSource, withSourceLength))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode CollisionDetectionMessengerModifier::load(DataReader &reader) {
+	if (_revision != 0x3ea)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!modHeader.load(reader))
+		return kDataReadErrorReadFailed;
+
+	if (!reader.readU32(messageAndModifierFlags) || !enableWhen.load(reader) || !disableWhen.load(reader)
+		|| !send.load(reader) || !reader.readU16(unknown2) || !reader.readU32(destination)
+		|| !reader.readBytes(unknown3) || !with.load(reader) || !reader.readU8(withSourceLength)
+		|| !reader.readU8(unknown4) || !reader.readNonTerminatedStr(withSource, withSourceLength))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode KeyboardMessengerModifier::load(DataReader &reader) {
 	if (_revision != 0x3eb)
 		return kDataReadErrorUnsupportedRevision;
@@ -772,6 +806,12 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 	case DataObjectTypes::kPlugInModifier:
 		dataObject = new PlugInModifier();
 		break;
+	case DataObjectTypes::kTimerMessengerModifier:
+		dataObject = new TimerMessengerModifier();
+		break;
+	case DataObjectTypes::kCollisionDetectionMessengerModifier:
+		dataObject = new CollisionDetectionMessengerModifier();
+		break;
 	case DataObjectTypes::kKeyboardMessengerModifier:
 		dataObject = new KeyboardMessengerModifier();
 		break;
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index 51e69fc2b1a..a33a3428e2b 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -107,7 +107,8 @@ enum DataObjectType {
 	kBehaviorModifier                    = 0x2c6,
 	kMessengerModifier                   = 0x2da,
 	kSetModifier                         = 0x2df,
-	kCollisionDetectionMessengerModifier = 0x2ee,	// NYI
+	kTimerMessengerModifier              = 0x2e4,
+	kCollisionDetectionMessengerModifier = 0x2ee,
 	kBoundaryDetectionMessengerModifier  = 0x2f8,	// NYI
 	kKeyboardMessengerModifier           = 0x302,
 	kTextStyleModifier                   = 0x32a,
@@ -495,6 +496,71 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct TimerMessengerModifier : public DataObject {
+	TypicalModifierHeader modHeader;
+
+	enum TimerFlags {
+		kTimerFlagLooping = 0x10000000,
+	};
+
+	uint32 messageAndTimerFlags;
+	Event executeWhen;
+	Event send;
+	Event terminateWhen;
+	uint16 unknown2;
+	uint32 destination;
+	uint8 unknown4[10];
+	MessageDataLocator with;
+	uint8 unknown5;
+	uint8 minutes;
+	uint8 seconds;
+	uint8 hundredthsOfSeconds;
+	uint32 unknown6;
+	uint32 unknown7;
+	uint8 unknown8[10];
+	uint8 withSourceLength;
+	uint8 unknown9;
+
+	Common::String withSource;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+struct CollisionDetectionMessengerModifier : public DataObject {
+	TypicalModifierHeader modHeader;
+
+	enum ModifierFlags {
+		kDetectLayerInFront = 0x10000000,
+		kDetectLayerBehind = 0x08000000,
+		kSendToCollidingElement = 0x02000000,
+		kSendToOnlyFirstCollidingElement = 0x00200000,
+
+		kDetectionModeMask = 0x01c00000,
+		kDetectionModeFirstContact = 0x01400000,
+		kDetectionModeWhileInContact = 0x01000000,
+		kDetectionModeExiting = 0x00800000,
+
+		kNoCollideWithParent = 0x00100000,
+	};
+
+	uint32 messageAndModifierFlags;
+	Event enableWhen;
+	Event disableWhen;
+	Event send;
+	uint16 unknown2;
+	uint32 destination;
+	uint8 unknown3[10];
+	MessageDataLocator with;
+	uint8 withSourceLength;
+	uint8 unknown4;
+
+	Common::String withSource;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 struct KeyboardMessengerModifier : public DataObject {
 	enum KeyStateFlags {
 		kOnDown = 0x10000000,
diff --git a/engines/mtropolis/modifier_factory.cpp b/engines/mtropolis/modifier_factory.cpp
index df31ba634be..6600962f3c8 100644
--- a/engines/mtropolis/modifier_factory.cpp
+++ b/engines/mtropolis/modifier_factory.cpp
@@ -67,6 +67,10 @@ IModifierFactory *getModifierFactoryForDataObjectType(const Data::DataObjectType
 		return ModifierFactory<MiniscriptModifier, Data::MiniscriptModifier>::getInstance();
 	case Data::DataObjectTypes::kIfMessengerModifier:
 		return ModifierFactory<IfMessengerModifier, Data::IfMessengerModifier>::getInstance();
+	case Data::DataObjectTypes::kTimerMessengerModifier:
+		return ModifierFactory<TimerMessengerModifier, Data::TimerMessengerModifier>::getInstance();
+	case Data::DataObjectTypes::kCollisionDetectionMessengerModifier:
+		return ModifierFactory<CollisionDetectionMessengerModifier, Data::CollisionDetectionMessengerModifier>::getInstance();
 	case Data::DataObjectTypes::kKeyboardMessengerModifier:
 		return ModifierFactory<KeyboardMessengerModifier, Data::KeyboardMessengerModifier>::getInstance();
 	case Data::DataObjectTypes::kTextStyleModifier:
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index d7d94534780..7c60f84f0a9 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -71,7 +71,7 @@ bool MiniscriptModifier::load(ModifierLoaderContext &context, const Data::Minisc
 MessengerSendSpec::MessengerSendSpec() : withType(kMessageWithNothing), withSourceGUID(0), destination(0) {
 }
 
-bool MessengerSendSpec::load(const Data::Event& dataEvent, uint32 dataMessageFlags, uint16 dataWith, uint32 dataWithSourceGUID, uint32 dataDestination) {
+bool MessengerSendSpec::load(const Data::Event& dataEvent, uint32 dataMessageFlags, uint16 dataWith, const Common::String &dataWithSourceName, uint32 dataWithSourceGUID, uint32 dataDestination) {
 	messageFlags.relay = ((dataMessageFlags & 0x20000000) == 0);
 	messageFlags.cascade = ((dataMessageFlags & 0x40000000) == 0);
 	messageFlags.immediate = ((dataMessageFlags & 0x80000000) == 0);
@@ -82,6 +82,7 @@ bool MessengerSendSpec::load(const Data::Event& dataEvent, uint32 dataMessageFla
 	this->destination = dataDestination;
 	this->withSourceGUID = dataWithSourceGUID;
 	this->withType = static_cast<MessageWithType>(dataWith);
+	this->withSourceName = dataWithSourceName;
 
 	return true;
 }
@@ -90,7 +91,7 @@ bool MessengerModifier::load(ModifierLoaderContext &context, const Data::Messeng
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
 
-	if (!_when.load(data.when) || !_sendSpec.load(data.send, data.messageFlags, data.with.locationType, data.with.guid, data.destination))
+	if (!_when.load(data.when) || !_sendSpec.load(data.send, data.messageFlags, data.with.locationType, data.withSourceName, data.with.guid, data.destination))
 		return false;
 
 	return true;
@@ -119,7 +120,7 @@ bool IfMessengerModifier::load(ModifierLoaderContext &context, const Data::IfMes
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
 
-	if (!_when.load(data.when) || !_sendSpec.load(data.send, data.messageFlags, data.with, data.withSourceGUID, data.destination))
+	if (!_when.load(data.when) || !_sendSpec.load(data.send, data.messageFlags, data.with, data.withSource, data.withSourceGUID, data.destination))
 		return false;
 
 	_program = MiniscriptParser::parse(data.program);
@@ -129,6 +130,56 @@ bool IfMessengerModifier::load(ModifierLoaderContext &context, const Data::IfMes
 	return true;
 }
 
+bool TimerMessengerModifier::load(ModifierLoaderContext &context, const Data::TimerMessengerModifier &data) {
+	if (!loadTypicalHeader(data.modHeader))
+		return false;
+
+	if (!_executeWhen.load(data.executeWhen) || !this->_terminateWhen.load(data.terminateWhen))
+		return false;
+
+	if (!_sendSpec.load(data.send, data.messageAndTimerFlags, data.with.locationType, data.withSource, data.with.guid, data.destination))
+		return false;
+
+	_milliseconds = data.minutes * (60 * 1000) + data.seconds * (1000) + data.hundredthsOfSeconds * 10;
+	_looping = ((data.messageAndTimerFlags & Data::TimerMessengerModifier::kTimerFlagLooping) != 0);
+
+	return true;
+}
+
+bool CollisionDetectionMessengerModifier::load(ModifierLoaderContext &context, const Data::CollisionDetectionMessengerModifier &data) {
+
+	if (!loadTypicalHeader(data.modHeader))
+		return false;
+
+	if (!_enableWhen.load(data.enableWhen) || !this->_disableWhen.load(data.disableWhen))
+		return false;
+
+	if (!_sendSpec.load(data.send, data.messageAndModifierFlags, data.with.locationType, data.withSource, data.with.guid, data.destination))
+		return false;
+
+	_detectInFront = ((data.messageAndModifierFlags & Data::CollisionDetectionMessengerModifier::kDetectLayerInFront) != 0);
+	_detectBehind = ((data.messageAndModifierFlags & Data::CollisionDetectionMessengerModifier::kDetectLayerBehind) != 0);
+	_ignoreParent = ((data.messageAndModifierFlags & Data::CollisionDetectionMessengerModifier::kNoCollideWithParent) != 0);
+	_sendToCollidingElement = ((data.messageAndModifierFlags & Data::CollisionDetectionMessengerModifier::kSendToCollidingElement) != 0);
+	_sendToOnlyFirstCollidingElement = ((data.messageAndModifierFlags & Data::CollisionDetectionMessengerModifier::kSendToOnlyFirstCollidingElement) != 0);
+
+	switch (data.messageAndModifierFlags & Data::CollisionDetectionMessengerModifier::kDetectionModeMask) {
+	case Data::CollisionDetectionMessengerModifier::kDetectionModeFirstContact:
+		_detectionMode = kDetectionModeFirstContact;
+		break;
+	case Data::CollisionDetectionMessengerModifier::kDetectionModeWhileInContact:
+		_detectionMode = kDetectionModeWhileInContact;
+		break;
+	case Data::CollisionDetectionMessengerModifier::kDetectionModeExiting:
+		_detectionMode = kDetectionModeExiting;
+		break;
+	default:
+		return false;	// Unknown flag combination
+	}
+
+	return true;
+}
+
 bool KeyboardMessengerModifier::load(ModifierLoaderContext &context, const Data::KeyboardMessengerModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -166,7 +217,7 @@ bool KeyboardMessengerModifier::load(ModifierLoaderContext &context, const Data:
 		break;
 	}
 
-	if (!_sendSpec.load(data.message, data.messageFlagsAndKeyStates, data.with, data.withSourceGUID, data.destination))
+	if (!_sendSpec.load(data.message, data.messageFlagsAndKeyStates, data.with, data.withSource, data.withSourceGUID, data.destination))
 		return false;
 
 	return true;
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 809fae818b9..6cfe3bd7dc2 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -55,12 +55,13 @@ private:
 
 struct MessengerSendSpec {
 	MessengerSendSpec();
-	bool load(const Data::Event &dataEvent, uint32 dataMessageFlags, uint16 dataWith, uint32 dataWithSourceGUID, uint32 dataDestination);
+	bool load(const Data::Event &dataEvent, uint32 dataMessageFlags, uint16 dataWith, const Common::String &dataWithSourceName, uint32 dataWithSourceGUID, uint32 dataDestination);
 
 	Event send;
 	MessageFlags messageFlags;
 	MessageWithType withType;
 	uint32 withSourceGUID;
+	Common::String withSourceName;
 	uint32 destination; // May be a MessageDestination or GUID
 };
 
@@ -98,6 +99,41 @@ private:
 	Common::SharedPtr<MiniscriptProgram> _program;
 };
 
+class TimerMessengerModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::TimerMessengerModifier &data);
+
+private:
+	Event _executeWhen;
+	Event _terminateWhen;
+	MessengerSendSpec _sendSpec;
+	uint32 _milliseconds;
+	bool _looping;
+};
+
+class CollisionDetectionMessengerModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::CollisionDetectionMessengerModifier &data);
+
+private:
+	enum DetectionMode {
+		kDetectionModeFirstContact,
+		kDetectionModeWhileInContact,
+		kDetectionModeExiting,
+	};
+
+	Event _enableWhen;
+	Event _disableWhen;
+	MessengerSendSpec _sendSpec;
+
+	DetectionMode _detectionMode;
+	bool _detectInFront;
+	bool _detectBehind;
+	bool _ignoreParent;
+	bool _sendToCollidingElement; // ... instead of to send spec destination, but send spec with/flags still apply!
+	bool _sendToOnlyFirstCollidingElement;
+};
+
 class KeyboardMessengerModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::KeyboardMessengerModifier &data);
diff --git a/engines/mtropolis/module.mk b/engines/mtropolis/module.mk
index 873333d02f6..e44198f1243 100644
--- a/engines/mtropolis/module.mk
+++ b/engines/mtropolis/module.mk
@@ -9,10 +9,10 @@ MODULE_OBJS = \
 	modifiers.o \
 	modifier_factory.o \
 	mtropolis.o \
-	plugin_obsidian.o \
-	plugin_obsidian_data.o \
-	plugin_standard.o \
-	plugin_standard_data.o \
+	plugin/obsidian.o \
+	plugin/obsidian_data.o \
+	plugin/standard.o \
+	plugin/standard_data.o \
 	runtime.o \
 	vthread.o
 
diff --git a/engines/mtropolis/plugin_obsidian.cpp b/engines/mtropolis/plugin/obsidian.cpp
similarity index 97%
rename from engines/mtropolis/plugin_obsidian.cpp
rename to engines/mtropolis/plugin/obsidian.cpp
index 76a15a02ae0..a6220328bb0 100644
--- a/engines/mtropolis/plugin_obsidian.cpp
+++ b/engines/mtropolis/plugin/obsidian.cpp
@@ -19,7 +19,7 @@
  *
  */
 
-#include "mtropolis/plugin_obsidian.h"
+#include "mtropolis/plugin/obsidian.h"
 #include "mtropolis/plugins.h"
 
 namespace MTropolis {
diff --git a/engines/mtropolis/plugin_obsidian.h b/engines/mtropolis/plugin/obsidian.h
similarity index 100%
rename from engines/mtropolis/plugin_obsidian.h
rename to engines/mtropolis/plugin/obsidian.h
diff --git a/engines/mtropolis/plugin_obsidian_data.cpp b/engines/mtropolis/plugin/obsidian_data.cpp
similarity index 95%
rename from engines/mtropolis/plugin_obsidian_data.cpp
rename to engines/mtropolis/plugin/obsidian_data.cpp
index 2983fe838b4..85ac698db4c 100644
--- a/engines/mtropolis/plugin_obsidian_data.cpp
+++ b/engines/mtropolis/plugin/obsidian_data.cpp
@@ -19,7 +19,7 @@
  *
  */
 
-#include "mtropolis/plugin_obsidian_data.h"
+#include "mtropolis/plugin/obsidian_data.h"
 
 namespace MTropolis {
 
diff --git a/engines/mtropolis/plugin_obsidian_data.h b/engines/mtropolis/plugin/obsidian_data.h
similarity index 100%
rename from engines/mtropolis/plugin_obsidian_data.h
rename to engines/mtropolis/plugin/obsidian_data.h
diff --git a/engines/mtropolis/plugin_standard.cpp b/engines/mtropolis/plugin/standard.cpp
similarity index 98%
rename from engines/mtropolis/plugin_standard.cpp
rename to engines/mtropolis/plugin/standard.cpp
index 8fe4adc57e1..593d0b7250c 100644
--- a/engines/mtropolis/plugin_standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -19,7 +19,7 @@
  *
  */
 
-#include "mtropolis/plugin_standard.h"
+#include "mtropolis/plugin/standard.h"
 #include "mtropolis/plugins.h"
 
 
diff --git a/engines/mtropolis/plugin_standard.h b/engines/mtropolis/plugin/standard.h
similarity index 97%
rename from engines/mtropolis/plugin_standard.h
rename to engines/mtropolis/plugin/standard.h
index ab1d9ade6e0..b7a93cec287 100644
--- a/engines/mtropolis/plugin_standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -24,7 +24,7 @@
 
 #include "mtropolis/modifier_factory.h"
 #include "mtropolis/runtime.h"
-#include "mtropolis/plugin_standard_data.h"
+#include "mtropolis/plugin/standard_data.h"
 
 namespace MTropolis {
 
diff --git a/engines/mtropolis/plugin_standard_data.cpp b/engines/mtropolis/plugin/standard_data.cpp
similarity index 98%
rename from engines/mtropolis/plugin_standard_data.cpp
rename to engines/mtropolis/plugin/standard_data.cpp
index 56b299c2494..5e6364e2b7e 100644
--- a/engines/mtropolis/plugin_standard_data.cpp
+++ b/engines/mtropolis/plugin/standard_data.cpp
@@ -19,7 +19,7 @@
  *
  */
 
-#include "mtropolis/plugin_standard_data.h"
+#include "mtropolis/plugin/standard_data.h"
 
 namespace MTropolis {
 
diff --git a/engines/mtropolis/plugin_standard_data.h b/engines/mtropolis/plugin/standard_data.h
similarity index 100%
rename from engines/mtropolis/plugin_standard_data.h
rename to engines/mtropolis/plugin/standard_data.h


Commit: 5303419e1a3ee72b8a50f6d3d8b6b6dc9bc97e31
    https://github.com/scummvm/scummvm/commit/5303419e1a3ee72b8a50f6d3d8b6b6dc9bc97e31
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add drag motion modifier loader

Changed paths:
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/modifier_factory.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index 6176fd0fd72..273d0805d61 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -548,6 +548,39 @@ DataReadErrorCode SetModifier::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode DragMotionModifier::load(DataReader &reader) {
+	if (_revision != 0x3e8)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!modHeader.load(reader))
+		return kDataReadErrorReadFailed;
+
+	if (!enableWhen.load(reader) || !disableWhen.load(reader))
+		return kDataReadErrorReadFailed;
+
+	if (reader.getProjectFormat() == kProjectFormatMacintosh) {
+		if (!reader.readU8(platform.mac.flags) || !reader.readU8(platform.mac.unknown3))
+			return kDataReadErrorReadFailed;
+
+		haveMacPart = true;
+	} else
+		haveMacPart = false;
+
+	if (reader.getProjectFormat() == kProjectFormatWindows) {
+		if (!reader.readU8(platform.win.unknown2) || !reader.readU8(platform.win.constrainHorizontal)
+			|| !reader.readU8(platform.win.constrainVertical) || !reader.readU8(platform.win.constrainToParent))
+			return kDataReadErrorReadFailed;
+
+		haveWinPart = true;
+	} else
+		haveWinPart = false;
+
+	if (!constraintMargin.load(reader) || !reader.readU16(unknown1))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode IfMessengerModifier::load(DataReader &reader) {
 	if (_revision != 0x3ea)
 		return kDataReadErrorUnsupportedRevision;
@@ -791,6 +824,9 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 	case DataObjectTypes::kSetModifier:
 		dataObject = new SetModifier();
 		break;
+	case DataObjectTypes::kDragMotionModifier:
+		dataObject = new DragMotionModifier();
+		break;
 	case DataObjectTypes::kIfMessengerModifier:
 		dataObject = new IfMessengerModifier();
 		break;
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index a33a3428e2b..faff4846599 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -96,7 +96,7 @@ enum DataObjectType {
 	kSoundEffectModifier                 = 0x1a4,	// NYI
 	kChangeSceneModifier                 = 0x136,	// NYI
 	kReturnModifier                      = 0x140,	// NYI
-	kDragMotionModifier                  = 0x208,	// NYI
+	kDragMotionModifier                  = 0x208,
 	kVectorMotionModifier                = 0x226,	// NYI
 	kPathMotionModifierV1                = 0x21c,	// NYI - Obsolete version
 	kPathMotionModifierV2                = 0x21b,	// NYI
@@ -473,6 +473,46 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct DragMotionModifier : public DataObject {
+	TypicalModifierHeader modHeader;
+
+	Event enableWhen;
+	Event disableWhen;
+
+	struct WinPart {
+		uint8_t unknown2;
+		uint8_t constrainHorizontal;
+		uint8_t constrainVertical;
+		uint8_t constrainToParent;
+	};
+
+	struct MacPart {
+		uint8_t flags;
+		uint8_t unknown3;
+
+		enum Flags {
+			kConstrainToParent = 0x10,
+			kConstrainVertical = 0x20,
+			kConstrainHorizontal = 0x40,
+		};
+	};
+
+	union PlatformPart {
+		WinPart win;
+		MacPart mac;
+	};
+
+	PlatformPart platform;
+
+	bool haveMacPart;
+	bool haveWinPart;
+	Rect constraintMargin;
+	uint16_t unknown1;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 struct IfMessengerModifier : public DataObject {
 	TypicalModifierHeader modHeader;
 
diff --git a/engines/mtropolis/modifier_factory.cpp b/engines/mtropolis/modifier_factory.cpp
index 6600962f3c8..edc68bb9cb8 100644
--- a/engines/mtropolis/modifier_factory.cpp
+++ b/engines/mtropolis/modifier_factory.cpp
@@ -65,6 +65,8 @@ IModifierFactory *getModifierFactoryForDataObjectType(const Data::DataObjectType
 		return ModifierFactory<BehaviorModifier, Data::BehaviorModifier>::getInstance();
 	case Data::DataObjectTypes::kMiniscriptModifier:
 		return ModifierFactory<MiniscriptModifier, Data::MiniscriptModifier>::getInstance();
+	case Data::DataObjectTypes::kDragMotionModifier:
+		return ModifierFactory<DragMotionModifier, Data::DragMotionModifier>::getInstance();
 	case Data::DataObjectTypes::kIfMessengerModifier:
 		return ModifierFactory<IfMessengerModifier, Data::IfMessengerModifier>::getInstance();
 	case Data::DataObjectTypes::kTimerMessengerModifier:
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 7c60f84f0a9..b4855b1d546 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -116,6 +116,42 @@ bool SetModifier::load(ModifierLoaderContext &context, const Data::SetModifier &
 	return true;
 }
 
+bool DragMotionModifier::load(ModifierLoaderContext &context, const Data::DragMotionModifier &data) {
+	if (!loadTypicalHeader(data.modHeader))
+		return false;
+
+	if (!_enableWhen.load(data.enableWhen) || !_disableWhen.load(data.disableWhen) || !_constraintMargin.load(data.constraintMargin))
+		return false;
+
+	bool constrainVertical = false;
+	bool constrainHorizontal = false;
+	if (data.haveMacPart) {
+		_constrainToParent = ((data.platform.mac.flags & Data::DragMotionModifier::MacPart::kConstrainToParent) != 0);
+		constrainVertical = ((data.platform.mac.flags & Data::DragMotionModifier::MacPart::kConstrainHorizontal) != 0);
+		constrainHorizontal = ((data.platform.mac.flags & Data::DragMotionModifier::MacPart::kConstrainVertical) != 0);
+	} else if (data.haveWinPart) {
+		_constrainToParent = (data.platform.win.constrainToParent != 0);
+		constrainVertical = (data.platform.win.constrainVertical != 0);
+		constrainHorizontal = (data.platform.win.constrainHorizontal != 0);
+	} else {
+		return false;
+	}
+
+	if (constrainVertical) {
+		if (constrainHorizontal)
+			return false;	// ???
+		else
+			_constraintDirection = kConstraintDirectionVertical;
+	} else {
+		if (constrainHorizontal)
+			_constraintDirection = kConstraintDirectionHorizontal;
+		else
+			_constraintDirection = kConstraintDirectionNone;
+	}
+
+	return true;
+}
+
 bool IfMessengerModifier::load(ModifierLoaderContext &context, const Data::IfMessengerModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 6cfe3bd7dc2..1724a285335 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -88,6 +88,25 @@ private:
 	Common::String _targetName;
 };
 
+class DragMotionModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::DragMotionModifier &data);
+
+private:
+	Event _enableWhen;
+	Event _disableWhen;
+
+	enum ConstraintDirection {
+		kConstraintDirectionNone,
+		kConstraintDirectionHorizontal,
+		kConstraintDirectionVertical,
+	};
+
+	ConstraintDirection _constraintDirection;
+	Rect16 _constraintMargin;
+	bool _constrainToParent;
+};
+
 class IfMessengerModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::IfMessengerModifier &data);
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index a46bc863eda..9f30f9d8c7d 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -30,6 +30,15 @@
 
 namespace MTropolis {
 
+bool Rect16::load(const Data::Rect& rect) {
+	top = rect.top;
+	left = rect.left;
+	bottom = rect.bottom;
+	right = rect.right;
+
+	return true;
+}
+
 bool ColorRGB8::load(const Data::ColorRGB16& color) {
 	this->r = (color.red * 510 + 1) / 131070;
 	this->g = (color.green * 510 + 1) / 131070;
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 70c7f00c242..8c66d47dba3 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -47,6 +47,15 @@ struct Point16 {
 	int16 y;
 };
 
+struct Rect16 {
+	int16 top;
+	int16 left;
+	int16 bottom;
+	int16 right;
+
+	bool load(const Data::Rect &rect);
+};
+
 struct ColorRGB8 {
 	uint8 r;
 	uint8 g;


Commit: 31fa569947332fdc6482a74db2bd6482d25c1dae
    https://github.com/scummvm/scummvm/commit/31fa569947332fdc6482a74db2bd6482d25c1dae
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Load integer, integer range, float, and string variables.  Fix label values in Miniscript.

Changed paths:
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/miniscript.h
    engines/mtropolis/modifier_factory.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/runtime.cpp


diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index 273d0805d61..0b6e74d4716 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -82,50 +82,6 @@ bool DataReader::readF64(double &value) {
 	return checkErrorAndReset();
 }
 
-bool DataReader::readF80(double &value) {
-	uint16_t signAndExponent;
-	uint64_t mantissa;
-	if (isBigEndian()) {
-		if (!readU16(signAndExponent) || !readU64(mantissa))
-			return false;
-	} else {
-		if (!readU64(mantissa) || !readU16(signAndExponent))
-			return false;
-	}
-
-	uint8_t sign = (signAndExponent >> 15) & 1;
-	int16_t exponent = signAndExponent & 0x7fff;
-
-	// Eliminate implicit 1 and truncate from 63 to 52 bits
-	mantissa &= (((static_cast<uint64_t>(1) << 52) - 1u) << 11);
-	mantissa >>= 11;
-
-	if (mantissa != 0 || exponent != 0) {
-		// Adjust exponent
-		exponent -= 15360;
-		if (exponent > 2046) {
-			// Too big, set to largest finite magnitude
-			exponent = 2046;
-			mantissa = (static_cast<uint64_t>(1) << 52) - 1u;
-		} else if (exponent < 0) {
-			// Subnormal number
-			mantissa |= (static_cast<uint64_t>(1) << 52);
-			if (exponent < -52) {
-				mantissa = 0;
-				exponent = 0;
-			} else {
-				mantissa >>= (-exponent);
-				exponent = 0;
-			}
-		}
-	}
-
-	uint64_t recombined = (static_cast<uint64_t>(sign) << 63) | (static_cast<uint64_t>(exponent) << 52) | static_cast<uint64_t>(mantissa);
-	memcpy(&value, &recombined, 8);
-
-	return true;
-}
-
 bool DataReader::read(void *dest, size_t size) {
 	while (size > 0) {
 		uint32 thisChunkSize = UINT32_MAX;
@@ -256,6 +212,76 @@ bool ColorRGB16::load(DataReader& reader) {
 		return false;
 }
 
+bool XPFloat::load(DataReader &reader) {
+	if (reader.getProjectFormat() == kProjectFormatMacintosh)
+		return reader.readU16(signAndExponent) && reader.readU64(mantissa);
+	else if (reader.getProjectFormat() == kProjectFormatWindows) {
+		uint64 doublePrecFloatBits;
+		if (!reader.readU64(doublePrecFloatBits))
+			return false;
+
+		uint8 sign = ((doublePrecFloatBits >> 63) & 1);
+		int16 exponent = ((doublePrecFloatBits >> 52) & ((1 << 11) - 1));
+		uint64 workMantissa = (doublePrecFloatBits & ((static_cast<uint64>(1) << 52) - 1));
+
+		if (exponent == 0) {
+			// Subnormal number or zero
+			if (workMantissa != 0) {
+				while (((workMantissa >> 52) & 1) == 0) {
+					exponent--;
+					workMantissa <<= 1;
+				}
+			}
+		} else {
+			workMantissa |= (static_cast<uint64>(1) << 52);
+		}
+
+		exponent += 15360;
+
+		signAndExponent = (sign << 15) | exponent;
+		mantissa = workMantissa << 11;
+
+		return true;
+	} else
+		return false;
+}
+
+double XPFloat::toDouble() const {
+	uint8 sign = (signAndExponent >> 15) & 1;
+	int16 exponent = signAndExponent & 0x7fff;
+
+	// Eliminate implicit 1 and truncate from 63 to 52 bits
+	uint64 workMantissa = this->mantissa & (((static_cast<uint64>(1) << 52) - 1u) << 11);
+	workMantissa >>= 11;
+
+	if (mantissa != 0 || exponent != 0) {
+		// Adjust exponent
+		exponent -= 15360;
+		if (exponent > 2046) {
+			// Too big, set to largest finite magnitude
+			exponent = 2046;
+			workMantissa = (static_cast<uint64_t>(1) << 52) - 1u;
+		} else if (exponent < 0) {
+			// Subnormal number
+			workMantissa |= (static_cast<uint64_t>(1) << 52);
+			if (exponent < -52) {
+				workMantissa = 0;
+				exponent = 0;
+			} else {
+				workMantissa >>= (-exponent);
+				exponent = 0;
+			}
+		}
+	}
+
+	uint64_t recombined = (static_cast<uint64_t>(sign) << 63) | (static_cast<uint64_t>(exponent) << 52) | static_cast<uint64_t>(mantissa);
+
+	double d;
+	memcpy(&d, &recombined, 8);
+
+	return d;
+}
+
 DataObject::DataObject() : _type(DataObjectTypes::kUnknown), _revision(0) {
 }
 
@@ -714,6 +740,26 @@ DataReadErrorCode BooleanVariableModifier::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode IntegerVariableModifier::load(DataReader &reader) {
+	if (_revision != 0x3e8)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !reader.readS32(value))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode IntegerRangeVariableModifier::load(DataReader &reader) {
+	if (_revision != 0x3e8)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !reader.readS32(min) || !reader.readS32(max))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode PointVariableModifier::load(DataReader &reader) {
 	if (_revision != 0x3e8)
 		return kDataReadErrorUnsupportedRevision;
@@ -724,6 +770,26 @@ DataReadErrorCode PointVariableModifier::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode FloatingPointVariableModifier::load(DataReader &reader) {
+	if (_revision != 0x3e8)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !value.load(reader))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode StringVariableModifier::load(DataReader &reader) {
+	if (_revision != 0x3e8)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!modHeader.load(reader) || !reader.readU32(lengthOfString) || !reader.readBytes(unknown1) || !reader.readTerminatedStr(value, lengthOfString))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 PlugInModifierData::~PlugInModifierData() {
 }
 
@@ -833,9 +899,21 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 	case DataObjectTypes::kBooleanVariableModifier:
 		dataObject = new BooleanVariableModifier();
 		break;
+	case DataObjectTypes::kIntegerVariableModifier:
+		dataObject = new IntegerVariableModifier();
+		break;
+	case DataObjectTypes::kIntegerRangeVariableModifier:
+		dataObject = new IntegerRangeVariableModifier();
+		break;
 	case DataObjectTypes::kPointVariableModifier:
 		dataObject = new PointVariableModifier();
 		break;
+	case DataObjectTypes::kFloatingPointVariableModifier:
+		dataObject = new FloatingPointVariableModifier();
+		break;
+	case DataObjectTypes::kStringVariableModifier:
+		dataObject = new StringVariableModifier();
+		break;
 	case DataObjectTypes::kDebris:
 		dataObject = new Debris();
 		break;
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index faff4846599..fcfc9fdac44 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -122,12 +122,12 @@ enum DataObjectType {
 
 	kCompoundVariableModifier            = 0x2c7,	// NYI
 	kBooleanVariableModifier             = 0x321,
-	kIntegerVariableModifier             = 0x322,	// NYI
-	kIntegerRangeVariableModifier        = 0x324,	// NYI
+	kIntegerVariableModifier             = 0x322,
+	kIntegerRangeVariableModifier        = 0x324,
 	kVectorVariableModifier              = 0x327,	// NYI
-	kFloatingPointVariableModifier       = 0x328,	// NYI
 	kPointVariableModifier               = 0x326,
-	kStringVariableModifier              = 0x329,	// NYI
+	kFloatingPointVariableModifier       = 0x328,
+	kStringVariableModifier              = 0x329,
 
 	kDebris                              = 0xfffffffe,	// Deleted object
 	kPlugInModifier                      = 0xffffffff,
@@ -151,7 +151,6 @@ public:
 	bool readS64(int64 &value);
 	bool readF32(float &value);
 	bool readF64(double &value);
-	bool readF80(double &value);
 
 	bool read(void *dest, size_t size);
 
@@ -209,6 +208,14 @@ struct ColorRGB16 {
 	uint16 blue;
 };
 
+struct XPFloat {
+	bool load(DataReader &reader);
+	double toDouble() const;
+
+	uint64_t mantissa;
+	uint16_t signAndExponent;
+};
+
 class DataObject : public Common::NonCopyable {
 
 public:
@@ -728,8 +735,27 @@ protected:
 
 struct BooleanVariableModifier : public DataObject {
 	TypicalModifierHeader modHeader;
-	uint8_t value;
-	uint8_t unknown5;
+	uint8 value;
+	uint8 unknown5;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+struct IntegerVariableModifier : public DataObject {
+	TypicalModifierHeader modHeader;
+	uint8 unknown1[4];
+	int32 value;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+struct IntegerRangeVariableModifier : public DataObject {
+	TypicalModifierHeader modHeader;
+	uint8 unknown1[4];
+	int32 min;
+	int32 max;
 
 protected:
 	DataReadErrorCode load(DataReader &reader) override;
@@ -738,13 +764,32 @@ protected:
 struct PointVariableModifier : public DataObject {
 	TypicalModifierHeader modHeader;
 
-	uint8_t unknown5[4];
+	uint8 unknown5[4];
 	Point value;
 
 protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct FloatingPointVariableModifier : public DataObject {
+	TypicalModifierHeader modHeader;
+	uint8 unknown1[4];
+	XPFloat value;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+struct StringVariableModifier : public DataObject {
+	TypicalModifierHeader modHeader;
+	uint32 lengthOfString;
+	uint8 unknown1[4];
+	Common::String value;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 struct PlugInModifierData {
 	virtual ~PlugInModifierData();
 	virtual DataReadErrorCode load(const PlugInModifier &prefix, DataReader &reader) = 0;
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index dc9a036c994..ec6838d2264 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -128,17 +128,11 @@ bool MiniscriptInstructionLoader<MiniscriptInstructions::PushValue>::loadInstruc
 	if (dataType == 0)
 		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeNull, nullptr);
 	else if (dataType == 0x15) {
-		double d;
-		if (instrDataReader.getProjectFormat() == Data::kProjectFormatMacintosh) {
-			if (!instrDataReader.readF80(d))
-				return false;
-		} else if (instrDataReader.getProjectFormat() == Data::kProjectFormatWindows) {
-			if (!instrDataReader.readF64(d))
-				return false;
-		} else {
+		Data::XPFloat f;
+		if (!f.load(instrDataReader))
 			return false;
-		}
 
+		double d = f.toDouble();
 		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeDouble, &d);
 	} else if (dataType == 0x1a) {
 		uint8 boolValue;
@@ -153,13 +147,18 @@ bool MiniscriptInstructionLoader<MiniscriptInstructions::PushValue>::loadInstruc
 			return false;
 
 		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeLocalRef, &refValue);
-
 	} else if (dataType == 0x1fa) {
 		uint32 refValue;
 		if (!instrDataReader.readU32(refValue))
 			return false;
 
 		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeGlobalRef, &refValue);
+	} else if (dataType == 0x1d) {
+		MiniscriptInstructions::PushValue::Label label;
+		if (!instrDataReader.readU32(label.superGroup) || !instrDataReader.readU32(label.id))
+			return false;
+
+		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeLabel, &label);
 	} else
 		return false;
 
@@ -423,12 +422,15 @@ PushValue::PushValue(DataType dataType, const void *value) {
 		_value.b = *static_cast<const bool *>(value);
 		break;
 	case DataType::kDataTypeDouble:
-		_value.d = *static_cast<const double *>(value);
+		_value.f = *static_cast<const double *>(value);
 		break;
 	case DataType::kDataTypeLocalRef:
 	case DataType::kDataTypeGlobalRef:
 		_value.ref = *static_cast<const uint32 *>(value);
 		break;
+	case DataType::kDataTypeLabel:
+		_value.lbl = *static_cast<const Label *>(value);
+		break;
 	default:
 		break;
 	}
diff --git a/engines/mtropolis/miniscript.h b/engines/mtropolis/miniscript.h
index b6fb50aca1c..8943d732ba1 100644
--- a/engines/mtropolis/miniscript.h
+++ b/engines/mtropolis/miniscript.h
@@ -195,6 +195,12 @@ namespace MiniscriptInstructions {
 			kDataTypeBool,
 			kDataTypeLocalRef,
 			kDataTypeGlobalRef,
+			kDataTypeLabel,
+		};
+
+		struct Label {
+			uint32 superGroup;
+			uint32 id;
 		};
 
 		PushValue(DataType dataType, const void *value);
@@ -202,8 +208,9 @@ namespace MiniscriptInstructions {
 	private:
 		union ValueUnion {
 			bool b;
-			double d;
+			double f;
 			uint32 ref;
+			Label lbl;
 		};
 
 		DataType _dataType;
diff --git a/engines/mtropolis/modifier_factory.cpp b/engines/mtropolis/modifier_factory.cpp
index edc68bb9cb8..10dbdf479e7 100644
--- a/engines/mtropolis/modifier_factory.cpp
+++ b/engines/mtropolis/modifier_factory.cpp
@@ -85,8 +85,17 @@ IModifierFactory *getModifierFactoryForDataObjectType(const Data::DataObjectType
 		return ModifierFactory<SetModifier, Data::SetModifier>::getInstance();
 	case Data::DataObjectTypes::kBooleanVariableModifier:
 		return ModifierFactory<BooleanVariableModifier, Data::BooleanVariableModifier>::getInstance();
+	case Data::DataObjectTypes::kIntegerVariableModifier:
+		return ModifierFactory<IntegerVariableModifier, Data::IntegerVariableModifier>::getInstance();
+	case Data::DataObjectTypes::kIntegerRangeVariableModifier:
+		return ModifierFactory<IntegerRangeVariableModifier, Data::IntegerRangeVariableModifier>::getInstance();
 	case Data::DataObjectTypes::kPointVariableModifier:
 		return ModifierFactory<PointVariableModifier, Data::PointVariableModifier>::getInstance();
+	case Data::DataObjectTypes::kFloatingPointVariableModifier:
+		return ModifierFactory<FloatingPointVariableModifier, Data::FloatingPointVariableModifier>::getInstance();
+	case Data::DataObjectTypes::kStringVariableModifier:
+		return ModifierFactory<StringVariableModifier, Data::StringVariableModifier>::getInstance();
+
 	default:
 		return nullptr;
 	}
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index b4855b1d546..915bd8f4aca 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -319,6 +319,25 @@ bool BooleanVariableModifier::load(ModifierLoaderContext &context, const Data::B
 	return true;
 }
 
+bool IntegerVariableModifier::load(ModifierLoaderContext& context, const Data::IntegerVariableModifier& data) {
+	if (!loadTypicalHeader(data.modHeader))
+		return false;
+
+	_value = data.value;
+
+	return true;
+}
+
+bool IntegerRangeVariableModifier::load(ModifierLoaderContext& context, const Data::IntegerRangeVariableModifier& data) {
+	if (!loadTypicalHeader(data.modHeader))
+		return false;
+
+	_min = data.min;
+	_max = data.max;
+
+	return true;
+}
+
 bool PointVariableModifier::load(ModifierLoaderContext &context, const Data::PointVariableModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -329,4 +348,22 @@ bool PointVariableModifier::load(ModifierLoaderContext &context, const Data::Poi
 	return true;
 }
 
+bool FloatingPointVariableModifier::load(ModifierLoaderContext &context, const Data::FloatingPointVariableModifier &data) {
+	if (!loadTypicalHeader(data.modHeader))
+		return false;
+
+	_value = data.value.toDouble();
+
+	return true;
+}
+
+bool StringVariableModifier::load(ModifierLoaderContext &context, const Data::StringVariableModifier &data) {
+	if (!loadTypicalHeader(data.modHeader))
+		return false;
+
+	_value = data.value;
+
+	return true;
+}
+
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 1724a285335..292929ca089 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -276,6 +276,23 @@ private:
 	bool _value;
 };
 
+class IntegerVariableModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::IntegerVariableModifier &data);
+
+private:
+	int32 _value;
+};
+
+class IntegerRangeVariableModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::IntegerRangeVariableModifier &data);
+
+private:
+	int32 _min;
+	int32 _max;
+};
+
 class PointVariableModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::PointVariableModifier &data);
@@ -284,6 +301,22 @@ private:
 	Point16 _value;
 };
 
+class FloatingPointVariableModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::FloatingPointVariableModifier &data);
+
+private:
+	double _value;
+};
+
+class StringVariableModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::StringVariableModifier &data);
+
+private:
+	Common::String _value;
+};
+
 }	// End of namespace MTropolis
 
 #endif
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 9f30f9d8c7d..1a31511fabb 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -345,6 +345,7 @@ void Project::loadBootStream(size_t streamIndex) {
 
 	size_t numObjectsLoaded = 0;
 	for (;;) {
+		int64 globalPos = stream.pos() + streamDesc.pos;
 		Common::SharedPtr<Data::DataObject> dataObject;
 		Data::loadDataObject(plugInDataLoaderRegistry, reader, dataObject);
 


Commit: f262a1f6ba4963dcbe618962fc24bafc84a62fef
    https://github.com/scummvm/scummvm/commit/f262a1f6ba4963dcbe618962fc24bafc84a62fef
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add vector motion and vector variable modifier loaders

Changed paths:
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/modifier_factory.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h


diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index 0b6e74d4716..fe0b8eac0ec 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -607,6 +607,21 @@ DataReadErrorCode DragMotionModifier::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode VectorMotionModifier::load(DataReader &reader) {
+	if (_revision != 0x3e9)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!modHeader.load(reader))
+		return kDataReadErrorReadFailed;
+
+	if (!enableWhen.load(reader) || !disableWhen.load(reader) || !varSource.load(reader)
+		|| !reader.readU16(unknown1) || !reader.readU8(varSourceNameLength) || !reader.readU8(unknown2)
+		|| !reader.readNonTerminatedStr(varSourceName, varSourceNameLength))
+		return kDataReadErrorNone;
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode IfMessengerModifier::load(DataReader &reader) {
 	if (_revision != 0x3ea)
 		return kDataReadErrorUnsupportedRevision;
@@ -760,6 +775,16 @@ DataReadErrorCode IntegerRangeVariableModifier::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode VectorVariableModifier::load(DataReader &reader) {
+	if (_revision != 0x3e8)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !angleRadians.load(reader) || !magnitude.load(reader))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode PointVariableModifier::load(DataReader &reader) {
 	if (_revision != 0x3e8)
 		return kDataReadErrorUnsupportedRevision;
@@ -893,6 +918,9 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 	case DataObjectTypes::kDragMotionModifier:
 		dataObject = new DragMotionModifier();
 		break;
+	case DataObjectTypes::kVectorMotionModifier:
+		dataObject = new VectorMotionModifier();
+		break;
 	case DataObjectTypes::kIfMessengerModifier:
 		dataObject = new IfMessengerModifier();
 		break;
@@ -911,6 +939,9 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 	case DataObjectTypes::kFloatingPointVariableModifier:
 		dataObject = new FloatingPointVariableModifier();
 		break;
+	case DataObjectTypes::kVectorVariableModifier:
+		dataObject = new VectorVariableModifier();
+		break;
 	case DataObjectTypes::kStringVariableModifier:
 		dataObject = new StringVariableModifier();
 		break;
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index fcfc9fdac44..29e67af8534 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -97,7 +97,7 @@ enum DataObjectType {
 	kChangeSceneModifier                 = 0x136,	// NYI
 	kReturnModifier                      = 0x140,	// NYI
 	kDragMotionModifier                  = 0x208,
-	kVectorMotionModifier                = 0x226,	// NYI
+	kVectorMotionModifier                = 0x226,
 	kPathMotionModifierV1                = 0x21c,	// NYI - Obsolete version
 	kPathMotionModifierV2                = 0x21b,	// NYI
 	kSceneTransitionModifier             = 0x26c,	// NYI
@@ -124,7 +124,7 @@ enum DataObjectType {
 	kBooleanVariableModifier             = 0x321,
 	kIntegerVariableModifier             = 0x322,
 	kIntegerRangeVariableModifier        = 0x324,
-	kVectorVariableModifier              = 0x327,	// NYI
+	kVectorVariableModifier              = 0x327,
 	kPointVariableModifier               = 0x326,
 	kFloatingPointVariableModifier       = 0x328,
 	kStringVariableModifier              = 0x329,
@@ -520,6 +520,22 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct VectorMotionModifier : public DataObject {
+	TypicalModifierHeader modHeader;
+
+	Event enableWhen;
+	Event disableWhen;
+	MessageDataLocator varSource;
+	uint16 unknown1;
+	uint8 varSourceNameLength;
+	uint8 unknown2;
+
+	Common::String varSourceName;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 struct IfMessengerModifier : public DataObject {
 	TypicalModifierHeader modHeader;
 
@@ -761,6 +777,16 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct VectorVariableModifier : public DataObject {
+	TypicalModifierHeader modHeader;
+	uint8 unknown1[4];
+	XPFloat angleRadians;
+	XPFloat magnitude;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 struct PointVariableModifier : public DataObject {
 	TypicalModifierHeader modHeader;
 
diff --git a/engines/mtropolis/modifier_factory.cpp b/engines/mtropolis/modifier_factory.cpp
index 10dbdf479e7..63996457188 100644
--- a/engines/mtropolis/modifier_factory.cpp
+++ b/engines/mtropolis/modifier_factory.cpp
@@ -67,6 +67,8 @@ IModifierFactory *getModifierFactoryForDataObjectType(const Data::DataObjectType
 		return ModifierFactory<MiniscriptModifier, Data::MiniscriptModifier>::getInstance();
 	case Data::DataObjectTypes::kDragMotionModifier:
 		return ModifierFactory<DragMotionModifier, Data::DragMotionModifier>::getInstance();
+	case Data::DataObjectTypes::kVectorMotionModifier:
+		return ModifierFactory<VectorMotionModifier, Data::VectorMotionModifier>::getInstance();
 	case Data::DataObjectTypes::kIfMessengerModifier:
 		return ModifierFactory<IfMessengerModifier, Data::IfMessengerModifier>::getInstance();
 	case Data::DataObjectTypes::kTimerMessengerModifier:
@@ -90,6 +92,8 @@ IModifierFactory *getModifierFactoryForDataObjectType(const Data::DataObjectType
 	case Data::DataObjectTypes::kIntegerRangeVariableModifier:
 		return ModifierFactory<IntegerRangeVariableModifier, Data::IntegerRangeVariableModifier>::getInstance();
 	case Data::DataObjectTypes::kPointVariableModifier:
+		return ModifierFactory<VectorVariableModifier, Data::VectorVariableModifier>::getInstance();
+	case Data::DataObjectTypes::kVectorVariableModifier:
 		return ModifierFactory<PointVariableModifier, Data::PointVariableModifier>::getInstance();
 	case Data::DataObjectTypes::kFloatingPointVariableModifier:
 		return ModifierFactory<FloatingPointVariableModifier, Data::FloatingPointVariableModifier>::getInstance();
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 915bd8f4aca..60047347e1c 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -152,6 +152,20 @@ bool DragMotionModifier::load(ModifierLoaderContext &context, const Data::DragMo
 	return true;
 }
 
+bool VectorMotionModifier::load(ModifierLoaderContext &context, const Data::VectorMotionModifier &data) {
+	if (!loadTypicalHeader(data.modHeader))
+		return false;
+
+	if (!_enableWhen.load(data.enableWhen) || !_disableWhen.load(data.disableWhen))
+		return false;
+
+	_sourceVarGUID = data.varSource.guid;
+	_sourceVarLocType = static_cast<MessageWithType>(data.varSource.locationType);
+	_sourceVarName = data.varSourceName;
+
+	return true;
+}
+
 bool IfMessengerModifier::load(ModifierLoaderContext &context, const Data::IfMessengerModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -338,6 +352,16 @@ bool IntegerRangeVariableModifier::load(ModifierLoaderContext& context, const Da
 	return true;
 }
 
+bool VectorVariableModifier::load(ModifierLoaderContext &context, const Data::VectorVariableModifier &data) {
+	if (!loadTypicalHeader(data.modHeader))
+		return false;
+
+	_angleRadians = data.angleRadians.toDouble();
+	_magnitude = data.magnitude.toDouble();
+
+	return true;
+}
+
 bool PointVariableModifier::load(ModifierLoaderContext &context, const Data::PointVariableModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 292929ca089..d37c1f415c0 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -107,6 +107,19 @@ private:
 	bool _constrainToParent;
 };
 
+class VectorMotionModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::VectorMotionModifier &data);
+
+private:
+	Event _enableWhen;
+	Event _disableWhen;
+
+	MessageWithType _sourceVarLocType;
+	uint32 _sourceVarGUID;
+	Common::String _sourceVarName;
+};
+
 class IfMessengerModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::IfMessengerModifier &data);
@@ -293,6 +306,15 @@ private:
 	int32 _max;
 };
 
+class VectorVariableModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::VectorVariableModifier &data);
+
+private:
+	double _angleRadians;
+	double _magnitude;
+};
+
 class PointVariableModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::PointVariableModifier &data);


Commit: 0e26437384b20c4cc5de77e14a24bd6d3b30f0df
    https://github.com/scummvm/scummvm/commit/0e26437384b20c4cc5de77e14a24bd6d3b30f0df
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add boundary detection messenger modifier loader, add label data locators, consolidate some messenger structures.

Changed paths:
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/modifier_factory.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index fe0b8eac0ec..bff543dcfd5 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -538,7 +538,7 @@ DataReadErrorCode MiniscriptModifier::load(DataReader &reader) {
 }
 
 bool MessageDataLocator::load(DataReader& reader) {
-	if (!reader.readU16(locationType) || !reader.readBytes(unknown1) || !reader.readU32(guid) || !reader.readBytes(unknown2))
+	if (!reader.readU16(locationType) || !reader.readU32(superGroupID) || !reader.readU32(guidOrLabelID) || !reader.readBytes(unknown2))
 		return false;
 
 	return true;
@@ -626,9 +626,9 @@ DataReadErrorCode IfMessengerModifier::load(DataReader &reader) {
 	if (_revision != 0x3ea)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !reader.readU32(messageFlags) || !when.load(reader) || !send.load(reader) ||
-		!reader.readU16(unknown6) || !reader.readU32(destination) || !reader.readBytes(unknown7) || !reader.readU16(with)
-		|| !reader.readBytes(unknown8) || !reader.readU32(withSourceGUID) || !reader.readBytes(unknown9) || !reader.readU8(withSourceLength) || !reader.readU8(unknown10))
+	if (!modHeader.load(reader) || !reader.readU32(messageFlags) || !when.load(reader) || !send.load(reader)
+		|| !reader.readU16(unknown6) || !reader.readU32(destination) || !reader.readBytes(unknown7) || !with.load(reader)
+		|| !reader.readBytes(unknown9) || !reader.readU8(withSourceLength) || !reader.readU8(unknown10))
 		return kDataReadErrorReadFailed;
 
 	if (withSourceLength > 0 && !reader.readNonTerminatedStr(withSource, withSourceLength))
@@ -658,6 +658,22 @@ DataReadErrorCode TimerMessengerModifier::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode BoundaryDetectionMessengerModifier::load(DataReader &reader) {
+	if (_revision != 0x3ea)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!modHeader.load(reader))
+		return kDataReadErrorReadFailed;
+
+	if (!reader.readU16(messageFlagsHigh) || !enableWhen.load(reader) || !disableWhen.load(reader)
+		|| !send.load(reader) || !reader.readU16(unknown2) || !reader.readU32(destination)
+		|| !reader.readBytes(unknown3) || !with.load(reader) || !reader.readU8(withSourceLength)
+		|| !reader.readU8(unknown4) || !reader.readNonTerminatedStr(withSource, withSourceLength))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode CollisionDetectionMessengerModifier::load(DataReader &reader) {
 	if (_revision != 0x3ea)
 		return kDataReadErrorUnsupportedRevision;
@@ -681,8 +697,7 @@ DataReadErrorCode KeyboardMessengerModifier::load(DataReader &reader) {
 	if (!modHeader.load(reader) || !reader.readU32(messageFlagsAndKeyStates) || !reader.readU16(unknown2)
 		|| !reader.readU16(keyModifiers) || !reader.readU8(keycode) || !reader.readBytes(unknown4)
 		|| !message.load(reader) || !reader.readU16(unknown7) || !reader.readU32(destination)
-		|| !reader.readBytes(unknown9) || !reader.readU16(with) || !reader.readBytes(unknown11)
-		|| !reader.readU32(withSourceGUID) || !reader.readBytes(unknown13) || !reader.readU8(withSourceLength)
+		|| !reader.readBytes(unknown9) || !with.load(reader) || !reader.readU8(withSourceLength)
 		|| !reader.readU8(unknown14))
 		return kDataReadErrorReadFailed;
 
@@ -957,6 +972,9 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 	case DataObjectTypes::kCollisionDetectionMessengerModifier:
 		dataObject = new CollisionDetectionMessengerModifier();
 		break;
+	case DataObjectTypes::kBoundaryDetectionMessengerModifier:
+		dataObject = new BoundaryDetectionMessengerModifier();
+		break;
 	case DataObjectTypes::kKeyboardMessengerModifier:
 		dataObject = new KeyboardMessengerModifier();
 		break;
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index 29e67af8534..1bb73392f84 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -109,7 +109,7 @@ enum DataObjectType {
 	kSetModifier                         = 0x2df,
 	kTimerMessengerModifier              = 0x2e4,
 	kCollisionDetectionMessengerModifier = 0x2ee,
-	kBoundaryDetectionMessengerModifier  = 0x2f8,	// NYI
+	kBoundaryDetectionMessengerModifier  = 0x2f8,
 	kKeyboardMessengerModifier           = 0x302,
 	kTextStyleModifier                   = 0x32a,
 	kGraphicModifier                     = 0x334,
@@ -435,8 +435,8 @@ enum MessageFlags {
 
 struct MessageDataLocator {
 	uint16 locationType;
-	uint8 unknown1[4];
-	uint32 guid;
+	uint32 superGroupID;
+	uint32 guidOrLabelID;
 	uint8 unknown2[36];
 
 	bool load(DataReader &reader);
@@ -545,10 +545,8 @@ struct IfMessengerModifier : public DataObject {
 	uint16 unknown6;
 	uint32 destination;
 	uint8 unknown7[10];
-	uint16 with;
-	uint8 unknown8[4];
-	uint32 withSourceGUID;
-	uint8 unknown9[46];
+	MessageDataLocator with;
+	uint8 unknown9[10];
 	uint8 withSourceLength;
 	uint8 unknown10;
 	MiniscriptProgram program;
@@ -590,9 +588,35 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
-struct CollisionDetectionMessengerModifier : public DataObject {
+struct BoundaryDetectionMessengerModifier : public DataObject {
+	enum Flags {
+		kDetectTopEdge = 0x1000,
+		kDetectBottomEdge = 0x0800,
+		kDetectLeftEdge = 0x0400,
+		kDetectRightEdge = 0x0200,
+		kDetectExiting = 0x0100, // Off = once exited
+		kWhileDetected = 0x0080, // Off = on first detected
+	};
+
 	TypicalModifierHeader modHeader;
+	uint16 messageFlagsHigh;
+	Event enableWhen;
+	Event disableWhen;
+	Event send;
+	uint16 unknown2;
+	uint32 destination;
+	uint8 unknown3[10];
+	MessageDataLocator with;
+	uint8 withSourceLength;
+	uint8 unknown4;
+
+	Common::String withSource;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
 
+struct CollisionDetectionMessengerModifier : public DataObject {
 	enum ModifierFlags {
 		kDetectLayerInFront = 0x10000000,
 		kDetectLayerBehind = 0x08000000,
@@ -607,6 +631,7 @@ struct CollisionDetectionMessengerModifier : public DataObject {
 		kNoCollideWithParent = 0x00100000,
 	};
 
+	TypicalModifierHeader modHeader;
 	uint32 messageAndModifierFlags;
 	Event enableWhen;
 	Event disableWhen;
@@ -668,10 +693,7 @@ struct KeyboardMessengerModifier : public DataObject {
 	uint16 unknown7;
 	uint32 destination;
 	uint8 unknown9[10];
-	uint16 with;
-	uint8 unknown11[4];
-	uint32 withSourceGUID;
-	uint8 unknown13[36];
+	MessageDataLocator with;
 	uint8 withSourceLength;
 	uint8 unknown14;
 
diff --git a/engines/mtropolis/modifier_factory.cpp b/engines/mtropolis/modifier_factory.cpp
index 63996457188..ff79009bc15 100644
--- a/engines/mtropolis/modifier_factory.cpp
+++ b/engines/mtropolis/modifier_factory.cpp
@@ -73,6 +73,8 @@ IModifierFactory *getModifierFactoryForDataObjectType(const Data::DataObjectType
 		return ModifierFactory<IfMessengerModifier, Data::IfMessengerModifier>::getInstance();
 	case Data::DataObjectTypes::kTimerMessengerModifier:
 		return ModifierFactory<TimerMessengerModifier, Data::TimerMessengerModifier>::getInstance();
+	case Data::DataObjectTypes::kBoundaryDetectionMessengerModifier:
+		return ModifierFactory<BoundaryDetectionMessengerModifier, Data::BoundaryDetectionMessengerModifier>::getInstance();
 	case Data::DataObjectTypes::kCollisionDetectionMessengerModifier:
 		return ModifierFactory<CollisionDetectionMessengerModifier, Data::CollisionDetectionMessengerModifier>::getInstance();
 	case Data::DataObjectTypes::kKeyboardMessengerModifier:
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 60047347e1c..599f53e5367 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -68,10 +68,10 @@ bool MiniscriptModifier::load(ModifierLoaderContext &context, const Data::Minisc
 	return true;
 }
 
-MessengerSendSpec::MessengerSendSpec() : withType(kMessageWithNothing), withSourceGUID(0), destination(0) {
+MessengerSendSpec::MessengerSendSpec() : destination(0) {
 }
 
-bool MessengerSendSpec::load(const Data::Event& dataEvent, uint32 dataMessageFlags, uint16 dataWith, const Common::String &dataWithSourceName, uint32 dataWithSourceGUID, uint32 dataDestination) {
+bool MessengerSendSpec::load(const Data::Event &dataEvent, uint32 dataMessageFlags, const Data::MessageDataLocator &dataLocator, const Common::String &dataWithSourceName, uint32 dataDestination) {
 	messageFlags.relay = ((dataMessageFlags & 0x20000000) == 0);
 	messageFlags.cascade = ((dataMessageFlags & 0x40000000) == 0);
 	messageFlags.immediate = ((dataMessageFlags & 0x80000000) == 0);
@@ -79,10 +79,10 @@ bool MessengerSendSpec::load(const Data::Event& dataEvent, uint32 dataMessageFla
 	if (!this->send.load(dataEvent))
 		return false;
 
+	if (!this->with.load(dataLocator, dataWithSourceName))
+		return false;
+
 	this->destination = dataDestination;
-	this->withSourceGUID = dataWithSourceGUID;
-	this->withType = static_cast<MessageWithType>(dataWith);
-	this->withSourceName = dataWithSourceName;
 
 	return true;
 }
@@ -91,7 +91,7 @@ bool MessengerModifier::load(ModifierLoaderContext &context, const Data::Messeng
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
 
-	if (!_when.load(data.when) || !_sendSpec.load(data.send, data.messageFlags, data.with.locationType, data.withSourceName, data.with.guid, data.destination))
+	if (!_when.load(data.when) || !_sendSpec.load(data.send, data.messageFlags, data.with, data.withSourceName, data.destination))
 		return false;
 
 	return true;
@@ -101,18 +101,9 @@ bool SetModifier::load(ModifierLoaderContext &context, const Data::SetModifier &
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
 
-	if (!_executeWhen.load(data.executeWhen))
+	if (!_executeWhen.load(data.executeWhen) || !_sourceLoc.load(data.sourceLocator, data.sourceName) || !_targetLoc.load(data.targetLocator, data.targetName))
 		return false;
 
-	_sourceLocType = static_cast<MessageWithType>(data.sourceLocator.locationType);
-	_targetLocType = static_cast<MessageWithType>(data.targetLocator.locationType);
-
-	_sourceGUID = data.sourceLocator.guid;
-	_targetGUID = data.targetLocator.guid;
-
-	_sourceName = data.sourceName;
-	_targetName = data.targetName;
-
 	return true;
 }
 
@@ -156,13 +147,9 @@ bool VectorMotionModifier::load(ModifierLoaderContext &context, const Data::Vect
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
 
-	if (!_enableWhen.load(data.enableWhen) || !_disableWhen.load(data.disableWhen))
+	if (!_enableWhen.load(data.enableWhen) || !_disableWhen.load(data.disableWhen) || !_sourceVarLoc.load(data.varSource, data.varSourceName))
 		return false;
 
-	_sourceVarGUID = data.varSource.guid;
-	_sourceVarLocType = static_cast<MessageWithType>(data.varSource.locationType);
-	_sourceVarName = data.varSourceName;
-
 	return true;
 }
 
@@ -170,7 +157,7 @@ bool IfMessengerModifier::load(ModifierLoaderContext &context, const Data::IfMes
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
 
-	if (!_when.load(data.when) || !_sendSpec.load(data.send, data.messageFlags, data.with, data.withSource, data.withSourceGUID, data.destination))
+	if (!_when.load(data.when) || !_sendSpec.load(data.send, data.messageFlags, data.with, data.withSource, data.destination))
 		return false;
 
 	_program = MiniscriptParser::parse(data.program);
@@ -187,7 +174,7 @@ bool TimerMessengerModifier::load(ModifierLoaderContext &context, const Data::Ti
 	if (!_executeWhen.load(data.executeWhen) || !this->_terminateWhen.load(data.terminateWhen))
 		return false;
 
-	if (!_sendSpec.load(data.send, data.messageAndTimerFlags, data.with.locationType, data.withSource, data.with.guid, data.destination))
+	if (!_sendSpec.load(data.send, data.messageAndTimerFlags, data.with, data.withSource, data.destination))
 		return false;
 
 	_milliseconds = data.minutes * (60 * 1000) + data.seconds * (1000) + data.hundredthsOfSeconds * 10;
@@ -196,15 +183,35 @@ bool TimerMessengerModifier::load(ModifierLoaderContext &context, const Data::Ti
 	return true;
 }
 
-bool CollisionDetectionMessengerModifier::load(ModifierLoaderContext &context, const Data::CollisionDetectionMessengerModifier &data) {
+bool BoundaryDetectionMessengerModifier::load(ModifierLoaderContext &context, const Data::BoundaryDetectionMessengerModifier &data) {
+	if (!loadTypicalHeader(data.modHeader))
+		return false;
 
+	if (!_enableWhen.load(data.enableWhen) || !this->_disableWhen.load(data.disableWhen))
+		return false;
+
+	_exitTriggerMode = ((data.messageFlagsHigh & Data::BoundaryDetectionMessengerModifier::kDetectExiting) != 0) ? kExitTriggerExiting : kExitTriggerOnceExited;
+	_detectionMode = ((data.messageFlagsHigh & Data::BoundaryDetectionMessengerModifier::kWhileDetected) != 0) ? kContinuous : kOnFirstDetection;
+		
+	_detectTopEdge = ((data.messageFlagsHigh & Data::BoundaryDetectionMessengerModifier::kDetectTopEdge) != 0);
+	_detectBottomEdge = ((data.messageFlagsHigh & Data::BoundaryDetectionMessengerModifier::kDetectBottomEdge) != 0);
+	_detectLeftEdge = ((data.messageFlagsHigh & Data::BoundaryDetectionMessengerModifier::kDetectLeftEdge) != 0);
+	_detectRightEdge = ((data.messageFlagsHigh & Data::BoundaryDetectionMessengerModifier::kDetectRightEdge) != 0);
+
+	if (!_send.load(data.send, data.messageFlagsHigh << 16, data.with, data.withSource, data.destination))
+		return false;
+
+	return true;
+}
+
+bool CollisionDetectionMessengerModifier::load(ModifierLoaderContext &context, const Data::CollisionDetectionMessengerModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
 
 	if (!_enableWhen.load(data.enableWhen) || !this->_disableWhen.load(data.disableWhen))
 		return false;
 
-	if (!_sendSpec.load(data.send, data.messageAndModifierFlags, data.with.locationType, data.withSource, data.with.guid, data.destination))
+	if (!_sendSpec.load(data.send, data.messageAndModifierFlags, data.with, data.withSource, data.destination))
 		return false;
 
 	_detectInFront = ((data.messageAndModifierFlags & Data::CollisionDetectionMessengerModifier::kDetectLayerInFront) != 0);
@@ -267,7 +274,7 @@ bool KeyboardMessengerModifier::load(ModifierLoaderContext &context, const Data:
 		break;
 	}
 
-	if (!_sendSpec.load(data.message, data.messageFlagsAndKeyStates, data.with, data.withSource, data.withSourceGUID, data.destination))
+	if (!_sendSpec.load(data.message, data.messageFlagsAndKeyStates, data.with, data.withSource, data.destination))
 		return false;
 
 	return true;
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index d37c1f415c0..f35e9a9c86d 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -55,13 +55,11 @@ private:
 
 struct MessengerSendSpec {
 	MessengerSendSpec();
-	bool load(const Data::Event &dataEvent, uint32 dataMessageFlags, uint16 dataWith, const Common::String &dataWithSourceName, uint32 dataWithSourceGUID, uint32 dataDestination);
+	bool load(const Data::Event &dataEvent, uint32 dataMessageFlags, const Data::MessageDataLocator &dataLocator, const Common::String &dataWithSourceName, uint32 dataDestination);
 
 	Event send;
 	MessageFlags messageFlags;
-	MessageWithType withType;
-	uint32 withSourceGUID;
-	Common::String withSourceName;
+	MessageDataLocator with;
 	uint32 destination; // May be a MessageDestination or GUID
 };
 
@@ -80,12 +78,8 @@ public:
 
 private:
 	Event _executeWhen;
-	MessageWithType _sourceLocType;
-	MessageWithType _targetLocType;
-	uint32 _sourceGUID;
-	uint32 _targetGUID;
-	Common::String _sourceName;
-	Common::String _targetName;
+	MessageDataLocator _sourceLoc;
+	MessageDataLocator _targetLoc;
 };
 
 class DragMotionModifier : public Modifier {
@@ -115,9 +109,7 @@ private:
 	Event _enableWhen;
 	Event _disableWhen;
 
-	MessageWithType _sourceVarLocType;
-	uint32 _sourceVarGUID;
-	Common::String _sourceVarName;
+	MessageDataLocator _sourceVarLoc;
 };
 
 class IfMessengerModifier : public Modifier {
@@ -143,6 +135,32 @@ private:
 	bool _looping;
 };
 
+class BoundaryDetectionMessengerModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::BoundaryDetectionMessengerModifier &data);
+
+private:
+	enum ExitTriggerMode {
+		kExitTriggerExiting,
+		kExitTriggerOnceExited,
+	};
+
+	enum DetectionMode {
+		kContinuous,
+		kOnFirstDetection,
+	};
+
+	Event _enableWhen;
+	Event _disableWhen;
+	ExitTriggerMode _exitTriggerMode;
+	DetectionMode _detectionMode;
+	bool _detectTopEdge;
+	bool _detectBottomEdge;
+	bool _detectLeftEdge;
+	bool _detectRightEdge;
+	MessengerSendSpec _send;
+};
+
 class CollisionDetectionMessengerModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::CollisionDetectionMessengerModifier &data);
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 1a31511fabb..365cb40e2f8 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -49,6 +49,19 @@ bool ColorRGB8::load(const Data::ColorRGB16& color) {
 MessageFlags::MessageFlags() : relay(true), cascade(true), immediate(true) {
 }
 
+
+MessageDataLocator::MessageDataLocator() : locatorType(kMessageDataLocatorTypeNothing), superGroupID(0), guidOrLabelID(0) {
+}
+
+bool MessageDataLocator::load(const Data::MessageDataLocator& data, const Common::String& dataSourceName) {
+	guidOrLabelID = data.guidOrLabelID;
+	superGroupID = data.superGroupID;
+	locatorType = static_cast<MessageDataLocatorType>(data.locationType);
+	sourceName = dataSourceName;
+
+	return true;
+}
+
 Event::Event() : eventType(0), eventInfo(0) {
 }
 
@@ -345,7 +358,6 @@ void Project::loadBootStream(size_t streamIndex) {
 
 	size_t numObjectsLoaded = 0;
 	for (;;) {
-		int64 globalPos = stream.pos() + streamDesc.pos;
 		Common::SharedPtr<Data::DataObject> dataObject;
 		Data::loadDataObject(plugInDataLoaderRegistry, reader, dataObject);
 
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 8c66d47dba3..aeee610ee35 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -65,17 +65,29 @@ struct ColorRGB8 {
 };
 
 struct MessageFlags {
+	MessageFlags();
+
 	bool relay : 1;
 	bool cascade : 1;
 	bool immediate : 1;
+};
 
-	MessageFlags();
+enum MessageDataLocatorType {
+	kMessageDataLocatorTypeNothing = 0,
+	kMessageDataLocatorTypeIncomingData = 0x1b,
+	kMessageDataLocatorTypeVariable = 0x1c,
+	kMessageDataLocatorTypeLabel = 0x1d,
 };
 
-enum MessageWithType {
-	kMessageWithNothing = 0,
-	kMessageWithIncomingData = 0x1b,
-	kMessageWithVariable = 0x1c,
+struct MessageDataLocator {
+	MessageDataLocator(); 
+	bool load(const Data::MessageDataLocator &data, const Common::String &dataSourceName);
+
+	MessageDataLocatorType locatorType;
+	uint32 superGroupID;
+	uint32 guidOrLabelID;
+
+	Common::String sourceName;
 };
 
 enum MessageDestination {


Commit: cb91300e6160905c5286befa64ae8169a4f2acbc
    https://github.com/scummvm/scummvm/commit/cb91300e6160905c5286befa64ae8169a4f2acbc
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Properly recognize dynamic typed values for non-reference message payloads, add media cue modifier.

Changed paths:
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/plugin/standard_data.cpp
    engines/mtropolis/plugin/standard_data.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index bff543dcfd5..9a0e0dd8af3 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -21,6 +21,7 @@
 
 #include "mtropolis/data.h"
 #include "common/debug.h"
+#include "common/memstream.h"
 
 #include <float.h>
 
@@ -212,6 +213,10 @@ bool ColorRGB16::load(DataReader& reader) {
 		return false;
 }
 
+bool IntRange::load(DataReader& reader) {
+	return reader.readS32(min) && reader.readS32(max);
+}
+
 bool XPFloat::load(DataReader &reader) {
 	if (reader.getProjectFormat() == kProjectFormatMacintosh)
 		return reader.readU16(signAndExponent) && reader.readU64(mantissa);
@@ -238,7 +243,7 @@ bool XPFloat::load(DataReader &reader) {
 
 		exponent += 15360;
 
-		signAndExponent = (sign << 15) | exponent;
+		signAndExponent = static_cast<uint16>((sign << 15) | exponent);
 		mantissa = workMantissa << 11;
 
 		return true;
@@ -282,6 +287,129 @@ double XPFloat::toDouble() const {
 	return d;
 }
 
+bool XPFloatVector::load(DataReader& reader) {
+	return angleRadians.load(reader) && magnitude.load(reader);
+}
+
+bool Label::load(DataReader &reader) {
+	return reader.readU32(superGroupID) && reader.readU32(labelID);
+}
+
+bool InternalTypeTaggedValue::load(DataReader& reader) {
+	if (!reader.readU16(type))
+		return false;
+
+	uint8 contents[44];
+	if (!reader.readBytes(contents))
+		return false;
+
+	Common::MemoryReadStreamEndian contentsStream(contents, sizeof(contents), reader.isBigEndian());
+
+	DataReader valueReader(contentsStream, reader.getProjectFormat());
+
+	switch (type) {
+	case kNull:
+	case kIncomingData:
+	case kString:	// Not a bug - string data is external!
+		break;
+	case kInteger:
+		if (!valueReader.readS32(value.asInteger))
+			return false;
+		break;
+	case kPoint:
+		if (!value.asPoint.load(valueReader))
+			return false;
+		break;
+	case kIntegerRange:
+		if (!value.asIntegerRange.load(valueReader))
+			return false;
+		break;
+	case kFloat:
+		if (!value.asFloat.load(valueReader))
+			return false;
+		break;
+	case kBool:
+		if (!valueReader.readU8(value.asBool))
+			return false;
+		break;
+	case kVariableReference:
+		if (!valueReader.readU32(value.asVariableReference.unknown) || !valueReader.readU32(value.asVariableReference.guid))
+			return false;
+		break;
+	case kLabel:
+		if (!value.asLabel.load(valueReader))
+			return false;
+		break;
+	default:
+		warning("Unknown tagged value type %x", type);
+		return false;
+	}
+
+	return true;
+}
+
+bool PlugInTypeTaggedValue::load(DataReader &reader) {
+	if (!reader.readU16(type))
+		return false;
+
+	switch (type) {
+	case kNull:
+	case kIncomingData:
+		break;
+	case kInteger:
+		if (!reader.readS32(value.asInt))
+			return false;
+		break;
+	case kIntegerRange:
+		if (!value.asIntRange.load(reader))
+			return false;
+		break;
+	case kFloat:
+		if (!value.asFloat.load(reader))
+			return false;
+		break;
+	case kBoolean:
+		if (!reader.readU16(value.asBoolean))
+			return false;
+		break;
+	case kEvent:
+		if (!value.asEvent.load(reader))
+			return false;
+		break;
+	case kLabel:
+		// This is the opposite of internal vars...
+		if (!reader.readU32(value.asLabel.labelID) || !reader.readU32(value.asLabel.superGroupID))
+			return false;
+		break;
+	case kString: {
+			uint32 length1;
+			uint32 length2;
+			if (!reader.readU32(length1) || !reader.readU32(length2))
+				return false;
+			if (length1 != length2)	// ???
+				return false;
+			if (!reader.readTerminatedStr(this->str, length1))
+				return false;
+		} break;
+	case kVariableReference: {
+			uint32 extraDataSize;
+			if (!reader.readU32(value.asVarRefGUID) || !reader.readU32(extraDataSize))
+				return false;
+
+			if (extraDataSize > 0) {
+				this->extraData.resize(extraDataSize);
+				if (!reader.read(&extraData[0], extraDataSize))
+					return false;
+			}
+		} break;
+	default:
+		warning("Unknown plug-in value type %x", type);
+		return false;
+	}
+
+	return true;
+}
+
 DataObject::DataObject() : _type(DataObjectTypes::kUnknown), _revision(0) {
 }
 
@@ -537,13 +665,6 @@ DataReadErrorCode MiniscriptModifier::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
-bool MessageDataLocator::load(DataReader& reader) {
-	if (!reader.readU16(locationType) || !reader.readU32(superGroupID) || !reader.readU32(guidOrLabelID) || !reader.readBytes(unknown2))
-		return false;
-
-	return true;
-}
-
 DataReadErrorCode MessengerModifier::load(DataReader &reader) {
 	if (_revision != 0x3ea)
 		return kDataReadErrorUnsupportedRevision;
@@ -553,10 +674,10 @@ DataReadErrorCode MessengerModifier::load(DataReader &reader) {
 
 	// Unlike most cases, the "when" event is split in half in this case
 	if (!reader.readU32(messageFlags) || !reader.readU32(when.eventID) || !send.load(reader) || !reader.readU16(unknown14) || !reader.readU32(destination)
-		|| !reader.readBytes(unknown11) || !with.load(reader) || !reader.readU32(when.eventInfo) || !reader.readU8(withSourceLength) || !reader.readU8(unknown13))
+		|| !reader.readBytes(unknown11) || !with.load(reader) || !reader.readU32(when.eventInfo) || !reader.readU8(withSourceLength) || !reader.readU8(withStringLength))
 		return kDataReadErrorReadFailed;
 
-	if (withSourceLength > 0 && !reader.readNonTerminatedStr(withSourceName, withSourceLength))
+	if (!reader.readNonTerminatedStr(withSource, withSourceLength) || !reader.readNonTerminatedStr(withString, withStringLength))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
@@ -566,9 +687,12 @@ DataReadErrorCode SetModifier::load(DataReader &reader) {
 	if (_revision != 0x3e8)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !executeWhen.load(reader) || !sourceLocator.load(reader) || !targetLocator.load(reader)
-		|| !reader.readU8(unknown3) || !reader.readU8(sourceNameLength) || !reader.readU8(targetNameLength) || !reader.readBytes(unknown4)
-		|| !reader.readNonTerminatedStr(sourceName, sourceNameLength) || !reader.readNonTerminatedStr(targetName, targetNameLength))
+	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !executeWhen.load(reader)
+		|| !source.load(reader) || !target.load(reader) || !reader.readU8(unknown3)
+		|| !reader.readU8(sourceNameLength) || !reader.readU8(targetNameLength) || !reader.readU8(sourceStringLength)
+		|| !reader.readU8(targetStringLength)  || !reader.readU8(unknown4) || !reader.readNonTerminatedStr(sourceName, sourceNameLength)
+		|| !reader.readNonTerminatedStr(targetName, targetNameLength) || !reader.readNonTerminatedStr(sourceString, sourceNameLength)
+		|| !reader.readNonTerminatedStr(targetString, targetStringLength))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
@@ -614,9 +738,10 @@ DataReadErrorCode VectorMotionModifier::load(DataReader &reader) {
 	if (!modHeader.load(reader))
 		return kDataReadErrorReadFailed;
 
-	if (!enableWhen.load(reader) || !disableWhen.load(reader) || !varSource.load(reader)
-		|| !reader.readU16(unknown1) || !reader.readU8(varSourceNameLength) || !reader.readU8(unknown2)
-		|| !reader.readNonTerminatedStr(varSourceName, varSourceNameLength))
+	if (!enableWhen.load(reader) || !disableWhen.load(reader) || !vec.load(reader)
+		|| !reader.readU16(unknown1) || !reader.readU8(vecSourceLength) || !reader.readU8(vecStringLength)
+		|| !reader.readNonTerminatedStr(vecSource, vecSourceLength)
+		/*|| !reader.readNonTerminatedStr(vecString, vecStringLength)*/)	// mTropolis bug!
 		return kDataReadErrorNone;
 
 	return kDataReadErrorNone;
@@ -628,10 +753,10 @@ DataReadErrorCode IfMessengerModifier::load(DataReader &reader) {
 
 	if (!modHeader.load(reader) || !reader.readU32(messageFlags) || !when.load(reader) || !send.load(reader)
 		|| !reader.readU16(unknown6) || !reader.readU32(destination) || !reader.readBytes(unknown7) || !with.load(reader)
-		|| !reader.readBytes(unknown9) || !reader.readU8(withSourceLength) || !reader.readU8(unknown10))
+		|| !reader.readBytes(unknown9) || !reader.readU8(withSourceLength) || !reader.readU8(withStringLength))
 		return kDataReadErrorReadFailed;
 
-	if (withSourceLength > 0 && !reader.readNonTerminatedStr(withSource, withSourceLength))
+	if (!reader.readNonTerminatedStr(withSource, withSourceLength) || !reader.readNonTerminatedStr(withString, withStringLength))
 		return kDataReadErrorReadFailed;
 
 	if (!program.load(reader))
@@ -652,7 +777,8 @@ DataReadErrorCode TimerMessengerModifier::load(DataReader &reader) {
 		|| !reader.readBytes(unknown4) || !with.load(reader) || !reader.readU8(unknown5)
 		|| !reader.readU8(minutes) || !reader.readU8(seconds) || !reader.readU8(hundredthsOfSeconds)
 		|| !reader.readU32(unknown6) || !reader.readU32(unknown7) || !reader.readBytes(unknown8)
-		|| !reader.readU8(withSourceLength) || !reader.readU8(unknown9) || !reader.readNonTerminatedStr(withSource, withSourceLength))
+		|| !reader.readU8(withSourceLength) || !reader.readU8(withStringLength) || !reader.readNonTerminatedStr(withSource, withSourceLength)
+		 || !reader.readNonTerminatedStr(withString, withStringLength))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
@@ -668,7 +794,7 @@ DataReadErrorCode BoundaryDetectionMessengerModifier::load(DataReader &reader) {
 	if (!reader.readU16(messageFlagsHigh) || !enableWhen.load(reader) || !disableWhen.load(reader)
 		|| !send.load(reader) || !reader.readU16(unknown2) || !reader.readU32(destination)
 		|| !reader.readBytes(unknown3) || !with.load(reader) || !reader.readU8(withSourceLength)
-		|| !reader.readU8(unknown4) || !reader.readNonTerminatedStr(withSource, withSourceLength))
+		|| !reader.readU8(withStringLength) || !reader.readNonTerminatedStr(withSource, withSourceLength) || !reader.readNonTerminatedStr(withString, withStringLength))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
@@ -684,7 +810,7 @@ DataReadErrorCode CollisionDetectionMessengerModifier::load(DataReader &reader)
 	if (!reader.readU32(messageAndModifierFlags) || !enableWhen.load(reader) || !disableWhen.load(reader)
 		|| !send.load(reader) || !reader.readU16(unknown2) || !reader.readU32(destination)
 		|| !reader.readBytes(unknown3) || !with.load(reader) || !reader.readU8(withSourceLength)
-		|| !reader.readU8(unknown4) || !reader.readNonTerminatedStr(withSource, withSourceLength))
+		|| !reader.readU8(withStringLength) || !reader.readNonTerminatedStr(withSource, withSourceLength)  || !reader.readNonTerminatedStr(withString, withStringLength))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
@@ -698,10 +824,10 @@ DataReadErrorCode KeyboardMessengerModifier::load(DataReader &reader) {
 		|| !reader.readU16(keyModifiers) || !reader.readU8(keycode) || !reader.readBytes(unknown4)
 		|| !message.load(reader) || !reader.readU16(unknown7) || !reader.readU32(destination)
 		|| !reader.readBytes(unknown9) || !with.load(reader) || !reader.readU8(withSourceLength)
-		|| !reader.readU8(unknown14))
+		|| !reader.readU8(withStringLength))
 		return kDataReadErrorReadFailed;
 
-	if (withSourceLength > 0 && !reader.readNonTerminatedStr(withSource, withSourceLength))
+	if (!reader.readNonTerminatedStr(withSource, withSourceLength) || !reader.readNonTerminatedStr(withString, withStringLength))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
@@ -784,7 +910,7 @@ DataReadErrorCode IntegerRangeVariableModifier::load(DataReader &reader) {
 	if (_revision != 0x3e8)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !reader.readS32(min) || !reader.readS32(max))
+	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !range.load(reader))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
@@ -794,7 +920,7 @@ DataReadErrorCode VectorVariableModifier::load(DataReader &reader) {
 	if (_revision != 0x3e8)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !angleRadians.load(reader) || !magnitude.load(reader))
+	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !this->vector.load(reader))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
@@ -895,6 +1021,8 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 		return kDataReadErrorReadFailed;
 	}
 
+	debug(4, "Loading data object type %x", static_cast<int>(type));
+
 	DataObject *dataObject = nullptr;
 	switch (type) {
 	case DataObjectTypes::kProjectHeader:
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index 1bb73392f84..a2ad719ad92 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -208,12 +208,111 @@ struct ColorRGB16 {
 	uint16 blue;
 };
 
+struct IntRange {
+	bool load(DataReader &reader);
+
+	int32 min;
+	int32 max;
+};
+
 struct XPFloat {
 	bool load(DataReader &reader);
 	double toDouble() const;
 
-	uint64_t mantissa;
-	uint16_t signAndExponent;
+	uint64 mantissa;
+	uint16 signAndExponent;
+};
+
+struct XPFloatVector {
+	bool load(DataReader &reader);
+
+	XPFloat angleRadians;
+	XPFloat magnitude;
+};
+
+struct Label {
+	bool load(DataReader &reader);
+
+	uint32 superGroupID;
+	uint32 labelID;
+};
+
+// mTropolis uses two separate type-tagged value formats.
+//
+// InternalTypeTaggedValue is used by internal modifiers for messenger payloads and set modifiers
+// and seems to match Miniscript ops too.
+// InternalTypeTaggedValue is always 44 bytes in size and stores string data elsewhere in the containing structure.
+//
+// PlugInTypeTaggedValue is used by plug-ins and is fully self-contained.
+//
+// If you change something here, remember to update DynamicValue::load
+struct InternalTypeTaggedValue {
+	enum TypeCode {
+		kNull = 0x00,
+		kInteger = 0x01,
+		kString = 0x0d, // String data is stored externally from the value
+		kPoint = 0x10,
+		kIntegerRange = 0x11,
+		kFloat = 0x15,
+		kBool = 0x1a,
+		kIncomingData = 0x1b,
+		kVariableReference = 0x1c,
+		kLabel = 0x1d,
+	};
+
+	struct VariableReference {
+		uint32 unknown;
+		uint32 guid;
+	};
+
+	union ValueUnion {
+		uint8 asBool;
+		XPFloat asFloat;
+		int32 asInteger;
+		IntRange asIntegerRange;
+		VariableReference asVariableReference;
+		Label asLabel;
+		Point asPoint;
+	};
+
+	uint16 type;
+	ValueUnion value;
+
+	bool load(DataReader &reader);
+};
+
+struct PlugInTypeTaggedValue : public Common::NonCopyable {
+	enum TypeCode {
+		kNull = 0x00,
+		kInteger = 0x01,
+		kIntegerRange = 0xb,
+		kFloat = 0xf,
+		kBoolean = 0x14,
+		kEvent = 0x17,
+		kLabel = 0x64,
+		kString = 0x66,
+		kIncomingData = 0x6e,
+		kVariableReference = 0x73,	// Has extra data
+	};
+
+	union ValueUnion {
+		int32 asInt;
+		Point asPoint;
+		IntRange asIntRange;
+		XPFloat asFloat;
+		uint16 asBoolean;
+		Event asEvent;
+		Label asLabel;
+		uint32 asVarRefGUID;
+	};
+
+	uint16 type;
+	ValueUnion value;
+
+	Common::String str;
+	Common::Array<uint8> extraData;
+
+	bool load(DataReader &reader);
 };
 
 class DataObject : public Common::NonCopyable {
@@ -433,14 +532,6 @@ enum MessageFlags {
 	kMessageFlagNoImmediate = 0x80000000,
 };
 
-struct MessageDataLocator {
-	uint16 locationType;
-	uint32 superGroupID;
-	uint32 guidOrLabelID;
-	uint8 unknown2[36];
-
-	bool load(DataReader &reader);
-};
 
 struct MessengerModifier : public DataObject {
 	TypicalModifierHeader modHeader;
@@ -451,11 +542,12 @@ struct MessengerModifier : public DataObject {
 	uint16 unknown14;
 	uint32 destination;
 	uint8 unknown11[10];
-	MessageDataLocator with;
+	InternalTypeTaggedValue with;
 	uint8 withSourceLength;
-	uint8 unknown13;
+	uint8 withStringLength;
 
-	Common::String withSourceName;
+	Common::String withSource;
+	Common::String withString;
 
 protected:
 	DataReadErrorCode load(DataReader &reader) override;
@@ -466,15 +558,19 @@ struct SetModifier : public DataObject {
 
 	uint8 unknown1[4];
 	Event executeWhen;
-	MessageDataLocator sourceLocator;
-	MessageDataLocator targetLocator;
+	InternalTypeTaggedValue source;
+	InternalTypeTaggedValue target;
 	uint8 unknown3;
 	uint8 sourceNameLength;
 	uint8 targetNameLength;
-	uint8 unknown4[3];
+	uint8 sourceStringLength;
+	uint8 targetStringLength;
+	uint8 unknown4;
 
 	Common::String sourceName;
 	Common::String targetName;
+	Common::String sourceString;
+	Common::String targetString;
 
 protected:
 	DataReadErrorCode load(DataReader &reader) override;
@@ -525,12 +621,13 @@ struct VectorMotionModifier : public DataObject {
 
 	Event enableWhen;
 	Event disableWhen;
-	MessageDataLocator varSource;
+	InternalTypeTaggedValue vec;
 	uint16 unknown1;
-	uint8 varSourceNameLength;
-	uint8 unknown2;
+	uint8 vecSourceLength;
+	uint8 vecStringLength;
 
-	Common::String varSourceName;
+	Common::String vecSource;
+	Common::String vecString;
 
 protected:
 	DataReadErrorCode load(DataReader &reader) override;
@@ -545,13 +642,14 @@ struct IfMessengerModifier : public DataObject {
 	uint16 unknown6;
 	uint32 destination;
 	uint8 unknown7[10];
-	MessageDataLocator with;
+	InternalTypeTaggedValue with;
 	uint8 unknown9[10];
 	uint8 withSourceLength;
-	uint8 unknown10;
+	uint8 withStringLength;
 	MiniscriptProgram program;
 
 	Common::String withSource;
+	Common::String withString;
 
 protected:
 	DataReadErrorCode load(DataReader &reader) override;
@@ -571,7 +669,7 @@ struct TimerMessengerModifier : public DataObject {
 	uint16 unknown2;
 	uint32 destination;
 	uint8 unknown4[10];
-	MessageDataLocator with;
+	InternalTypeTaggedValue with;
 	uint8 unknown5;
 	uint8 minutes;
 	uint8 seconds;
@@ -580,9 +678,10 @@ struct TimerMessengerModifier : public DataObject {
 	uint32 unknown7;
 	uint8 unknown8[10];
 	uint8 withSourceLength;
-	uint8 unknown9;
+	uint8 withStringLength;
 
 	Common::String withSource;
+	Common::String withString;
 
 protected:
 	DataReadErrorCode load(DataReader &reader) override;
@@ -606,11 +705,12 @@ struct BoundaryDetectionMessengerModifier : public DataObject {
 	uint16 unknown2;
 	uint32 destination;
 	uint8 unknown3[10];
-	MessageDataLocator with;
+	InternalTypeTaggedValue with;
 	uint8 withSourceLength;
-	uint8 unknown4;
+	uint8 withStringLength;
 
 	Common::String withSource;
+	Common::String withString;
 
 protected:
 	DataReadErrorCode load(DataReader &reader) override;
@@ -639,11 +739,12 @@ struct CollisionDetectionMessengerModifier : public DataObject {
 	uint16 unknown2;
 	uint32 destination;
 	uint8 unknown3[10];
-	MessageDataLocator with;
+	InternalTypeTaggedValue with;
 	uint8 withSourceLength;
-	uint8 unknown4;
+	uint8 withStringLength;
 
 	Common::String withSource;
+	Common::String withString;
 
 protected:
 	DataReadErrorCode load(DataReader &reader) override;
@@ -693,11 +794,12 @@ struct KeyboardMessengerModifier : public DataObject {
 	uint16 unknown7;
 	uint32 destination;
 	uint8 unknown9[10];
-	MessageDataLocator with;
+	InternalTypeTaggedValue with;
 	uint8 withSourceLength;
-	uint8 unknown14;
+	uint8 withStringLength;
 
 	Common::String withSource;
+	Common::String withString;
 
 protected:
 	DataReadErrorCode load(DataReader &reader) override;
@@ -792,8 +894,7 @@ protected:
 struct IntegerRangeVariableModifier : public DataObject {
 	TypicalModifierHeader modHeader;
 	uint8 unknown1[4];
-	int32 min;
-	int32 max;
+	IntRange range;
 
 protected:
 	DataReadErrorCode load(DataReader &reader) override;
@@ -802,8 +903,7 @@ protected:
 struct VectorVariableModifier : public DataObject {
 	TypicalModifierHeader modHeader;
 	uint8 unknown1[4];
-	XPFloat angleRadians;
-	XPFloat magnitude;
+	XPFloatVector vector;
 
 protected:
 	DataReadErrorCode load(DataReader &reader) override;
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 599f53e5367..bef3fe7bd02 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -68,30 +68,11 @@ bool MiniscriptModifier::load(ModifierLoaderContext &context, const Data::Minisc
 	return true;
 }
 
-MessengerSendSpec::MessengerSendSpec() : destination(0) {
-}
-
-bool MessengerSendSpec::load(const Data::Event &dataEvent, uint32 dataMessageFlags, const Data::MessageDataLocator &dataLocator, const Common::String &dataWithSourceName, uint32 dataDestination) {
-	messageFlags.relay = ((dataMessageFlags & 0x20000000) == 0);
-	messageFlags.cascade = ((dataMessageFlags & 0x40000000) == 0);
-	messageFlags.immediate = ((dataMessageFlags & 0x80000000) == 0);
-
-	if (!this->send.load(dataEvent))
-		return false;
-
-	if (!this->with.load(dataLocator, dataWithSourceName))
-		return false;
-
-	this->destination = dataDestination;
-
-	return true;
-}
-
 bool MessengerModifier::load(ModifierLoaderContext &context, const Data::MessengerModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
 
-	if (!_when.load(data.when) || !_sendSpec.load(data.send, data.messageFlags, data.with, data.withSourceName, data.destination))
+	if (!_when.load(data.when) || !_sendSpec.load(data.send, data.messageFlags, data.with, data.withSource, data.withString, data.destination))
 		return false;
 
 	return true;
@@ -101,7 +82,7 @@ bool SetModifier::load(ModifierLoaderContext &context, const Data::SetModifier &
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
 
-	if (!_executeWhen.load(data.executeWhen) || !_sourceLoc.load(data.sourceLocator, data.sourceName) || !_targetLoc.load(data.targetLocator, data.targetName))
+	if (!_executeWhen.load(data.executeWhen) || !_source.load(data.source, data.sourceName, data.sourceString) || !_target.load(data.target, data.targetName, data.targetString))
 		return false;
 
 	return true;
@@ -147,7 +128,7 @@ bool VectorMotionModifier::load(ModifierLoaderContext &context, const Data::Vect
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
 
-	if (!_enableWhen.load(data.enableWhen) || !_disableWhen.load(data.disableWhen) || !_sourceVarLoc.load(data.varSource, data.varSourceName))
+	if (!_enableWhen.load(data.enableWhen) || !_disableWhen.load(data.disableWhen) || !_vec.load(data.vec, data.vecSource, data.vecString))
 		return false;
 
 	return true;
@@ -157,7 +138,7 @@ bool IfMessengerModifier::load(ModifierLoaderContext &context, const Data::IfMes
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
 
-	if (!_when.load(data.when) || !_sendSpec.load(data.send, data.messageFlags, data.with, data.withSource, data.destination))
+	if (!_when.load(data.when) || !_sendSpec.load(data.send, data.messageFlags, data.with, data.withSource, data.withString, data.destination))
 		return false;
 
 	_program = MiniscriptParser::parse(data.program);
@@ -174,7 +155,7 @@ bool TimerMessengerModifier::load(ModifierLoaderContext &context, const Data::Ti
 	if (!_executeWhen.load(data.executeWhen) || !this->_terminateWhen.load(data.terminateWhen))
 		return false;
 
-	if (!_sendSpec.load(data.send, data.messageAndTimerFlags, data.with, data.withSource, data.destination))
+	if (!_sendSpec.load(data.send, data.messageAndTimerFlags, data.with, data.withSource, data.withString, data.destination))
 		return false;
 
 	_milliseconds = data.minutes * (60 * 1000) + data.seconds * (1000) + data.hundredthsOfSeconds * 10;
@@ -198,7 +179,7 @@ bool BoundaryDetectionMessengerModifier::load(ModifierLoaderContext &context, co
 	_detectLeftEdge = ((data.messageFlagsHigh & Data::BoundaryDetectionMessengerModifier::kDetectLeftEdge) != 0);
 	_detectRightEdge = ((data.messageFlagsHigh & Data::BoundaryDetectionMessengerModifier::kDetectRightEdge) != 0);
 
-	if (!_send.load(data.send, data.messageFlagsHigh << 16, data.with, data.withSource, data.destination))
+	if (!_send.load(data.send, data.messageFlagsHigh << 16, data.with, data.withSource, data.withString, data.destination))
 		return false;
 
 	return true;
@@ -211,7 +192,7 @@ bool CollisionDetectionMessengerModifier::load(ModifierLoaderContext &context, c
 	if (!_enableWhen.load(data.enableWhen) || !this->_disableWhen.load(data.disableWhen))
 		return false;
 
-	if (!_sendSpec.load(data.send, data.messageAndModifierFlags, data.with, data.withSource, data.destination))
+	if (!_sendSpec.load(data.send, data.messageAndModifierFlags, data.with, data.withSource, data.withString, data.destination))
 		return false;
 
 	_detectInFront = ((data.messageAndModifierFlags & Data::CollisionDetectionMessengerModifier::kDetectLayerInFront) != 0);
@@ -274,7 +255,7 @@ bool KeyboardMessengerModifier::load(ModifierLoaderContext &context, const Data:
 		break;
 	}
 
-	if (!_sendSpec.load(data.message, data.messageFlagsAndKeyStates, data.with, data.withSource, data.destination))
+	if (!_sendSpec.load(data.message, data.messageFlagsAndKeyStates, data.with, data.withSource, data.withString, data.destination))
 		return false;
 
 	return true;
@@ -353,8 +334,8 @@ bool IntegerRangeVariableModifier::load(ModifierLoaderContext& context, const Da
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
 
-	_min = data.min;
-	_max = data.max;
+	if (!_range.load(data.range))
+		return false;
 
 	return true;
 }
@@ -363,8 +344,8 @@ bool VectorVariableModifier::load(ModifierLoaderContext &context, const Data::Ve
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
 
-	_angleRadians = data.angleRadians.toDouble();
-	_magnitude = data.magnitude.toDouble();
+	_vector.angleRadians = data.vector.angleRadians.toDouble();
+	_vector.magnitude = data.vector.magnitude.toDouble();
 
 	return true;
 }
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index f35e9a9c86d..65e1f922152 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -53,16 +53,6 @@ private:
 	Common::SharedPtr<MiniscriptProgram> _program;
 };
 
-struct MessengerSendSpec {
-	MessengerSendSpec();
-	bool load(const Data::Event &dataEvent, uint32 dataMessageFlags, const Data::MessageDataLocator &dataLocator, const Common::String &dataWithSourceName, uint32 dataDestination);
-
-	Event send;
-	MessageFlags messageFlags;
-	MessageDataLocator with;
-	uint32 destination; // May be a MessageDestination or GUID
-};
-
 class MessengerModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::MessengerModifier &data);
@@ -78,8 +68,8 @@ public:
 
 private:
 	Event _executeWhen;
-	MessageDataLocator _sourceLoc;
-	MessageDataLocator _targetLoc;
+	DynamicValue _source;
+	DynamicValue _target;
 };
 
 class DragMotionModifier : public Modifier {
@@ -109,7 +99,7 @@ private:
 	Event _enableWhen;
 	Event _disableWhen;
 
-	MessageDataLocator _sourceVarLoc;
+	DynamicValue _vec;
 };
 
 class IfMessengerModifier : public Modifier {
@@ -320,8 +310,7 @@ public:
 	bool load(ModifierLoaderContext &context, const Data::IntegerRangeVariableModifier &data);
 
 private:
-	int32 _min;
-	int32 _max;
+	IntRange _range;
 };
 
 class VectorVariableModifier : public Modifier {
@@ -329,8 +318,7 @@ public:
 	bool load(ModifierLoaderContext &context, const Data::VectorVariableModifier &data);
 
 private:
-	double _angleRadians;
-	double _magnitude;
+	AngleMagVector _vector;
 };
 
 class PointVariableModifier : public Modifier {
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 593d0b7250c..65615529ef6 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -39,12 +39,44 @@ bool STransCtModifier::load(const PlugInModifierLoaderContext& context, const Da
 	return true;
 }
 
-StandardPlugIn::StandardPlugIn() : _cursorModifierFactory(this), _sTransCtModifierFactory(this) {
+bool MediaCueMessengerModifier::load(const PlugInModifierLoaderContext& context, const Data::Standard::MediaCueMessengerModifier& data) {
+	if (data.enableWhen.type != Data::PlugInTypeTaggedValue::kEvent)
+		return false;
+
+	_enableWhen.load(data.enableWhen.value.asEvent);
+
+	if (data.disableWhen.type != Data::PlugInTypeTaggedValue::kEvent)
+		return false;
+
+	_disableWhen.load(data.disableWhen.value.asEvent);
+
+	if (data.triggerTiming.type != Data::PlugInTypeTaggedValue::kInteger)
+		return false;
+
+	_triggerTiming = static_cast<TriggerTiming>(data.triggerTiming.value.asInt);
+
+	if (data.nonStandardMessageFlags.type != Data::PlugInTypeTaggedValue::kInteger)
+		return false;
+
+	int32 msgFlags = data.nonStandardMessageFlags.value.asInt;
+
+	MessageFlags messageFlags;
+	messageFlags.immediate = ((msgFlags & Data::Standard::MediaCueMessengerModifier::kMessageFlagImmediate) != 0);
+	messageFlags.cascade = ((msgFlags & Data::Standard::MediaCueMessengerModifier::kMessageFlagCascade) != 0);
+	messageFlags.relay = ((msgFlags & Data::Standard::MediaCueMessengerModifier::kMessageFlagRelay) != 0);
+	if (!_send.load(data.sendEvent, messageFlags, data.with, data.destination))
+		return false;
+
+	return true;
+}
+
+StandardPlugIn::StandardPlugIn() : _cursorModifierFactory(this), _sTransCtModifierFactory(this), _mediaCueModifierFactory(this) {
 }
 
 void StandardPlugIn::registerModifiers(IPlugInModifierRegistrar *registrar) const {
 	registrar->registerPlugInModifier("CursorMod", &_cursorModifierFactory);
 	registrar->registerPlugInModifier("STransCt", &_sTransCtModifierFactory);
+	registrar->registerPlugInModifier("MediaCue", &_mediaCueModifierFactory);
 }
 
 } // End of namespace Standard
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index b7a93cec287..343494f5c78 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -22,6 +22,7 @@
 #ifndef MTROPOLIS_PLUGIN_STANDARD_H
 #define MTROPOLIS_PLUGIN_STANDARD_H
 
+#include "mtropolis/modifiers.h"
 #include "mtropolis/modifier_factory.h"
 #include "mtropolis/runtime.h"
 #include "mtropolis/plugin/standard_data.h"
@@ -48,6 +49,23 @@ public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::STransCtModifier &data);
 };
 
+class MediaCueMessengerModifier : public Modifier {
+public:
+	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::MediaCueMessengerModifier &data);
+
+private:
+	enum TriggerTiming {
+		kTriggerTimingStart = 0,
+		kTriggerTimingDuring = 1,
+		kTriggerTimingEnd = 2,
+	};
+
+	Event _enableWhen;
+	Event _disableWhen;
+	TriggerTiming _triggerTiming;
+	MessengerSendSpec _send;
+};
+
 class StandardPlugIn : public MTropolis::PlugIn {
 public:
 	StandardPlugIn();
@@ -57,6 +75,7 @@ public:
 private:
 	PlugInModifierFactory<CursorModifier, Data::Standard::CursorModifier> _cursorModifierFactory;
 	PlugInModifierFactory<STransCtModifier, Data::Standard::STransCtModifier> _sTransCtModifierFactory;
+	PlugInModifierFactory<MediaCueMessengerModifier, Data::Standard::MediaCueMessengerModifier> _mediaCueModifierFactory;
 };
 
 } // End of namespace Standard
diff --git a/engines/mtropolis/plugin/standard_data.cpp b/engines/mtropolis/plugin/standard_data.cpp
index 5e6364e2b7e..350b3b024f6 100644
--- a/engines/mtropolis/plugin/standard_data.cpp
+++ b/engines/mtropolis/plugin/standard_data.cpp
@@ -51,6 +51,19 @@ DataReadErrorCode STransCtModifier::load(const PlugInModifier &prefix, DataReade
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode MediaCueMessengerModifier::load(const PlugInModifier &prefix, DataReader &reader) {
+	if (prefix.plugInRevision != 1)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!enableWhen.load(reader) || !disableWhen.load(reader) || !sendEvent.load(reader)
+		|| !nonStandardMessageFlags.load(reader) || !reader.readU16(unknown1) || !reader.readU32(destination)
+		|| !reader.readU32(unknown2) || !with.load(reader) || !executeAt.load(reader)
+		|| !triggerTiming.load(reader))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 } // End of namespace Standard
 
 } // End of namespace Data
diff --git a/engines/mtropolis/plugin/standard_data.h b/engines/mtropolis/plugin/standard_data.h
index 38a029514fa..7260136b6b3 100644
--- a/engines/mtropolis/plugin/standard_data.h
+++ b/engines/mtropolis/plugin/standard_data.h
@@ -65,6 +65,34 @@ protected:
 	DataReadErrorCode load(const PlugInModifier &prefix, DataReader &reader) override;
 };
 
+struct MediaCueMessengerModifier : public PlugInModifierData {
+	enum MessageFlags {
+		kMessageFlagImmediate = 0x1,
+		kMessageFlagCascade = 0x2,
+		kMessageFlagRelay = 0x3,
+	};
+
+	enum TriggerTiming {
+		kTriggerTimingStart = 0,
+		kTriggerTimingDuring = 1,
+		kTriggerTimingEnd = 2,
+	};
+
+	PlugInTypeTaggedValue enableWhen;
+	PlugInTypeTaggedValue disableWhen;
+	PlugInTypeTaggedValue sendEvent;
+	PlugInTypeTaggedValue nonStandardMessageFlags;	// int type, non-standard
+	uint16 unknown1;
+	uint32 destination;
+	uint32 unknown2;
+	PlugInTypeTaggedValue with;
+	PlugInTypeTaggedValue executeAt;	// May validly be a label, variable, integer, or integer range
+	PlugInTypeTaggedValue triggerTiming;	// int type
+
+protected:
+	DataReadErrorCode load(const PlugInModifier &prefix, DataReader &reader) override;
+};
+
 } // End of namespace Standard
 
 } // End of namespace Data
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 365cb40e2f8..401dd6b253e 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -24,12 +24,20 @@
 #include "mtropolis/vthread.h"
 #include "mtropolis/modifier_factory.h"
 
+#include "common/debug.h"
 #include "common/file.h"
 #include "common/substream.h"
 
 
 namespace MTropolis {
 
+bool Point16::load(const Data::Point& point) {
+	x = point.x;
+	y = point.y;
+
+	return true;
+}
+
 bool Rect16::load(const Data::Rect& rect) {
 	top = rect.top;
 	left = rect.left;
@@ -39,30 +47,333 @@ bool Rect16::load(const Data::Rect& rect) {
 	return true;
 }
 
+bool IntRange::load(const Data::IntRange& range) {
+	max = range.max;
+	min = range.min;
+
+	return true;
+}
+
+bool Label::load(const Data::Label& label) {
+	id = label.labelID;
+	superGroupID = label.superGroupID;
+
+	return true;
+}
+
 bool ColorRGB8::load(const Data::ColorRGB16& color) {
 	this->r = (color.red * 510 + 1) / 131070;
 	this->g = (color.green * 510 + 1) / 131070;
 	this->b = (color.blue * 510 + 1) / 131070;
+
 	return true;
 }
 
 MessageFlags::MessageFlags() : relay(true), cascade(true), immediate(true) {
 }
 
+DynamicValue::DynamicValue() : _type(kTypeNull) {
+}
+
+DynamicValue::DynamicValue(const DynamicValue &other) : _type(kTypeNull) {
+	initFromOther(other);
+}
+
+DynamicValue::~DynamicValue() {
+	clear();
+}
+
+bool DynamicValue::load(const Data::InternalTypeTaggedValue &data, const Common::String &varSource, const Common::String &varString) {
+	switch (data.type) {
+	case Data::InternalTypeTaggedValue::kNull:
+		_type = kTypeNull;
+		break;
+	case Data::InternalTypeTaggedValue::kIncomingData:
+		_type = kTypeIncomingData;
+		break;
+	case Data::InternalTypeTaggedValue::kInteger:
+		_type = kTypeInteger;
+		_value.asInt = data.value.asInteger;
+		break;
+	case Data::InternalTypeTaggedValue::kString:
+		_type = kTypeString;
+		_str = varString;
+		break;
+	case Data::InternalTypeTaggedValue::kPoint:
+		_type = kTypePoint;
+		if (!_value.asPoint.load(data.value.asPoint))
+			return false;
+		break;
+	case Data::InternalTypeTaggedValue::kIntegerRange:
+		_type = KTypeIntegerRange;
+		if (!_value.asIntRange.load(data.value.asIntegerRange))
+			return false;
+		break;
+	case Data::InternalTypeTaggedValue::kFloat:
+		_type = kTypeFloat;
+		_value.asFloat = data.value.asFloat.toDouble();
+		break;
+	case Data::InternalTypeTaggedValue::kBool:
+		_type = kTypeBoolean;
+		_value.asBool = (data.value.asBool != 0);
+		break;
+	case Data::InternalTypeTaggedValue::kVariableReference:
+		_type = kTypeVariableReference;
+		_value.asVarReference.guid = data.value.asVariableReference.guid;
+		_value.asVarReference.source = &_str;
+		_str = varSource;
+		break;
+	case Data::InternalTypeTaggedValue::kLabel:
+		_type = kTypeLabel;
+		if (!_value.asLabel.load(data.value.asLabel))
+			return false;
+		break;
+	default:
+		assert(false);
+		return false;
+	}
+
+	return true;
+}
+
+bool DynamicValue::load(const Data::PlugInTypeTaggedValue& data) {
+	switch (data.type) {
+	case Data::PlugInTypeTaggedValue::kNull:
+		_type = kTypeNull;
+		break;
+	case Data::PlugInTypeTaggedValue::kIncomingData:
+		_type = kTypeIncomingData;
+		break;
+	case Data::PlugInTypeTaggedValue::kInteger:
+		_type = kTypeInteger;
+		_value.asInt = data.value.asInt;
+		break;
+	case Data::PlugInTypeTaggedValue::kIntegerRange:
+		_type = KTypeIntegerRange;
+		if (!_value.asIntRange.load(data.value.asIntRange))
+			return false;
+		break;
+	case Data::PlugInTypeTaggedValue::kFloat:
+		_type = kTypeFloat;
+		_value.asFloat = data.value.asFloat.toDouble();
+		break;
+	case Data::PlugInTypeTaggedValue::kBoolean:
+		_type = kTypeBoolean;
+		_value.asBool = (data.value.asBoolean != 0);
+		break;
+	case Data::PlugInTypeTaggedValue::kEvent:
+		_type = kTypeEvent;
+		if (!_value.asEvent.load(data.value.asEvent))
+			return false;
+		break;
+	case Data::PlugInTypeTaggedValue::kLabel:
+		_type = kTypeLabel;
+		if (!_value.asLabel.load(data.value.asLabel))
+			return false;
+		break;
+	case Data::PlugInTypeTaggedValue::kString:
+		_type = kTypeString;
+		_str = data.str;
+		break;
+	case Data::PlugInTypeTaggedValue::kVariableReference:
+		_type = kTypeVariableReference;
+		_value.asVarReference.guid = data.value.asVarRefGUID;
+		_value.asVarReference.source = &_str;
+		_str.clear();	// Extra data doesn't seem to correlate to this
+		break;
+	default:
+		assert(false);
+		return false;
+	}
+
+	return true;
+}
+
+
+DynamicValue::Type DynamicValue::getType() const {
+	return _type;
+}
+
+const int32 &DynamicValue::getInt() const {
+	assert(_type == kTypeInteger);
+	return _value.asInt;
+}
+
+const double &DynamicValue::getFloat() const {
+	assert(_type == kTypeFloat);
+	return _value.asFloat;
+}
+
+const Point16 &DynamicValue::getPoint() const {
+	assert(_type == kTypePoint);
+	return _value.asPoint;
+}
+
+const IntRange &DynamicValue::getIntRange() const {
+	assert(_type == KTypeIntegerRange);
+	return _value.asIntRange;
+}
+
+const AngleMagVector &DynamicValue::getVector() const {
+	assert(_type == kTypeVector);
+	return _value.asVector;
+}
+
+const Label &DynamicValue::getLabel() const {
+	assert(_type == kTypeLabel);
+	return _value.asLabel;
+}
+
+const Event &DynamicValue::getEvent() const {
+	assert(_type == kTypeEvent);
+	return _value.asEvent;
+}
+
+const VarReference &DynamicValue::getVarReference() const {
+	assert(_type == kTypeVariableReference);
+	return _value.asVarReference;
+}
+
+const Common::String &DynamicValue::getString() const {
+	assert(_type == kTypeString);
+	return _str;
+}
+
+const bool &DynamicValue::getBool() const {
+	assert(_type == kTypeBoolean);
+	return _value.asBool;
+}
+
+DynamicValue &DynamicValue::operator=(const DynamicValue &other) {
+	if (this != &other) {
+		clear();
+		initFromOther(other);
+	}
+
+	return *this;
+}
+
+bool DynamicValue::operator==(const DynamicValue &other) const {
+	if (_type != other._type)
+		return false;
+
+	switch (_type) {
+	case kTypeNull:
+		return true;
+	case kTypeInteger:
+		return _value.asInt == other._value.asInt;
+	case kTypeFloat:
+		return _value.asFloat == other._value.asFloat;
+	case kTypePoint:
+		return _value.asPoint == other._value.asPoint;
+	case KTypeIntegerRange:
+		return _value.asIntRange == other._value.asIntRange;
+	case kTypeVector:
+		return _value.asVector == other._value.asVector;
+	case kTypeLabel:
+		return _value.asLabel == other._value.asLabel;
+	case kTypeEvent:
+		return _value.asEvent == other._value.asEvent;
+	case kTypeVariableReference:
+		return _value.asVarReference == other._value.asVarReference;
+	case kTypeIncomingData:
+		return true;
+	case kTypeString:
+		return _str == other._str;
+	case kTypeBoolean:
+		return _value.asBool == other._value.asBool;
+	default:
+		break;
+	}
+
+	assert(false);
+	return false;
+}
+
+void DynamicValue::clear() {
+	_str.clear();
+	_type = kTypeNull;
+}
+
+void DynamicValue::initFromOther(const DynamicValue& other) {
+	assert(_type == kTypeNull);
+
+	_type = other._type;
+
+	switch (_type) {
+	case kTypeNull:
+	case kTypeIncomingData:
+		break;
+	case kTypeInteger:
+		_value.asInt = other._value.asInt;
+		break;
+	case kTypeFloat:
+		_value.asFloat = other._value.asFloat;
+		break;
+	case kTypePoint:
+		_value.asPoint = other._value.asPoint;
+		break;
+	case KTypeIntegerRange:
+		_value.asIntRange = other._value.asIntRange;
+		break;
+	case kTypeVector:
+		_value.asVector = other._value.asVector;
+		break;
+	case kTypeLabel:
+		_value.asLabel = other._value.asLabel;
+		break;
+	case kTypeEvent:
+		_value.asEvent = other._value.asEvent;
+		break;
+	case kTypeVariableReference:
+		_value.asVarReference = other._value.asVarReference;
+		_str = other._str;
+		_value.asVarReference.source = &_str;
+		break;
+	case kTypeString:
+		_str = other._str;
+		break;
+	case kTypeBoolean:
+		_value.asBool = other._value.asBool;
+		break;
+	default:
+		assert(false);
+		break;
+	}
+}
 
-MessageDataLocator::MessageDataLocator() : locatorType(kMessageDataLocatorTypeNothing), superGroupID(0), guidOrLabelID(0) {
+MessengerSendSpec::MessengerSendSpec() : destination(0) {
 }
 
-bool MessageDataLocator::load(const Data::MessageDataLocator& data, const Common::String& dataSourceName) {
-	guidOrLabelID = data.guidOrLabelID;
-	superGroupID = data.superGroupID;
-	locatorType = static_cast<MessageDataLocatorType>(data.locationType);
-	sourceName = dataSourceName;
+bool MessengerSendSpec::load(const Data::Event &dataEvent, uint32 dataMessageFlags, const Data::InternalTypeTaggedValue &dataLocator, const Common::String &dataWithSource, const Common::String &dataWithString, uint32 dataDestination) {
+	messageFlags.relay = ((dataMessageFlags & 0x20000000) == 0);
+	messageFlags.cascade = ((dataMessageFlags & 0x40000000) == 0);
+	messageFlags.immediate = ((dataMessageFlags & 0x80000000) == 0);
+
+	if (!this->send.load(dataEvent))
+		return false;
+
+	if (!this->with.load(dataLocator, dataWithSource, dataWithString))
+		return false;
+
+	this->destination = dataDestination;
 
 	return true;
 }
 
-Event::Event() : eventType(0), eventInfo(0) {
+bool MessengerSendSpec::load(const Data::PlugInTypeTaggedValue &dataEvent, const MessageFlags &dataMessageFlags, const Data::PlugInTypeTaggedValue &dataWith, uint32 dataDestination) {
+	if (dataEvent.type != Data::PlugInTypeTaggedValue::kEvent)
+		return false;
+
+	if (!this->send.load(dataEvent.value.asEvent))
+		return false;
+
+	if (!this->with.load(dataWith))
+		return false;
+
+	this->destination = dataDestination;
+
+	return true;
 }
 
 bool Event::load(const Data::Event &data) {
@@ -220,6 +531,8 @@ Project::~Project() {
 void Project::loadFromDescription(const ProjectDescription& desc) {
 	_resources = desc.getResources();
 
+	debug(1, "Loading new project...");
+
 	const Common::Array<Common::SharedPtr<PlugIn> > &plugIns = desc.getPlugIns();
 
 	for (Common::Array<Common::SharedPtr<PlugIn> >::const_iterator it = plugIns.begin(), itEnd = plugIns.end(); it != itEnd; ++it) {
@@ -269,7 +582,6 @@ void Project::loadFromDescription(const ProjectDescription& desc) {
 
 	if (!dataObject || dataObject->getType() != Data::DataObjectTypes::kProjectHeader) {
 		error("Expected project header but found something else");
-
 	}
 
 	Data::loadDataObject(plugInDataLoaderRegistry, reader, dataObject);
@@ -283,6 +595,8 @@ void Project::loadFromDescription(const ProjectDescription& desc) {
 		error("Project declared a different number of segments than the project description provided");
 	}
 
+	debug(1, "Catalog loaded OK, identified %i streams", static_cast<int>(catalog->streams.size()));
+
 	_streams.resize(catalog->streams.size());
 	for (size_t i = 0; i < _streams.size(); i++) {
 		StreamDesc &streamDesc = _streams[i];
@@ -316,6 +630,8 @@ void Project::loadFromDescription(const ProjectDescription& desc) {
 		error("Failed to find boot stream");
 	}
 
+	debug(1, "Loading boot stream");
+
 	loadBootStream(bootStreamIndex);
 }
 
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index aeee610ee35..3c2547edae1 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -45,6 +45,16 @@ struct ModifierLoaderContext;
 struct Point16 {
 	int16 x;
 	int16 y;
+
+	bool load(const Data::Point &point);
+
+	inline bool operator==(const Point16& other) const {
+		return x == other.x && y == other.y;
+	}
+
+	inline bool operator!=(const Point16 &other) const {
+		return !((*this) == other);
+	}
 };
 
 struct Rect16 {
@@ -54,6 +64,85 @@ struct Rect16 {
 	int16 right;
 
 	bool load(const Data::Rect &rect);
+
+	inline bool operator==(const Rect16 &other) const {
+		return top == other.top && left == other.left && bottom == other.bottom && right == other.right;
+	}
+
+	inline bool operator!=(const Rect16 &other) const {
+		return !((*this) == other);
+	}
+};
+
+struct IntRange {
+	int32 min;
+	int32 max;
+
+	bool load(const Data::IntRange &range);
+
+	inline bool operator==(const IntRange &other) const {
+		return min == other.min && max == other.max;
+	}
+
+	inline bool operator!=(const IntRange &other) const {
+		return !((*this) == other);
+	}
+};
+
+struct Label {
+	uint32 superGroupID;
+	uint32 id;
+
+	bool load(const Data::Label &label);
+
+	inline bool operator==(const Label &other) const {
+		return superGroupID == other.superGroupID && id == other.id;
+	}
+
+	inline bool operator!=(const Label &other) const {
+		return !((*this) == other);
+	}
+};
+
+struct Event {
+	uint32 eventType;
+	uint32 eventInfo;
+
+	bool load(const Data::Event &data);
+
+	inline bool operator==(const Event &other) const {
+		return eventType == other.eventType && eventInfo == other.eventInfo;
+	}
+
+	inline bool operator!=(const Event &other) const {
+		return !((*this) == other);
+	}
+};
+
+struct VarReference {
+	uint32 guid;
+	Common::String *source;
+
+	inline bool operator==(const VarReference &other) const {
+		return guid == other.guid && (*source) == (*other.source);
+	}
+
+	inline bool operator!=(const VarReference &other) const {
+		return !((*this) == other);
+	}
+};
+
+struct AngleMagVector {
+	double angleRadians;
+	double magnitude;
+
+	inline bool operator==(const AngleMagVector &other) const {
+		return angleRadians == other.angleRadians && magnitude == other.magnitude;
+	}
+
+	inline bool operator!=(const AngleMagVector &other) const {
+		return !((*this) == other);
+	}
 };
 
 struct ColorRGB8 {
@@ -72,22 +161,79 @@ struct MessageFlags {
 	bool immediate : 1;
 };
 
-enum MessageDataLocatorType {
-	kMessageDataLocatorTypeNothing = 0,
-	kMessageDataLocatorTypeIncomingData = 0x1b,
-	kMessageDataLocatorTypeVariable = 0x1c,
-	kMessageDataLocatorTypeLabel = 0x1d,
-};
+struct DynamicValue {
+	enum Type {
+		kTypeNull,
+		kTypeInteger,
+		kTypeFloat,
+		kTypePoint,
+		KTypeIntegerRange,
+		kTypeBoolean,
+		kTypeVector,
+		kTypeLabel,
+		kTypeEvent,
+		kTypeVariableReference,
+		kTypeIncomingData,
+		kTypeString,
+	};
 
-struct MessageDataLocator {
-	MessageDataLocator(); 
-	bool load(const Data::MessageDataLocator &data, const Common::String &dataSourceName);
+	DynamicValue();
+	DynamicValue(const DynamicValue &other);
+	~DynamicValue();
 
-	MessageDataLocatorType locatorType;
-	uint32 superGroupID;
-	uint32 guidOrLabelID;
+	bool load(const Data::InternalTypeTaggedValue &data, const Common::String &varSource, const Common::String &varString);
+	bool load(const Data::PlugInTypeTaggedValue &data);
+
+	Type getType() const;
+
+	const int32 &getInt() const;
+	const double &getFloat() const;
+	const Point16 &getPoint() const;
+	const IntRange &getIntRange() const;
+	const AngleMagVector &getVector() const;
+	const Label &getLabel() const;
+	const Event &getEvent() const;
+	const VarReference &getVarReference() const;
+	const Common::String &getString() const;
+	const bool &getBool() const;
+
+	DynamicValue &operator=(const DynamicValue &other);
+
+	bool operator==(const DynamicValue &other) const;
+	inline bool operator!=(const DynamicValue& other) const {
+		return !((*this) == other);
+	}
+
+private:
+	union ValueUnion {
+		double asFloat;
+		int32 asInt;
+		IntRange asIntRange;
+		AngleMagVector asVector;
+		Label asLabel;
+		VarReference asVarReference;
+		Event asEvent;
+		Point16 asPoint;
+		bool asBool;
+	};
 
-	Common::String sourceName;
+	void clear();
+	void initFromOther(const DynamicValue &other);
+
+	Type _type;
+	ValueUnion _value;
+	Common::String _str;
+};
+
+struct MessengerSendSpec {
+	MessengerSendSpec();
+	bool load(const Data::Event &dataEvent, uint32 dataMessageFlags, const Data::InternalTypeTaggedValue &dataLocator, const Common::String &dataWithSource, const Common::String &dataWithString, uint32 dataDestination);
+	bool load(const Data::PlugInTypeTaggedValue &dataEvent, const MessageFlags &dataMessageFlags, const Data::PlugInTypeTaggedValue &dataWith, uint32 dataDestination);
+
+	Event send;
+	MessageFlags messageFlags;
+	DynamicValue with;
+	uint32 destination; // May be a MessageDestination or GUID
 };
 
 enum MessageDestination {
@@ -160,15 +306,6 @@ struct VolumeState {
 	bool isMounted;
 };
 
-struct Event {
-	Event();
-
-	uint32 eventType;
-	uint32 eventInfo;
-
-	bool load(const Data::Event &data);
-};
-
 class Runtime {
 public:
 	Runtime();


Commit: bf1a7d881d1aba9117b2df13ee5a3b2f9a9d89b7
    https://github.com/scummvm/scummvm/commit/bf1a7d881d1aba9117b2df13ee5a3b2f9a9d89b7
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add change scene, vector motion, scene transition, element transition, compound variable, and MIDI modifier loaders

Changed paths:
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/modifier_factory.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/plugin/standard_data.cpp
    engines/mtropolis/plugin/standard_data.h
    engines/mtropolis/runtime.cpp


diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index 9a0e0dd8af3..077c841230f 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -386,9 +386,8 @@ bool PlugInTypeTaggedValue::load(DataReader &reader) {
 			uint32 length2;
 			if (!reader.readU32(length1) || !reader.readU32(length2))
 				return false;
-			if (length1 != length2)	// ???
-				return false;
-			if (!reader.readTerminatedStr(this->str, length1))
+			// Usually length1 == length2 but sometimes not?
+			if (!reader.readTerminatedStr(this->str, length2))
 				return false;
 		} break;
 	case kVariableReference: {
@@ -698,6 +697,17 @@ DataReadErrorCode SetModifier::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode ChangeSceneModifier::load(DataReader &reader) {
+	if (_revision != 0x3e9)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!modHeader.load(reader) || !reader.readU32(changeSceneFlags) || !executeWhen.load(reader)
+		|| !reader.readU32(targetSectionGUID) || !reader.readU32(targetSubsectionGUID) || !reader.readU32(targetSceneGUID))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode DragMotionModifier::load(DataReader &reader) {
 	if (_revision != 0x3e8)
 		return kDataReadErrorUnsupportedRevision;
@@ -747,6 +757,36 @@ DataReadErrorCode VectorMotionModifier::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode SceneTransitionModifier::load(DataReader &reader) {
+	if (_revision != 0x3e9)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!modHeader.load(reader))
+		return kDataReadErrorReadFailed;
+
+	if (!enableWhen.load(reader) || !disableWhen.load(reader) || !reader.readU16(transitionType)
+		|| !reader.readU16(direction) || !reader.readU16(unknown3) || !reader.readU16(steps)
+		|| !reader.readU32(duration) || !reader.readBytes(unknown5))
+		return kDataReadErrorNone;
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode ElementTransitionModifier::load(DataReader &reader) {
+	if (_revision != 0x3e9)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!modHeader.load(reader))
+		return kDataReadErrorReadFailed;
+
+	if (!enableWhen.load(reader) || !disableWhen.load(reader) || !reader.readU16(revealType)
+		|| !reader.readU16(transitionType) || !reader.readU16(unknown3) || !reader.readU16(unknown4)
+		|| !reader.readU16(steps) || !reader.readU16(rate))
+		return kDataReadErrorNone;
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode IfMessengerModifier::load(DataReader &reader) {
 	if (_revision != 0x3ea)
 		return kDataReadErrorUnsupportedRevision;
@@ -886,6 +926,19 @@ DataReadErrorCode GraphicModifier::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode CompoundVariableModifier::load(DataReader &reader) {
+	if (_revision != 1)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(modifierFlags) || !reader.readU32(sizeIncludingTag) || !reader.readBytes(unknown1)
+		|| !reader.readU32(guid) || !reader.readBytes(unknown4) || !reader.readU32(unknown5)
+		|| !editorLayoutPosition.load(reader) || !reader.readU16(lengthOfName) || !reader.readU16(numChildren)
+		|| !reader.readTerminatedStr(name, lengthOfName) || !reader.readBytes(unknown7))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode BooleanVariableModifier::load(DataReader &reader) {
 	if (_revision != 0x3e8)
 		return kDataReadErrorUnsupportedRevision;
@@ -1058,15 +1111,27 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 	case DataObjectTypes::kSetModifier:
 		dataObject = new SetModifier();
 		break;
+	case DataObjectTypes::kChangeSceneModifier:
+		dataObject = new ChangeSceneModifier();
+		break;
 	case DataObjectTypes::kDragMotionModifier:
 		dataObject = new DragMotionModifier();
 		break;
 	case DataObjectTypes::kVectorMotionModifier:
 		dataObject = new VectorMotionModifier();
 		break;
+	case DataObjectTypes::kSceneTransitionModifier:
+		dataObject = new SceneTransitionModifier();
+		break;
+	case DataObjectTypes::kElementTransitionModifier:
+		dataObject = new ElementTransitionModifier();
+		break;
 	case DataObjectTypes::kIfMessengerModifier:
 		dataObject = new IfMessengerModifier();
 		break;
+	case DataObjectTypes::kCompoundVariableModifier:
+		dataObject = new CompoundVariableModifier();
+		break;
 	case DataObjectTypes::kBooleanVariableModifier:
 		dataObject = new BooleanVariableModifier();
 		break;
@@ -1139,6 +1204,7 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 		Common::SharedPtr<PlugInModifierData> plugInModifierData(plugInLoader->createModifierData());
 		errorCode = plugInModifierData->load(*static_cast<const PlugInModifier *>(dataObject), reader);
 		if (errorCode != kDataReadErrorNone) {
+			warning("Plug-in modifier failed to load");
 			outObject.reset();
 			return errorCode;
 		}
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index a2ad719ad92..404fd39e41a 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -93,15 +93,15 @@ enum DataObjectType {
 	kImageAsset                          = 0xe,		// NYI
 	kMToonAsset                          = 0xf,		// NYI
 
-	kSoundEffectModifier                 = 0x1a4,	// NYI
-	kChangeSceneModifier                 = 0x136,	// NYI
+	kChangeSceneModifier                 = 0x136,
 	kReturnModifier                      = 0x140,	// NYI
+	kSoundEffectModifier                 = 0x1a4,	// NYI
 	kDragMotionModifier                  = 0x208,
-	kVectorMotionModifier                = 0x226,
 	kPathMotionModifierV1                = 0x21c,	// NYI - Obsolete version
 	kPathMotionModifierV2                = 0x21b,	// NYI
-	kSceneTransitionModifier             = 0x26c,	// NYI
-	kElementTransitionModifier           = 0x276,	// NYI
+	kVectorMotionModifier                = 0x226,
+	kSceneTransitionModifier             = 0x26c,
+	kElementTransitionModifier           = 0x276,
 	kSharedSceneModifier                 = 0x29a,	// NYI
 	kIfMessengerModifier                 = 0x2bc,
 	kBehaviorModifier                    = 0x2c6,
@@ -120,7 +120,7 @@ enum DataObjectType {
 	kColorTableModifier                  = 0x4c4,	// NYI
 	kSaveAndRestoreModifier              = 0x4d8,	// NYI
 
-	kCompoundVariableModifier            = 0x2c7,	// NYI
+	kCompoundVariableModifier            = 0x2c7,
 	kBooleanVariableModifier             = 0x321,
 	kIntegerVariableModifier             = 0x322,
 	kIntegerRangeVariableModifier        = 0x324,
@@ -576,6 +576,28 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct ChangeSceneModifier : public DataObject {
+	TypicalModifierHeader modHeader;
+
+	enum ChangeSceneFlags {
+		kChangeSceneFlagNextScene       = 0x80000000,
+		kChangeSceneFlagPrevScene       = 0x40000000,
+		kChangeSceneFlagSpecificScene   = 0x20000000,
+		kChangeSceneFlagAddToReturnList = 0x10000000,
+		kChangeSceneFlagAddToDestList   = 0x08000000,
+		kChangeSceneFlagWrapAround      = 0x04000000,
+	};
+
+	uint32 changeSceneFlags;
+	Event executeWhen;
+	uint32 targetSectionGUID;
+	uint32 targetSubsectionGUID;
+	uint32 targetSceneGUID;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 struct DragMotionModifier : public DataObject {
 	TypicalModifierHeader modHeader;
 
@@ -633,6 +655,38 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct SceneTransitionModifier : public DataObject {
+	TypicalModifierHeader modHeader;
+
+	Event enableWhen;
+	Event disableWhen;
+	uint16 transitionType;
+	uint16 direction;
+	uint16 unknown3;
+	uint16 steps;
+	uint32 duration;
+	uint8 unknown5[2];
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+struct ElementTransitionModifier : public DataObject {
+	TypicalModifierHeader modHeader;
+
+	Event enableWhen;
+	Event disableWhen;
+	uint16 revealType;
+	uint16 transitionType;
+	uint16 unknown3;
+	uint16 unknown4;
+	uint16 steps;
+	uint16 rate;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 struct IfMessengerModifier : public DataObject {
 	TypicalModifierHeader modHeader;
 
@@ -873,6 +927,25 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct CompoundVariableModifier : public DataObject {
+	// This doesn't follow the usual modifier header layout
+	uint32 modifierFlags;
+	uint32 sizeIncludingTag;
+	uint8 unknown1[2];	// Extra field
+	uint32 guid;
+	uint8 unknown4[6];
+	uint32 unknown5;
+	Point editorLayoutPosition;
+	uint16 lengthOfName;
+	uint16 numChildren;
+	uint8 unknown7[4];
+
+	Common::String name;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 struct BooleanVariableModifier : public DataObject {
 	TypicalModifierHeader modHeader;
 	uint8 value;
diff --git a/engines/mtropolis/modifier_factory.cpp b/engines/mtropolis/modifier_factory.cpp
index ff79009bc15..185ab63a1ef 100644
--- a/engines/mtropolis/modifier_factory.cpp
+++ b/engines/mtropolis/modifier_factory.cpp
@@ -65,10 +65,16 @@ IModifierFactory *getModifierFactoryForDataObjectType(const Data::DataObjectType
 		return ModifierFactory<BehaviorModifier, Data::BehaviorModifier>::getInstance();
 	case Data::DataObjectTypes::kMiniscriptModifier:
 		return ModifierFactory<MiniscriptModifier, Data::MiniscriptModifier>::getInstance();
+	case Data::DataObjectTypes::kChangeSceneModifier:
+		return ModifierFactory<ChangeSceneModifier, Data::ChangeSceneModifier>::getInstance();
 	case Data::DataObjectTypes::kDragMotionModifier:
 		return ModifierFactory<DragMotionModifier, Data::DragMotionModifier>::getInstance();
 	case Data::DataObjectTypes::kVectorMotionModifier:
 		return ModifierFactory<VectorMotionModifier, Data::VectorMotionModifier>::getInstance();
+	case Data::DataObjectTypes::kSceneTransitionModifier:
+		return ModifierFactory<SceneTransitionModifier, Data::SceneTransitionModifier>::getInstance();
+	case Data::DataObjectTypes::kElementTransitionModifier:
+		return ModifierFactory<ElementTransitionModifier, Data::ElementTransitionModifier>::getInstance();
 	case Data::DataObjectTypes::kIfMessengerModifier:
 		return ModifierFactory<IfMessengerModifier, Data::IfMessengerModifier>::getInstance();
 	case Data::DataObjectTypes::kTimerMessengerModifier:
@@ -87,6 +93,8 @@ IModifierFactory *getModifierFactoryForDataObjectType(const Data::DataObjectType
 		return ModifierFactory<MessengerModifier, Data::MessengerModifier>::getInstance();
 	case Data::DataObjectTypes::kSetModifier:
 		return ModifierFactory<SetModifier, Data::SetModifier>::getInstance();
+	case Data::DataObjectTypes::kCompoundVariableModifier:
+		return ModifierFactory<CompoundVariableModifier, Data::CompoundVariableModifier>::getInstance();
 	case Data::DataObjectTypes::kBooleanVariableModifier:
 		return ModifierFactory<BooleanVariableModifier, Data::BooleanVariableModifier>::getInstance();
 	case Data::DataObjectTypes::kIntegerVariableModifier:
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index bef3fe7bd02..6e968aa0637 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -88,6 +88,29 @@ bool SetModifier::load(ModifierLoaderContext &context, const Data::SetModifier &
 	return true;
 }
 
+bool ChangeSceneModifier::load(ModifierLoaderContext& context, const Data::ChangeSceneModifier& data) {
+	if (!loadTypicalHeader(data.modHeader))
+		return false;
+
+	if (!_executeWhen.load(data.executeWhen))
+		return false;
+
+	if ((data.changeSceneFlags & Data::ChangeSceneModifier::kChangeSceneFlagNextScene) != 0)
+		_sceneSelectionType = kSceneSelectionTypeNext;
+	else if ((data.changeSceneFlags & Data::ChangeSceneModifier::kChangeSceneFlagPrevScene) != 0)
+		_sceneSelectionType = kSceneSelectionTypePrevious;
+	else if ((data.changeSceneFlags & Data::ChangeSceneModifier::kChangeSceneFlagSpecificScene) != 0)
+		_sceneSelectionType = kSceneSelectionTypeSpecific;
+	else
+		return false;
+
+	_addToReturnList = ((data.changeSceneFlags & Data::ChangeSceneModifier::kChangeSceneFlagAddToReturnList) != 0);
+	_addToDestList = ((data.changeSceneFlags & Data::ChangeSceneModifier::kChangeSceneFlagAddToDestList) != 0);
+	_addToWrapAround = ((data.changeSceneFlags & Data::ChangeSceneModifier::kChangeSceneFlagWrapAround) != 0);
+
+	return true;
+}
+
 bool DragMotionModifier::load(ModifierLoaderContext &context, const Data::DragMotionModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -134,6 +157,36 @@ bool VectorMotionModifier::load(ModifierLoaderContext &context, const Data::Vect
 	return true;
 }
 
+bool SceneTransitionModifier::load(ModifierLoaderContext &context, const Data::SceneTransitionModifier &data) {
+	if (!loadTypicalHeader(data.modHeader))
+		return false;
+
+	if (!_enableWhen.load(data.enableWhen) || !_disableWhen.load(data.disableWhen))
+		return false;
+
+	_duration = data.duration;
+	_steps = data.steps;
+	_transitionType = static_cast<TransitionType>(data.transitionType);
+	_transitionDirection = static_cast<TransitionDirection>(data.direction);
+
+	return true;
+}
+
+bool ElementTransitionModifier::load(ModifierLoaderContext &context, const Data::ElementTransitionModifier &data) {
+	if (!loadTypicalHeader(data.modHeader))
+		return false;
+
+	if (!_enableWhen.load(data.enableWhen) || !_disableWhen.load(data.disableWhen))
+		return false;
+
+	_rate = data.rate;
+	_steps = data.steps;
+	_transitionType = static_cast<TransitionType>(data.transitionType);
+	_revealType = static_cast<RevealType>(data.revealType);
+
+	return true;
+}
+
 bool IfMessengerModifier::load(ModifierLoaderContext &context, const Data::IfMessengerModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -312,6 +365,33 @@ bool GraphicModifier::load(ModifierLoaderContext& context, const Data::GraphicMo
 	return true;
 }
 
+bool CompoundVariableModifier::load(ModifierLoaderContext &context, const Data::CompoundVariableModifier &data) {
+	if (data.numChildren > 0) {
+		ChildLoaderContext loaderContext;
+		loaderContext.containerUnion.modifierContainer = this;
+		loaderContext.type = ChildLoaderContext::kTypeModifierList;
+		loaderContext.remainingCount = data.numChildren;
+
+		context.childLoaderStack->contexts.push_back(loaderContext);
+	}
+
+	if (!_modifierFlags.load(data.modifierFlags))
+		return false;
+	_guid = data.guid;
+	_name = data.name;
+
+	return true;
+}
+
+const Common::Array<Common::SharedPtr<Modifier> > &CompoundVariableModifier::getModifiers() const {
+	return _children;
+}
+
+void CompoundVariableModifier::appendModifier(const Common::SharedPtr<Modifier>& modifier) {
+	_children.push_back(modifier);
+}
+
+
 bool BooleanVariableModifier::load(ModifierLoaderContext &context, const Data::BooleanVariableModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 65e1f922152..2ce8a10c99a 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -72,6 +72,27 @@ private:
 	DynamicValue _target;
 };
 
+class ChangeSceneModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::ChangeSceneModifier &data);
+
+private:
+	enum SceneSelectionType {
+		kSceneSelectionTypeNext,
+		kSceneSelectionTypePrevious,
+		kSceneSelectionTypeSpecific,
+	};
+
+	Event _executeWhen;
+	SceneSelectionType _sceneSelectionType;
+	uint32 _targetSectionGUID;
+	uint32 _targetSubsectionGUID;
+	uint32 _targetSceneGUID;
+	bool _addToReturnList;
+	bool _addToDestList;
+	bool _addToWrapAround;
+};
+
 class DragMotionModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::DragMotionModifier &data);
@@ -102,6 +123,63 @@ private:
 	DynamicValue _vec;
 };
 
+class SceneTransitionModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::SceneTransitionModifier &data);
+
+	enum TransitionType {
+		kTransitionTypePatternDissolve = 0x0406,
+		kTransitionTypeRandomDissolve  = 0x0410,	// No steps
+		kTransitionTypeFade            = 0x041a,
+		kTransitionTypeSlide           = 0x03e8,	// Directional
+		kTransitionTypePush            = 0x03f2,	// Directional
+		kTransitionTypeZoom            = 0x03fc,
+		kTransitionTypeWipe            = 0x0424,	// Directional
+	};
+
+	enum TransitionDirection {
+		kTransitionDirectionUp = 0x385,
+		kTransitionDirectionDown = 0x385,
+		kTransitionDirectionLeft = 0x386,
+		kTransitionDirectionRight = 0x387,
+	};
+
+private:
+	Event _enableWhen;
+	Event _disableWhen;
+
+	uint32 _duration;	// 6000000 is maximum
+	uint16 _steps;
+	TransitionType _transitionType;
+	TransitionDirection _transitionDirection;
+};
+
+class ElementTransitionModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::ElementTransitionModifier &data);
+
+	enum TransitionType {
+		kTransitionTypeRectangularIris = 0x03e8,
+		kTransitionTypeOvalIris = 0x03f2,
+		kTransitionTypeZoom = 0x044c,
+		kTransitionTypeFade = 0x2328,
+	};
+
+	enum RevealType {
+		kRevealTypeReveal = 0,
+		kRevealTypeConceal = 1,
+	};
+
+private:
+	Event _enableWhen;
+	Event _disableWhen;
+
+	uint32 _rate;	// 1-100, higher is faster
+	uint16 _steps;
+	TransitionType _transitionType;
+	RevealType _revealType;
+};
+
 class IfMessengerModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::IfMessengerModifier &data);
@@ -289,6 +367,17 @@ private:
 	Common::Array<Point16> _polyPoints;
 };
 
+class CompoundVariableModifier : public Modifier, public IModifierContainer {
+public:
+	bool load(ModifierLoaderContext &context, const Data::CompoundVariableModifier &data);
+
+private:
+	const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const override;
+	void appendModifier(const Common::SharedPtr<Modifier> &modifier) override;
+
+	Common::Array<Common::SharedPtr<Modifier> > _children;
+};
+
 class BooleanVariableModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::BooleanVariableModifier &data);
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 65615529ef6..c9c8951255c 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -70,13 +70,76 @@ bool MediaCueMessengerModifier::load(const PlugInModifierLoaderContext& context,
 	return true;
 }
 
-StandardPlugIn::StandardPlugIn() : _cursorModifierFactory(this), _sTransCtModifierFactory(this), _mediaCueModifierFactory(this) {
+bool ObjectReferenceVariableModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::ObjectReferenceVariableModifier &data) {
+	if (data.setToSourceParentWhen.type != Data::PlugInTypeTaggedValue::kEvent)
+		return false;
+
+	_setToSourceParentWhen.load(data.setToSourceParentWhen.value.asEvent);
+
+	if (data.objectPath.type != Data::PlugInTypeTaggedValue::kString)
+		return false;
+
+	_objectPath = data.objectPath.str;
+
+	return true;
+}
+
+bool MidiModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::MidiModifier &data) {
+	if (data.executeWhen.type != Data::PlugInTypeTaggedValue::kEvent)
+		return false;
+
+	_executeWhen.load(data.executeWhen.value.asEvent);
+	if (data.terminateWhen.type != Data::PlugInTypeTaggedValue::kEvent)
+		return false;
+
+	_terminateWhen.load(data.terminateWhen.value.asEvent);
+
+	if (data.embeddedFlag) {
+		_mode = kModeFile;
+		_embeddedFile = data.embeddedFile;
+
+		_modeSpecific.file.loop = (data.modeSpecific.embedded.loop != 0);
+		_modeSpecific.file.overrideTempo = (data.modeSpecific.embedded.overrideTempo != 0);
+		_modeSpecific.file.volume = (data.modeSpecific.embedded.volume != 0);
+
+		if (data.embeddedFadeIn.type != Data::PlugInTypeTaggedValue::kFloat
+			|| data.embeddedFadeOut.type != Data::PlugInTypeTaggedValue::kFloat
+			|| data.embeddedTempo.type != Data::PlugInTypeTaggedValue::kFloat)
+			return false;
+
+		_modeSpecific.file.fadeIn = data.embeddedFadeIn.value.asFloat.toDouble();
+		_modeSpecific.file.fadeOut = data.embeddedFadeOut.value.asFloat.toDouble();
+		_modeSpecific.file.tempo = data.embeddedTempo.value.asFloat.toDouble();
+	} else {
+		_mode = kModeSingleNote;
+
+		if (data.singleNoteDuration.type != Data::PlugInTypeTaggedValue::kFloat)
+			return false;
+
+		_modeSpecific.singleNote.channel = data.modeSpecific.singleNote.channel;
+		_modeSpecific.singleNote.note = data.modeSpecific.singleNote.note;
+		_modeSpecific.singleNote.velocity = data.modeSpecific.singleNote.velocity;
+		_modeSpecific.singleNote.program = data.modeSpecific.singleNote.program;
+		_modeSpecific.singleNote.duration = data.singleNoteDuration.value.asFloat.toDouble();
+	}
+
+	return true;
+}
+
+StandardPlugIn::StandardPlugIn()
+	: _cursorModifierFactory(this)
+	, _sTransCtModifierFactory(this)
+	, _mediaCueModifierFactory(this)
+	, _objRefVarModifierFactory(this)
+	, _midiModifierFactory(this) {
 }
 
 void StandardPlugIn::registerModifiers(IPlugInModifierRegistrar *registrar) const {
 	registrar->registerPlugInModifier("CursorMod", &_cursorModifierFactory);
 	registrar->registerPlugInModifier("STransCt", &_sTransCtModifierFactory);
 	registrar->registerPlugInModifier("MediaCue", &_mediaCueModifierFactory);
+	registrar->registerPlugInModifier("ObjRefP", &_objRefVarModifierFactory);
+	registrar->registerPlugInModifier("MIDIModf", &_midiModifierFactory);
 }
 
 } // End of namespace Standard
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index 343494f5c78..db2a88829cb 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -66,6 +66,56 @@ private:
 	MessengerSendSpec _send;
 };
 
+class ObjectReferenceVariableModifier : public Modifier {
+public:
+	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::ObjectReferenceVariableModifier &data);
+
+private:
+	Event _setToSourceParentWhen;
+	Common::String _objectPath;
+};
+
+class MidiModifier : public Modifier {
+public:
+	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::MidiModifier &data);
+
+private:
+	struct FilePart {
+		bool loop;
+		bool overrideTempo;
+		bool volume;
+		double tempo;
+		double fadeIn;
+		double fadeOut;
+	};
+
+	struct SingleNotePart {
+		uint8 channel;
+		uint8 note;
+		uint8 velocity;
+		uint8 program;
+		double duration;
+	};
+
+	union ModeSpecificUnion {
+		FilePart file;
+		SingleNotePart singleNote;
+	};
+
+	enum Mode {
+		kModeFile,
+		kModeSingleNote,
+	};
+
+	Event _executeWhen;
+	Event _terminateWhen;
+
+	Mode _mode;
+	ModeSpecificUnion _modeSpecific;
+
+	Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> _embeddedFile;
+};
+
 class StandardPlugIn : public MTropolis::PlugIn {
 public:
 	StandardPlugIn();
@@ -76,6 +126,8 @@ private:
 	PlugInModifierFactory<CursorModifier, Data::Standard::CursorModifier> _cursorModifierFactory;
 	PlugInModifierFactory<STransCtModifier, Data::Standard::STransCtModifier> _sTransCtModifierFactory;
 	PlugInModifierFactory<MediaCueMessengerModifier, Data::Standard::MediaCueMessengerModifier> _mediaCueModifierFactory;
+	PlugInModifierFactory<ObjectReferenceVariableModifier, Data::Standard::ObjectReferenceVariableModifier> _objRefVarModifierFactory;
+	PlugInModifierFactory<MidiModifier, Data::Standard::MidiModifier> _midiModifierFactory;
 };
 
 } // End of namespace Standard
diff --git a/engines/mtropolis/plugin/standard_data.cpp b/engines/mtropolis/plugin/standard_data.cpp
index 350b3b024f6..15b2d0fdc88 100644
--- a/engines/mtropolis/plugin/standard_data.cpp
+++ b/engines/mtropolis/plugin/standard_data.cpp
@@ -64,6 +64,53 @@ DataReadErrorCode MediaCueMessengerModifier::load(const PlugInModifier &prefix,
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode ObjectReferenceVariableModifier::load(const PlugInModifier &prefix, DataReader &reader) {
+	if (prefix.plugInRevision != 2)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!setToSourceParentWhen.load(reader) || !unknown1.load(reader) || !objectPath.load(reader))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode MidiModifier::load(const PlugInModifier &prefix, DataReader &reader) {
+	if (prefix.plugInRevision != 1 && prefix.plugInRevision != 2)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!executeWhen.load(reader) || !terminateWhen.load(reader) || !reader.readU8(embeddedFlag))
+		return kDataReadErrorReadFailed;
+
+	if (embeddedFlag) {
+		if (!reader.readU8(modeSpecific.embedded.hasFile))
+			return kDataReadErrorReadFailed;
+		if (modeSpecific.embedded.hasFile) {
+			embeddedFile = Common::SharedPtr<EmbeddedFile>(new EmbeddedFile());
+
+			uint8 bigEndianLength[4];
+			if (!reader.readBytes(bigEndianLength))
+				return kDataReadErrorReadFailed;
+
+			uint32 length = (bigEndianLength[0] << 24) + (bigEndianLength[1] << 16) + (bigEndianLength[2] << 8) + bigEndianLength[3];
+
+			embeddedFile->contents.resize(length);
+			if (length > 0 && !reader.read(&embeddedFile->contents[0], length))
+				return kDataReadErrorReadFailed;
+		}
+
+		if (!reader.readU8(modeSpecific.embedded.loop) || !reader.readU8(modeSpecific.embedded.overrideTempo)
+			|| !reader.readU8(modeSpecific.embedded.volume) || !embeddedTempo.load(reader)
+			|| !embeddedFadeIn.load(reader) || !embeddedFadeOut.load(reader))
+			return kDataReadErrorReadFailed;
+	} else {
+		if (!reader.readU8(modeSpecific.singleNote.channel) || !reader.readU8(modeSpecific.singleNote.note) || !reader.readU8(modeSpecific.singleNote.velocity)
+			|| !reader.readU8(modeSpecific.singleNote.program) || !singleNoteDuration.load(reader))
+			return kDataReadErrorReadFailed;
+	}
+
+	return kDataReadErrorNone;
+}
+
 } // End of namespace Standard
 
 } // End of namespace Data
diff --git a/engines/mtropolis/plugin/standard_data.h b/engines/mtropolis/plugin/standard_data.h
index 7260136b6b3..9c087b6c7dc 100644
--- a/engines/mtropolis/plugin/standard_data.h
+++ b/engines/mtropolis/plugin/standard_data.h
@@ -93,6 +93,56 @@ protected:
 	DataReadErrorCode load(const PlugInModifier &prefix, DataReader &reader) override;
 };
 
+struct ObjectReferenceVariableModifier : public PlugInModifierData {
+	PlugInTypeTaggedValue setToSourceParentWhen;
+	PlugInTypeTaggedValue unknown1;
+	PlugInTypeTaggedValue objectPath;
+
+protected:
+	DataReadErrorCode load(const PlugInModifier &prefix, DataReader &reader) override;
+};
+
+struct MidiModifier : public PlugInModifierData {
+	struct EmbeddedFile {
+		Common::Array<uint8> contents;
+	};
+
+	struct EmbeddedPart {
+		uint8 hasFile;
+		uint8 loop;
+		uint8 overrideTempo;
+		uint8 volume;
+	};
+
+	struct SingleNotePart {
+		uint8 channel;
+		uint8 note;
+		uint8 velocity;
+		uint8 program;
+	};
+
+	union ModeSpecificUnion {
+		EmbeddedPart embedded;
+		SingleNotePart singleNote;
+	};
+
+	PlugInTypeTaggedValue executeWhen;
+	PlugInTypeTaggedValue terminateWhen;
+
+	uint8 embeddedFlag;
+	ModeSpecificUnion modeSpecific;
+
+	PlugInTypeTaggedValue embeddedTempo;		// Float
+	PlugInTypeTaggedValue embeddedFadeIn;		// Float
+	PlugInTypeTaggedValue embeddedFadeOut;		// Float
+	PlugInTypeTaggedValue singleNoteDuration;	// Float
+
+	Common::SharedPtr<EmbeddedFile> embeddedFile;
+
+protected:
+	DataReadErrorCode load(const PlugInModifier &prefix, DataReader &reader) override;
+};
+
 } // End of namespace Standard
 
 } // End of namespace Data
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 401dd6b253e..81e02fdaba4 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -835,6 +835,8 @@ Modifier::~Modifier() {
 bool Modifier::loadTypicalHeader(const Data::TypicalModifierHeader& typicalHeader) {
 	if (!_modifierFlags.load(typicalHeader.modifierFlags))
 		return false;
+	_guid = typicalHeader.guid;
+	_name = typicalHeader.name;
 
 	return true;
 }


Commit: e69e1f775489425348ff73b7d78aed5833255fe4
    https://github.com/scummvm/scummvm/commit/e69e1f775489425348ff73b7d78aed5833255fe4
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add Obsidian movement and rectshift loader stubs

Changed paths:
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/plugin/obsidian.cpp
    engines/mtropolis/plugin/obsidian.h
    engines/mtropolis/plugin/obsidian_data.cpp
    engines/mtropolis/plugin/obsidian_data.h


diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index 077c841230f..e834f5505dd 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -356,6 +356,10 @@ bool PlugInTypeTaggedValue::load(DataReader &reader) {
 	case kNull:
 	case kIncomingData:
 		break;
+	case kPoint:
+		if (!value.asPoint.load(reader))
+			return false;
+		break;
 	case kInteger:
 		if (!reader.readS32(value.asInt))
 			return false;
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index 404fd39e41a..e66e4f50574 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -285,6 +285,7 @@ struct PlugInTypeTaggedValue : public Common::NonCopyable {
 	enum TypeCode {
 		kNull = 0x00,
 		kInteger = 0x01,
+		kPoint = 0xa,
 		kIntegerRange = 0xb,
 		kFloat = 0xf,
 		kBoolean = 0x14,
diff --git a/engines/mtropolis/plugin/obsidian.cpp b/engines/mtropolis/plugin/obsidian.cpp
index a6220328bb0..aa7ac255c76 100644
--- a/engines/mtropolis/plugin/obsidian.cpp
+++ b/engines/mtropolis/plugin/obsidian.cpp
@@ -26,10 +26,20 @@ namespace MTropolis {
 
 namespace Obsidian {
 
-ObsidianPlugIn::ObsidianPlugIn() {
+bool MovementModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::MovementModifier &data) {
+	return true;
+}
+
+bool RectShiftModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::RectShiftModifier &data) {
+	return true;
+}
+
+ObsidianPlugIn::ObsidianPlugIn() : _movementModifierFactory(this), _rectShiftModifierFactory(this) {
 }
 
 void ObsidianPlugIn::registerModifiers(IPlugInModifierRegistrar *registrar) const {
+	registrar->registerPlugInModifier("Movement", &_movementModifierFactory);
+	registrar->registerPlugInModifier("rectshift", &_rectShiftModifierFactory);
 }
 
 } // End of namespace ObsidianPlugIn
diff --git a/engines/mtropolis/plugin/obsidian.h b/engines/mtropolis/plugin/obsidian.h
index 9b0e948b520..bf848b54f2a 100644
--- a/engines/mtropolis/plugin/obsidian.h
+++ b/engines/mtropolis/plugin/obsidian.h
@@ -22,17 +22,34 @@
 #ifndef MTROPOLIS_PLUGIN_OBSIDIAN_H
 #define MTROPOLIS_PLUGIN_OBSIDIAN_H
 
+#include "mtropolis/modifiers.h"
+#include "mtropolis/modifier_factory.h"
 #include "mtropolis/runtime.h"
+#include "mtropolis/plugin/obsidian_data.h"
 
 namespace MTropolis {
 
 namespace Obsidian {
 
+class MovementModifier : public Modifier {
+public:
+	bool load(const PlugInModifierLoaderContext &context, const Data::Obsidian::MovementModifier &data);
+};
+
+class RectShiftModifier : public Modifier {
+public:
+	bool load(const PlugInModifierLoaderContext &context, const Data::Obsidian::RectShiftModifier &data);
+};
+
 class ObsidianPlugIn : public MTropolis::PlugIn {
 public:
 	ObsidianPlugIn();
 
 	void registerModifiers(IPlugInModifierRegistrar *registrar) const override;
+
+private:
+	PlugInModifierFactory<MovementModifier, Data::Obsidian::MovementModifier> _movementModifierFactory;
+	PlugInModifierFactory<RectShiftModifier, Data::Obsidian::RectShiftModifier> _rectShiftModifierFactory;
 };
 
 } // End of namespace Obsidian
diff --git a/engines/mtropolis/plugin/obsidian_data.cpp b/engines/mtropolis/plugin/obsidian_data.cpp
index 85ac698db4c..92ea208269c 100644
--- a/engines/mtropolis/plugin/obsidian_data.cpp
+++ b/engines/mtropolis/plugin/obsidian_data.cpp
@@ -27,6 +27,36 @@ namespace Data {
 
 namespace Obsidian {
 
+DataReadErrorCode MovementModifier::load(const PlugInModifier& prefix, DataReader& reader) {
+	if (prefix.plugInRevision != 0)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!unknown1Event.load(reader)
+		|| !unknown2Event.load(reader)
+		|| !unknown3Point.load(reader)
+		|| !unknown4Bool.load(reader)
+		|| !unknown5Point.load(reader)
+		|| !unknown6Int.load(reader)
+		|| !unknown7Float.load(reader)
+		|| !unknown8Int.load(reader)
+		|| !unknown9Event.load(reader)
+		|| !unknown10Label.load(reader)
+		|| !unknown11Null.load(reader)
+		|| !unknown12Int.load(reader))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode RectShiftModifier::load(const PlugInModifier &prefix, DataReader &reader) {
+	if (prefix.plugInRevision != 1)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!unknown1Event.load(reader) || !unknown2Event.load(reader) || !unknown3Int.load(reader))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
 
 } // End of namespace Obsidian
 
diff --git a/engines/mtropolis/plugin/obsidian_data.h b/engines/mtropolis/plugin/obsidian_data.h
index 6c5ed92db44..b9c35007106 100644
--- a/engines/mtropolis/plugin/obsidian_data.h
+++ b/engines/mtropolis/plugin/obsidian_data.h
@@ -30,6 +30,42 @@ namespace Data {
 
 namespace Obsidian {
 
+// Known Obsidian custom modifiers:
+// Movement - Heat/water effects, "fly" behavior (?)
+// rectshift - Heat/water/star effects (?)
+// xorCheck - Inspiration realm canvas puzzle
+// xorMod - Inspiration realm canvas puzzle
+// WordMixer - Bureau realm WordMixer terminal
+// Dictionary - Bureau realm file cabinet dictionary
+// TextWork - Text manipulation operations
+
+struct MovementModifier : public PlugInModifierData {
+	PlugInTypeTaggedValue unknown1Event;	// Probably "enable when"
+	PlugInTypeTaggedValue unknown2Event;	// Probably "disable when"
+	PlugInTypeTaggedValue unknown3Point;
+	PlugInTypeTaggedValue unknown4Bool;
+	PlugInTypeTaggedValue unknown5Point;
+	PlugInTypeTaggedValue unknown6Int;
+	PlugInTypeTaggedValue unknown7Float;
+	PlugInTypeTaggedValue unknown8Int;
+	PlugInTypeTaggedValue unknown9Event;
+	PlugInTypeTaggedValue unknown10Label;
+	PlugInTypeTaggedValue unknown11Null;
+	PlugInTypeTaggedValue unknown12Int;
+
+protected:
+	DataReadErrorCode load(const PlugInModifier &prefix, DataReader &reader) override;
+};
+
+struct RectShiftModifier : public PlugInModifierData {
+	PlugInTypeTaggedValue unknown1Event; // Probably "enable when"
+	PlugInTypeTaggedValue unknown2Event; // Probably "disable when"
+	PlugInTypeTaggedValue unknown3Int;
+
+protected:
+	DataReadErrorCode load(const PlugInModifier &prefix, DataReader &reader) override;
+};
+
 
 } // End of namespace Obsidian
 


Commit: abb8ccb79d740d1cad986224121b0c1271a609f5
    https://github.com/scummvm/scummvm/commit/abb8ccb79d740d1cad986224121b0c1271a609f5
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add list modifier and preliminary structural element loading

Changed paths:
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/modifier_factory.h
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/plugin/obsidian_data.cpp
    engines/mtropolis/plugin/obsidian_data.h
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/plugin/standard_data.cpp
    engines/mtropolis/plugin/standard_data.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index e834f5505dd..edb91aa41e8 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -135,6 +135,10 @@ bool DataReader::readNonTerminatedStr(Common::String &value, size_t size) {
 	return true;
 }
 
+bool DataReader::seek(int64 pos) {
+	return _stream.seek(pos);
+}
+
 bool DataReader::skip(size_t count) {
 	while (count > 0) {
 		uint64 thisChunkSize = INT64_MAX;
@@ -152,6 +156,10 @@ bool DataReader::skip(size_t count) {
 	return true;
 }
 
+int64 DataReader::tell() const {
+	return _stream.pos();
+}
+
 ProjectFormat DataReader::getProjectFormat() const {
 	return _projectFormat;
 }
@@ -433,6 +441,91 @@ DataObjectTypes::DataObjectType DataObject::getType() const {
 	return _type;
 }
 
+ProjectLabelMap::ProjectLabelMap() : superGroups(nullptr) {
+}
+
+ProjectLabelMap::~ProjectLabelMap() {
+	if (superGroups)
+		delete[] superGroups;
+}
+
+DataReadErrorCode ProjectLabelMap::load(DataReader &reader) {
+	if (_revision != 0)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(persistFlags) || !reader.readU32(unknown1) || !reader.readU32(numSuperGroups) || !reader.readU32(nextAvailableID))
+		return kDataReadErrorReadFailed;
+
+	if (unknown1 != 0x16)
+		return kDataReadErrorUnrecognized;
+
+	superGroups = new SuperGroup[numSuperGroups];
+	for (size_t i = 0; i < numSuperGroups; i++) {
+		DataReadErrorCode subCode = loadSuperGroup(superGroups[i], reader);
+		if (subCode != kDataReadErrorNone)
+			return subCode;
+	}
+
+	return kDataReadErrorNone;
+}
+
+ProjectLabelMap::LabelTree::LabelTree() : children(nullptr) {
+}
+
+ProjectLabelMap::LabelTree::~LabelTree() {
+	if (children)
+		delete[] children;
+}
+
+ProjectLabelMap::SuperGroup::SuperGroup()
+	: tree(nullptr) {
+}
+
+ProjectLabelMap::SuperGroup::~SuperGroup() {
+	if (tree)
+		delete[] tree;
+}
+
+DataReadErrorCode ProjectLabelMap::loadSuperGroup(SuperGroup &sg, DataReader &reader) {
+	if (!reader.readU32(sg.nameLength) || !reader.readU32(sg.id) || !reader.readU32(sg.unknown2)
+		|| !reader.readNonTerminatedStr(sg.name, sg.nameLength) || !reader.readU32(sg.numChildren))
+		return kDataReadErrorReadFailed;
+
+	if (sg.numChildren) {
+		sg.tree = new LabelTree[sg.numChildren];
+		for (size_t i = 0; i < sg.numChildren; i++) {
+			DataReadErrorCode subCode = loadLabelTree(sg.tree[i], reader);
+			if (subCode != kDataReadErrorNone)
+				return subCode;
+		}
+	}
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode ProjectLabelMap::loadLabelTree(LabelTree &lt, DataReader &reader) {
+	if (!reader.readU32(lt.nameLength) || !reader.readU32(lt.isGroup) || !reader.readU32(lt.id)
+		|| !reader.readU32(lt.unknown1) || !reader.readU32(lt.flags) || !reader.readNonTerminatedStr(lt.name, lt.nameLength))
+		return kDataReadErrorReadFailed;
+
+	if (lt.isGroup) {
+		if (!reader.readU32(lt.numChildren))
+			return kDataReadErrorReadFailed;
+
+		if (lt.numChildren) {
+			lt.children = new LabelTree[lt.numChildren];
+			for (size_t i = 0; i < lt.numChildren; i++) {
+				DataReadErrorCode subCode = loadLabelTree(lt.children[i], reader);
+				if (subCode != kDataReadErrorNone)
+					return subCode;
+			}
+		}
+	} else
+		lt.numChildren = 0;
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode ProjectHeader::load(DataReader &reader) {
 	if (_revision != 0) {
 		return kDataReadErrorUnsupportedRevision;
@@ -446,7 +539,6 @@ DataReadErrorCode ProjectHeader::load(DataReader &reader) {
 }
 
 DataReadErrorCode PresentationSettings::load(DataReader &reader) {
-
 	if (_revision != 2)
 		return kDataReadErrorUnsupportedRevision;
 
@@ -498,6 +590,41 @@ DataReadErrorCode Unknown19::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode ProjectStructuralDef::load(DataReader &reader) {
+	if (_revision != 1 && _revision != 2)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(unknown1) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid)
+		|| !reader.readU32(structuralFlags) || !reader.readU16(lengthOfName) || !reader.readTerminatedStr(name, lengthOfName))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode SectionStructuralDef::load(DataReader &reader) {
+	if (_revision != 1)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(unknown1) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid)
+		|| !reader.readU16(lengthOfName) || !reader.readU32(structuralFlags) || !reader.readU16(unknown4)
+		|| !reader.readU16(unknown4) || !reader.readU32(segmentID) || !reader.readTerminatedStr(name, lengthOfName))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode SubsectionStructuralDef::load(DataReader &reader) {
+	if (_revision != 0)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(unknown1) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid)
+		|| !reader.readU16(lengthOfName) || !reader.readU32(structuralFlags) || !reader.readU16(sectionID)
+		|| !reader.readTerminatedStr(name, lengthOfName))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode GlobalObjectInfo::load(DataReader &reader) {
 	if (_revision != 0)
 		return kDataReadErrorUnsupportedRevision;
@@ -1082,6 +1209,9 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 
 	DataObject *dataObject = nullptr;
 	switch (type) {
+	case DataObjectTypes::kProjectLabelMap:
+		dataObject = new ProjectLabelMap();
+		break;
 	case DataObjectTypes::kProjectHeader:
 		dataObject = new ProjectHeader();
 		break;
@@ -1100,6 +1230,15 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 	case DataObjectTypes::kUnknown19:
 		dataObject = new Unknown19();
 		break;
+	case DataObjectTypes::kProjectStructuralDef:
+		dataObject = new ProjectStructuralDef();
+		break;
+	case DataObjectTypes::kSectionStructuralDef:
+		dataObject = new SectionStructuralDef();
+		break;
+	case DataObjectTypes::kSubsectionStructuralDef:
+		dataObject = new SubsectionStructuralDef();
+		break;
 	case DataObjectTypes::kGlobalObjectInfo:
 		dataObject = new GlobalObjectInfo();
 		break;
@@ -1206,7 +1345,7 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 		}
 
 		Common::SharedPtr<PlugInModifierData> plugInModifierData(plugInLoader->createModifierData());
-		errorCode = plugInModifierData->load(*static_cast<const PlugInModifier *>(dataObject), reader);
+		errorCode = plugInModifierData->load(plugInLoader->getPlugIn(), *static_cast<const PlugInModifier *>(dataObject), reader);
 		if (errorCode != kDataReadErrorNone) {
 			warning("Plug-in modifier failed to load");
 			outObject.reset();
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index e66e4f50574..3af07cf4883 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -32,6 +32,8 @@
 // This contains defs related to parsing of mTropolis stored data into structured data objects.
 namespace MTropolis {
 
+class PlugIn;
+
 namespace Data {
 
 struct PlugInModifier;
@@ -64,6 +66,7 @@ namespace DataObjectTypes {
 enum DataObjectType {
 	kUnknown                             = 0,
 
+	kProjectLabelMap                     = 0x22,
 	kProjectCatalog                      = 0x3e8,
 	kStreamHeader                        = 0x3e9,
 	kProjectHeader                       = 0x3ea,
@@ -72,15 +75,15 @@ enum DataObjectType {
 	kAssetCatalog                        = 0xd,
     kGlobalObjectInfo                    = 0x17,
 	kUnknown19                           = 0x19,
-	kProjectLabelMap                     = 0x22,	// NYI
 
-	kProjectStructuralDef                = 0x2,		// NYI
-	kSectionStructuralDef                = 0x3,		// NYI
-	kSceneStructuralDef                  = 0x8,
-	kSubsectionStructuralDef             = 0x21,	// NYI
+	kProjectStructuralDef                = 0x2,
+	kSectionStructuralDef                = 0x3,
+	kSubsectionStructuralDef             = 0x21,
+
+	kGraphicStructuralDef                = 0x8,		// NYI
 	kMovieStructuralDef                  = 0x5,		// NYI
 	kMToonStructuralDef                  = 0x6,		// NYI
-	kGraphicStructuralDef                = 0x7,		// NYI
+	kImageStructuralDef                  = 0x7,		// NYI
 	kSoundStructuralDef                  = 0xa,		// NYI
 
 	kTextLabelElement                    = 0x15,	// NYI
@@ -136,6 +139,13 @@ enum DataObjectType {
 
 } // End of namespace DataObjectTypes
 
+namespace StructuralFlags {
+	enum StructuralFlags {
+		kHasChildren = 0x4,
+		kNoMoreSiblings = 0x8,
+	};
+} // End of namespace StructuralFlags
+
 class DataReader {
 
 public:
@@ -165,8 +175,11 @@ public:
 	template<size_t TSize>
 	bool readBytes(uint8 (&arr)[TSize]);
 
+	bool seek(int64 pos);
 	bool skip(size_t count);
 
+	int64 tell() const;
+
 	ProjectFormat getProjectFormat() const;
 	bool isBigEndian() const;
 
@@ -333,9 +346,7 @@ protected:
 	uint16 _revision;
 };
 
-class ProjectHeader : public DataObject {
-
-public:
+struct ProjectHeader : public DataObject {
 	uint32 persistFlags;
 	uint32 sizeIncludingTag;
 	uint16 unknown1;
@@ -345,6 +356,57 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct ProjectLabelMap : public DataObject {
+	ProjectLabelMap();
+	~ProjectLabelMap();
+
+	struct LabelTree {
+		LabelTree();
+		~LabelTree();
+
+		enum {
+			kExpandedInEditor = 0x80000000,
+		};
+
+		uint32 nameLength;
+		uint32 isGroup;
+		uint32 id;
+		uint32 unknown1;
+		uint32 flags;
+
+		Common::String name;
+
+		uint32_t numChildren;
+		LabelTree *children;
+	};
+
+	struct SuperGroup {
+		SuperGroup();
+		~SuperGroup();
+
+		uint32 nameLength;
+		uint32 id;
+		uint32 unknown2;
+		Common::String name;
+
+		uint32_t numChildren;
+		LabelTree *tree;
+	};
+
+	uint32 persistFlags;
+	uint32 unknown1; // Always 0x16
+	uint32 numSuperGroups;
+	uint32 nextAvailableID;
+
+	SuperGroup *superGroups;
+
+private:
+	static DataReadErrorCode loadSuperGroup(SuperGroup &sg, DataReader &reader);
+	static DataReadErrorCode loadLabelTree(LabelTree &lt, DataReader &reader);
+
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 struct PresentationSettings : public DataObject {
 	uint32 persistFlags;
 	uint32 sizeIncludingTag;
@@ -393,6 +455,49 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct ProjectStructuralDef final : public DataObject {
+	uint32 unknown1; // Seems to always be 0x16
+	uint32 sizeIncludingTag;
+	uint32 guid;
+	uint32 structuralFlags;
+	uint16 lengthOfName;
+
+	Common::String name;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+struct SectionStructuralDef : public DataObject {
+	uint32 unknown1;
+	uint32 sizeIncludingTag;
+	uint32 guid;
+	uint16 lengthOfName;
+	uint32 structuralFlags;
+	uint16 unknown4;
+	uint16 sectionID;
+	uint32 segmentID;
+
+	Common::String name;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+struct SubsectionStructuralDef final : public DataObject {
+	uint32 unknown1;
+	uint32 sizeIncludingTag;
+	uint32 guid;
+	uint16 lengthOfName;
+	uint32 structuralFlags;
+	uint16 sectionID;
+
+	Common::String name;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 struct GlobalObjectInfo : public DataObject {
 	DataReadErrorCode load(DataReader &reader) override;
 
@@ -1014,7 +1119,7 @@ protected:
 
 struct PlugInModifierData {
 	virtual ~PlugInModifierData();
-	virtual DataReadErrorCode load(const PlugInModifier &prefix, DataReader &reader) = 0;
+	virtual DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) = 0;
 };
 
 struct PlugInModifier : public DataObject {
@@ -1048,6 +1153,7 @@ protected:
 
 struct IPlugInModifierDataFactory {
 	virtual Common::SharedPtr<Data::PlugInModifierData> createModifierData() const = 0;
+	virtual PlugIn &getPlugIn() const = 0;
 };
 
 class PlugInModifierRegistry {
diff --git a/engines/mtropolis/modifier_factory.h b/engines/mtropolis/modifier_factory.h
index fc7fb1b9dfe..d4c1786b423 100644
--- a/engines/mtropolis/modifier_factory.h
+++ b/engines/mtropolis/modifier_factory.h
@@ -60,6 +60,7 @@ public:
 
 	Common::SharedPtr<Modifier> createModifier(ModifierLoaderContext &context, const Data::PlugInModifier &plugInModifierData) const override;
 	Common::SharedPtr<Data::PlugInModifierData> createModifierData() const override;
+	PlugIn &getPlugIn() const override;
 
 private:
 	PlugIn &_plugIn;
@@ -86,6 +87,11 @@ Common::SharedPtr<Data::PlugInModifierData> PlugInModifierFactory<TModifier, TMo
 	return Common::SharedPtr<Data::PlugInModifierData>(new TModifierData());
 }
 
+template<typename TModifier, typename TModifierData>
+PlugIn& PlugInModifierFactory<TModifier, TModifierData>::getPlugIn() const {
+	return _plugIn;
+}
+
 IModifierFactory *getModifierFactoryForDataObjectType(Data::DataObjectTypes::DataObjectType dataObjectType);
 
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index 27f57ebbc1f..e03ee5b2875 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -24,6 +24,7 @@
 #include "mtropolis/runtime.h"
 
 #include "mtropolis/plugins.h"
+#include "mtropolis/plugin/standard.h"
 
 #include "common/config-manager.h"
 #include "common/macresman.h"
@@ -134,7 +135,10 @@ Common::Error MTropolisEngine::run() {
 		desc->addSegment(4, "Obsidian Data 5.MPX");
 		desc->addSegment(5, "Obsidian Data 6.MPX");
 
-		desc->addPlugIn(PlugIns::createStandard());
+		Common::SharedPtr<MTropolis::PlugIn> standardPlugIn = PlugIns::createStandard();
+		static_cast<Standard::StandardPlugIn *>(standardPlugIn.get())->getHacks().allowGarbledListModData = true;
+		desc->addPlugIn(standardPlugIn);
+
 		desc->addPlugIn(PlugIns::createObsidian());
 
 		_runtime->queueProject(desc);
@@ -156,7 +160,10 @@ Common::Error MTropolisEngine::run() {
 		for (int i = 0; i < 6; i++)
 			desc->addSegment(i, resources->getSegmentStream(i));
 
-		desc->addPlugIn(PlugIns::createStandard());
+		Common::SharedPtr<MTropolis::PlugIn> standardPlugIn = PlugIns::createStandard();
+		static_cast<Standard::StandardPlugIn *>(standardPlugIn.get())->getHacks().allowGarbledListModData = true;
+		desc->addPlugIn(standardPlugIn);
+
 		desc->addPlugIn(PlugIns::createObsidian());
 
 		desc->setResources(resPtr);
diff --git a/engines/mtropolis/plugin/obsidian_data.cpp b/engines/mtropolis/plugin/obsidian_data.cpp
index 92ea208269c..1080035a65c 100644
--- a/engines/mtropolis/plugin/obsidian_data.cpp
+++ b/engines/mtropolis/plugin/obsidian_data.cpp
@@ -27,7 +27,7 @@ namespace Data {
 
 namespace Obsidian {
 
-DataReadErrorCode MovementModifier::load(const PlugInModifier& prefix, DataReader& reader) {
+DataReadErrorCode MovementModifier::load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) {
 	if (prefix.plugInRevision != 0)
 		return kDataReadErrorUnsupportedRevision;
 
@@ -48,7 +48,7 @@ DataReadErrorCode MovementModifier::load(const PlugInModifier& prefix, DataReade
 	return kDataReadErrorNone;
 }
 
-DataReadErrorCode RectShiftModifier::load(const PlugInModifier &prefix, DataReader &reader) {
+DataReadErrorCode RectShiftModifier::load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) {
 	if (prefix.plugInRevision != 1)
 		return kDataReadErrorUnsupportedRevision;
 
diff --git a/engines/mtropolis/plugin/obsidian_data.h b/engines/mtropolis/plugin/obsidian_data.h
index b9c35007106..4c7bfb23a54 100644
--- a/engines/mtropolis/plugin/obsidian_data.h
+++ b/engines/mtropolis/plugin/obsidian_data.h
@@ -54,7 +54,7 @@ struct MovementModifier : public PlugInModifierData {
 	PlugInTypeTaggedValue unknown12Int;
 
 protected:
-	DataReadErrorCode load(const PlugInModifier &prefix, DataReader &reader) override;
+	DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override;
 };
 
 struct RectShiftModifier : public PlugInModifierData {
@@ -63,7 +63,7 @@ struct RectShiftModifier : public PlugInModifierData {
 	PlugInTypeTaggedValue unknown3Int;
 
 protected:
-	DataReadErrorCode load(const PlugInModifier &prefix, DataReader &reader) override;
+	DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override;
 };
 
 
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index c9c8951255c..024ed767071 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -126,12 +126,76 @@ bool MidiModifier::load(const PlugInModifierLoaderContext &context, const Data::
 	return true;
 }
 
+bool ListVariableModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::ListVariableModifier &data) {
+	if (!data.havePersistentData || data.numValues == 0)
+		return true;	// If the list is empty then we don't care, the actual value type is irrelevant because it can be reassigned
+
+	DynamicValueTypes::DynamicValueType expectedType = DynamicValueTypes::kInvalid;
+	switch (data.contentsType) {
+	case Data::Standard::ListVariableModifier::kContentsTypeInteger:
+		expectedType = DynamicValueTypes::kInteger;
+		break;
+	case Data::Standard::ListVariableModifier::kContentsTypePoint:
+		expectedType = DynamicValueTypes::kPoint;
+		break;
+	case Data::Standard::ListVariableModifier::kContentsTypeRange:
+		expectedType = DynamicValueTypes::kIntegerRange;
+		break;
+	case Data::Standard::ListVariableModifier::kContentsTypeFloat:
+		expectedType = DynamicValueTypes::kFloat;
+		break;
+	case Data::Standard::ListVariableModifier::kContentsTypeString:
+		expectedType = DynamicValueTypes::kString;
+		break;
+	case Data::Standard::ListVariableModifier::kContentsTypeObject:
+		if (data.persistentValuesGarbled) {
+			// Ignore and let the game fix it
+			return true;
+		} else {
+			warning("Object reference lists are not implemented");
+			return false;
+		}
+		break;
+	case Data::Standard::ListVariableModifier::kContentsTypeVector:
+		expectedType = DynamicValueTypes::kVector;
+		break;
+	case Data::Standard::ListVariableModifier::kContentsTypeBoolean:
+		expectedType = DynamicValueTypes::kBoolean;
+		break;
+	default:
+		warning("Unknown list data type");
+		return false;
+	}
+
+	for (size_t i = 0; i < data.numValues; i++) {
+		DynamicValue dynValue;
+		if (!dynValue.load(data.values[i]))
+			return false;
+
+		if (dynValue.getType() != expectedType) {
+			warning("List mod initialization element had the wrong type");
+			return false;
+		}
+
+		if (!_list.setAtIndex(i, dynValue)) {
+			warning("Failed to initialize list modifier, value was rejected");
+			return false;
+		}
+	}
+
+	return true;
+}
+
+StandardPlugInHacks::StandardPlugInHacks() : allowGarbledListModData(false) {
+}
+
 StandardPlugIn::StandardPlugIn()
 	: _cursorModifierFactory(this)
 	, _sTransCtModifierFactory(this)
 	, _mediaCueModifierFactory(this)
 	, _objRefVarModifierFactory(this)
-	, _midiModifierFactory(this) {
+	, _midiModifierFactory(this)
+	, _listVarModifierFactory(this) {
 }
 
 void StandardPlugIn::registerModifiers(IPlugInModifierRegistrar *registrar) const {
@@ -140,6 +204,15 @@ void StandardPlugIn::registerModifiers(IPlugInModifierRegistrar *registrar) cons
 	registrar->registerPlugInModifier("MediaCue", &_mediaCueModifierFactory);
 	registrar->registerPlugInModifier("ObjRefP", &_objRefVarModifierFactory);
 	registrar->registerPlugInModifier("MIDIModf", &_midiModifierFactory);
+	registrar->registerPlugInModifier("ListMod", &_listVarModifierFactory);
+}
+
+const StandardPlugInHacks &StandardPlugIn::getHacks() const {
+	return _hacks;
+}
+
+StandardPlugInHacks& StandardPlugIn::getHacks() {
+	return _hacks;
 }
 
 } // End of namespace Standard
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index db2a88829cb..a40862adaf2 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -116,18 +116,42 @@ private:
 	Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> _embeddedFile;
 };
 
+class ListVariableModifier : public Modifier {
+public:
+	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::ListVariableModifier &data);
+
+private:
+	DynamicList _list;
+};
+
+struct StandardPlugInHacks {
+	StandardPlugInHacks();
+
+	// If list mod values are illegible, just ignore them and flag it as garbled.
+	// Necessary to load object 00788ab9 (olL437Check) in Obsidian, which is supposed to be a list of
+	// 4 lists that are 3-size each, in the persistent data, but actually contains 4 identical values
+	// that appear to be garbage.
+	bool allowGarbledListModData;
+};
+
 class StandardPlugIn : public MTropolis::PlugIn {
 public:
 	StandardPlugIn();
 
 	void registerModifiers(IPlugInModifierRegistrar *registrar) const override;
 
+	const StandardPlugInHacks &getHacks() const;
+	StandardPlugInHacks &getHacks();
+
 private:
 	PlugInModifierFactory<CursorModifier, Data::Standard::CursorModifier> _cursorModifierFactory;
 	PlugInModifierFactory<STransCtModifier, Data::Standard::STransCtModifier> _sTransCtModifierFactory;
 	PlugInModifierFactory<MediaCueMessengerModifier, Data::Standard::MediaCueMessengerModifier> _mediaCueModifierFactory;
 	PlugInModifierFactory<ObjectReferenceVariableModifier, Data::Standard::ObjectReferenceVariableModifier> _objRefVarModifierFactory;
 	PlugInModifierFactory<MidiModifier, Data::Standard::MidiModifier> _midiModifierFactory;
+	PlugInModifierFactory<ListVariableModifier, Data::Standard::ListVariableModifier> _listVarModifierFactory;
+
+	StandardPlugInHacks _hacks;
 };
 
 } // End of namespace Standard
diff --git a/engines/mtropolis/plugin/standard_data.cpp b/engines/mtropolis/plugin/standard_data.cpp
index 15b2d0fdc88..e76f19be381 100644
--- a/engines/mtropolis/plugin/standard_data.cpp
+++ b/engines/mtropolis/plugin/standard_data.cpp
@@ -20,6 +20,7 @@
  */
 
 #include "mtropolis/plugin/standard_data.h"
+#include "mtropolis/plugin/standard.h"
 
 namespace MTropolis {
 
@@ -27,7 +28,7 @@ namespace Data {
 
 namespace Standard {
 
-DataReadErrorCode CursorModifier::load(const PlugInModifier &prefix, DataReader &reader) {
+DataReadErrorCode CursorModifier::load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) {
 	if (prefix.plugInRevision != 1)
 		return kDataReadErrorUnsupportedRevision;
 
@@ -38,7 +39,7 @@ DataReadErrorCode CursorModifier::load(const PlugInModifier &prefix, DataReader
 	return kDataReadErrorNone;
 }
 
-DataReadErrorCode STransCtModifier::load(const PlugInModifier &prefix, DataReader &reader) {
+DataReadErrorCode STransCtModifier::load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) {
 	if (prefix.plugInRevision != 0)
 		return kDataReadErrorUnsupportedRevision;
 
@@ -51,7 +52,7 @@ DataReadErrorCode STransCtModifier::load(const PlugInModifier &prefix, DataReade
 	return kDataReadErrorNone;
 }
 
-DataReadErrorCode MediaCueMessengerModifier::load(const PlugInModifier &prefix, DataReader &reader) {
+DataReadErrorCode MediaCueMessengerModifier::load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) {
 	if (prefix.plugInRevision != 1)
 		return kDataReadErrorUnsupportedRevision;
 
@@ -64,7 +65,7 @@ DataReadErrorCode MediaCueMessengerModifier::load(const PlugInModifier &prefix,
 	return kDataReadErrorNone;
 }
 
-DataReadErrorCode ObjectReferenceVariableModifier::load(const PlugInModifier &prefix, DataReader &reader) {
+DataReadErrorCode ObjectReferenceVariableModifier::load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) {
 	if (prefix.plugInRevision != 2)
 		return kDataReadErrorUnsupportedRevision;
 
@@ -74,7 +75,7 @@ DataReadErrorCode ObjectReferenceVariableModifier::load(const PlugInModifier &pr
 	return kDataReadErrorNone;
 }
 
-DataReadErrorCode MidiModifier::load(const PlugInModifier &prefix, DataReader &reader) {
+DataReadErrorCode MidiModifier::load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) {
 	if (prefix.plugInRevision != 1 && prefix.plugInRevision != 2)
 		return kDataReadErrorUnsupportedRevision;
 
@@ -111,6 +112,65 @@ DataReadErrorCode MidiModifier::load(const PlugInModifier &prefix, DataReader &r
 	return kDataReadErrorNone;
 }
 
+ListVariableModifier::ListVariableModifier() : values(nullptr) {
+}
+
+ListVariableModifier::~ListVariableModifier() {
+	if (values)
+		delete[] values;
+}
+
+DataReadErrorCode ListVariableModifier::load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) {
+	if (prefix.plugInRevision != 2 && prefix.plugInRevision != 3)
+		return kDataReadErrorUnsupportedRevision;
+
+	int64 privateDataPos = reader.tell();
+
+	if (!reader.readU16(unknown1) || !reader.readU32(contentsType) || !reader.readBytes(unknown2))
+		return kDataReadErrorReadFailed;
+
+	persistentValuesGarbled = false;
+
+	if (prefix.plugInRevision == 3) {
+		PlugInTypeTaggedValue persistentFlag;
+		if (!persistentFlag.load(reader) || persistentFlag.type != PlugInTypeTaggedValue::kBoolean)
+			return kDataReadErrorReadFailed;
+
+		havePersistentData = (persistentFlag.value.asBoolean != 0);
+		if (havePersistentData) {
+			PlugInTypeTaggedValue numValuesVar;
+			if (!numValuesVar.load(reader) || numValuesVar.type != PlugInTypeTaggedValue::kInteger || numValuesVar.value.asInt < 0)
+				return kDataReadErrorReadFailed;
+
+			numValues = static_cast<uint32>(numValuesVar.value.asInt);
+
+			values = new PlugInTypeTaggedValue[numValues];
+			for (size_t i = 0; i < numValues; i++) {
+				if (!values[i].load(reader)) {
+					if (static_cast<const MTropolis::Standard::StandardPlugIn &>(plugIn).getHacks().allowGarbledListModData) {
+						persistentValuesGarbled = true;
+						if (!reader.seek(privateDataPos + prefix.subObjectSize))
+							return kDataReadErrorReadFailed;
+						break;
+					} else {
+						return kDataReadErrorReadFailed;
+					}
+				}
+			}
+		} else {
+			numValues = 0;
+			values = nullptr;
+		}
+	} else {
+		havePersistentData = false;
+		numValues = 0;
+		values = nullptr;
+	}
+
+	return kDataReadErrorNone;
+}
+
+
 } // End of namespace Standard
 
 } // End of namespace Data
diff --git a/engines/mtropolis/plugin/standard_data.h b/engines/mtropolis/plugin/standard_data.h
index 9c087b6c7dc..7f219777eac 100644
--- a/engines/mtropolis/plugin/standard_data.h
+++ b/engines/mtropolis/plugin/standard_data.h
@@ -40,7 +40,7 @@ struct CursorModifier : public PlugInModifierData {
 	uint8 unknown4[4];
 
 protected:
-	DataReadErrorCode load(const PlugInModifier &prefix, DataReader &reader) override;
+	DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override;
 };
 
 struct STransCtModifier : public PlugInModifierData {
@@ -62,7 +62,7 @@ struct STransCtModifier : public PlugInModifierData {
 	uint8 unknown16[2];
 
 protected:
-	DataReadErrorCode load(const PlugInModifier &prefix, DataReader &reader) override;
+	DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override;
 };
 
 struct MediaCueMessengerModifier : public PlugInModifierData {
@@ -90,7 +90,7 @@ struct MediaCueMessengerModifier : public PlugInModifierData {
 	PlugInTypeTaggedValue triggerTiming;	// int type
 
 protected:
-	DataReadErrorCode load(const PlugInModifier &prefix, DataReader &reader) override;
+	DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override;
 };
 
 struct ObjectReferenceVariableModifier : public PlugInModifierData {
@@ -99,7 +99,7 @@ struct ObjectReferenceVariableModifier : public PlugInModifierData {
 	PlugInTypeTaggedValue objectPath;
 
 protected:
-	DataReadErrorCode load(const PlugInModifier &prefix, DataReader &reader) override;
+	DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override;
 };
 
 struct MidiModifier : public PlugInModifierData {
@@ -140,7 +140,36 @@ struct MidiModifier : public PlugInModifierData {
 	Common::SharedPtr<EmbeddedFile> embeddedFile;
 
 protected:
-	DataReadErrorCode load(const PlugInModifier &prefix, DataReader &reader) override;
+	DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override;
+};
+
+struct ListVariableModifier : public PlugInModifierData {
+	enum ContentsType {
+		kContentsTypeInteger = 1,
+		kContentsTypePoint = 2,
+		kContentsTypeRange = 3,
+		kContentsTypeFloat = 4,
+		kContentsTypeString = 5,
+		kContentsTypeObject = 6,
+		kContentsTypeVector = 8,
+		kContentsTypeBoolean = 9,
+	};
+
+	ListVariableModifier();
+	~ListVariableModifier();
+
+	uint16 unknown1;
+	uint32 contentsType;
+	uint8 unknown2[4];
+
+	bool havePersistentData;
+	uint32 numValues;
+	PlugInTypeTaggedValue *values;
+
+	bool persistentValuesGarbled;
+
+protected:
+	DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override;
 };
 
 } // End of namespace Standard
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 81e02fdaba4..7a44e20b66f 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -72,10 +72,399 @@ bool ColorRGB8::load(const Data::ColorRGB16& color) {
 MessageFlags::MessageFlags() : relay(true), cascade(true), immediate(true) {
 }
 
-DynamicValue::DynamicValue() : _type(kTypeNull) {
+
+DynamicListContainerBase::~DynamicListContainerBase() {
+}
+
+void DynamicListDefaultSetter::defaultSet(int32 &value) {
+	value = 0;
+}
+
+void DynamicListDefaultSetter::defaultSet(double &value) {
+	value = 0.0;
+}
+
+void DynamicListDefaultSetter::defaultSet(Point16 &value) {
+	value.x = 0;
+	value.y = 0;
+}
+
+void DynamicListDefaultSetter::defaultSet(IntRange &value) {
+	value.min = 0;
+	value.max = 0;
+}
+
+void DynamicListDefaultSetter::defaultSet(bool &value) {
+	value = false;
+}
+
+void DynamicListDefaultSetter::defaultSet(AngleMagVector &value) {
+	value.angleRadians = 0.0;
+	value.magnitude = 0.0;
+}
+
+void DynamicListDefaultSetter::defaultSet(Label &value) {
+	value.id = 0;
+	value.superGroupID = 0;
+}
+
+void DynamicListDefaultSetter::defaultSet(Event &value) {
+	value.eventType = 0;
+	value.eventInfo = 0;
+}
+
+void DynamicListDefaultSetter::defaultSet(Common::String &value) {
+}
+
+void DynamicListDefaultSetter::defaultSet(DynamicList &value) {
+}
+
+bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const int32 *&outPtr) {
+	if (dynValue.getType() != DynamicValueTypes::kInteger)
+		return false;
+	outPtr = &dynValue.getInt();
+	return true;
+}
+
+bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const double *&outPtr) {
+	if (dynValue.getType() != DynamicValueTypes::kFloat)
+		return false;
+	outPtr = &dynValue.getFloat();
+	return true;
+}
+
+bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const Point16 *&outPtr) {
+	if (dynValue.getType() != DynamicValueTypes::kPoint)
+		return false;
+	outPtr = &dynValue.getPoint();
+	return true;
+}
+
+bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const IntRange *&outPtr) {
+	if (dynValue.getType() != DynamicValueTypes::kIntegerRange)
+		return false;
+	outPtr = &dynValue.getIntRange();
+	return true;
+}
+
+bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const bool *&outPtr) {
+	if (dynValue.getType() != DynamicValueTypes::kBoolean)
+		return false;
+	outPtr = &dynValue.getBool();
+	return true;
+}
+
+bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const AngleMagVector *&outPtr) {
+	if (dynValue.getType() != DynamicValueTypes::kVector)
+		return false;
+	outPtr = &dynValue.getVector();
+	return true;
+}
+
+bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const Label *&outPtr) {
+	if (dynValue.getType() != DynamicValueTypes::kLabel)
+		return false;
+	outPtr = &dynValue.getLabel();
+	return true;
+}
+
+bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const Event *&outPtr) {
+	if (dynValue.getType() != DynamicValueTypes::kEvent)
+		return false;
+	outPtr = &dynValue.getEvent();
+	return true;
+}
+
+bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const Common::String *&outPtr) {
+	if (dynValue.getType() != DynamicValueTypes::kString)
+		return false;
+	outPtr = &dynValue.getString();
+	return true;
+}
+
+bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const DynamicList *&outPtr) {
+	if (dynValue.getType() != DynamicValueTypes::kBoolean)
+		return false;
+	outPtr = &dynValue.getList();
+	return true;
+}
+
+DynamicListContainer<void>::DynamicListContainer() : _size(0) {
+}
+
+bool DynamicListContainer<void>::setAtIndex(size_t index, const DynamicValue &dynValue) {
+	return true;
+}
+
+void DynamicListContainer<void>::setFrom(const DynamicListContainerBase &other) {
+	_size = other.getSize();	// ... the only thing we have anyway...
+}
+
+const void *DynamicListContainer<void>::getConstArrayPtr() const {
+	return nullptr;
+}
+
+size_t DynamicListContainer<void>::getSize() const {
+	return _size;
+}
+
+bool DynamicListContainer<void>::compareEqual(const DynamicListContainerBase &other) const {
+	return true;
+}
+
+bool DynamicListContainer<VarReference>::setAtIndex(size_t index, const DynamicValue &dynValue) {
+	if (dynValue.getType() != DynamicValueTypes::kVariableReference)
+		return false;
+
+	size_t requiredSize = index + 1;
+
+	if (_array.size() < requiredSize) {
+		size_t prevSize = _array.size();
+		_array.resize(requiredSize);
+		_strings.resize(requiredSize);
+
+		for (size_t i = prevSize; i < index; prevSize++) {
+			_array[i].guid = 0;
+		}
+
+		const VarReference &varRef = dynValue.getVarReference();
+		_array[index].guid = varRef.guid;
+		_strings[index] = *varRef.source;
+
+		rebuildStringPointers();
+	} else {
+		const VarReference &varRef = dynValue.getVarReference();
+		_array[index].guid = varRef.guid;
+		_strings[index] = *varRef.source;
+	}
+
+	return true;
+}
+
+void DynamicListContainer<VarReference>::setFrom(const DynamicListContainerBase &other) {
+	const DynamicListContainer<VarReference> &otherTyped = static_cast<const DynamicListContainer<VarReference> &>(other);
+
+	_array = otherTyped._array;
+	_strings = otherTyped._strings;
+	rebuildStringPointers();
+}
+
+const void *DynamicListContainer<VarReference>::getConstArrayPtr() const {
+	return &_array;
+}
+
+size_t DynamicListContainer<VarReference>::getSize() const {
+	return _array.size();
+}
+
+bool DynamicListContainer<VarReference>::compareEqual(const DynamicListContainerBase &other) const {
+	const DynamicListContainer<VarReference> &otherTyped = static_cast<const DynamicListContainer<VarReference> &>(other);
+	return _array == otherTyped._array;
+}
+
+void DynamicListContainer<VarReference>::rebuildStringPointers() {
+	assert(_strings.size() == _array.size());
+
+	size_t numStrings = _array.size();
+	for (size_t i = 0; i < numStrings; i++) {
+		_array[i].source = &_strings[i];
+	}
+}
+
+
+DynamicList::DynamicList() : _type(DynamicValueTypes::kEmpty), _container(nullptr) {
+}
+
+DynamicList::DynamicList(const DynamicList &other) : _type(DynamicValueTypes::kEmpty), _container(nullptr) {
+	initFromOther(other);
+}
+
+DynamicList::~DynamicList() {
+	clear();
+}
+
+DynamicValueTypes::DynamicValueType DynamicList::getType() const {
+	return _type;
+}
+
+const Common::Array<int32> &DynamicList::getInt() const {
+	assert(_type == DynamicValueTypes::kInteger);
+	return *static_cast<const Common::Array<int32> *>(_container->getConstArrayPtr());
+}
+
+const Common::Array<double> &DynamicList::getFloat() const {
+	assert(_type == DynamicValueTypes::kFloat);
+	return *static_cast<const Common::Array<double> *>(_container->getConstArrayPtr());
+}
+
+const Common::Array<Point16> &DynamicList::getPoint() const {
+	assert(_type == DynamicValueTypes::kPoint);
+	return *static_cast<const Common::Array<Point16> *>(_container->getConstArrayPtr());
+}
+
+const Common::Array<IntRange> &DynamicList::getIntRange() const {
+	assert(_type == DynamicValueTypes::kIntegerRange);
+	return *static_cast<const Common::Array<IntRange> *>(_container->getConstArrayPtr());
+}
+
+const Common::Array<AngleMagVector> &DynamicList::getVector() const {
+	assert(_type == DynamicValueTypes::kVector);
+	return *static_cast<const Common::Array<AngleMagVector> *>(_container->getConstArrayPtr());
+}
+
+const Common::Array<Label> &DynamicList::getLabel() const {
+	assert(_type == DynamicValueTypes::kLabel);
+	return *static_cast<const Common::Array<Label> *>(_container->getConstArrayPtr());
+}
+
+const Common::Array<Event> &DynamicList::getEvent() const {
+	assert(_type == DynamicValueTypes::kEvent);
+	return *static_cast<const Common::Array<Event> *>(_container->getConstArrayPtr());
+}
+
+const Common::Array<VarReference> &DynamicList::getVarReference() const {
+	assert(_type == DynamicValueTypes::kVariableReference);
+	return *static_cast<const Common::Array<VarReference> *>(_container->getConstArrayPtr());
+}
+
+const Common::Array<Common::String> &DynamicList::getString() const {
+	assert(_type == DynamicValueTypes::kString);
+	return *static_cast<const Common::Array<Common::String> *>(_container->getConstArrayPtr());
+}
+
+const Common::Array<bool> &DynamicList::getBool() const {
+	assert(_type == DynamicValueTypes::kBoolean);
+	return *static_cast<const Common::Array<bool> *>(_container->getConstArrayPtr());
+}
+
+bool DynamicList::setAtIndex(size_t index, const DynamicValue &value) {
+	if (_type != value.getType()) {
+		if (_container != nullptr && _container->getSize() != 0)
+			return false;
+		else {
+			clear();
+			changeToType(value.getType());
+			return _container->setAtIndex(index, value);
+		}
+	} else {
+		return _container->setAtIndex(index, value);
+	}
+}
+
+DynamicList &DynamicList::operator=(const DynamicList &other) {
+	if (this != &other) {
+		if (_type == DynamicValueTypes::kList && other._type == DynamicValueTypes::kList) {
+			// In this case, one operand may be inside of the other operand, so we need to copy instead of clear
+			DynamicList listClone(*this);
+			swap(listClone);
+		} else {
+			clear();
+			initFromOther(other);
+		}
+	}
+
+	return *this;
+}
+
+bool DynamicList::operator==(const DynamicList &other) const {
+	if (this == &other)
+		return true;
+
+	if (_type != other._type)
+		return false;
+
+	if (_container == nullptr)
+		return other._container == nullptr;
+
+	if (other._container == nullptr)
+		return false;	// (_container == nullptr)
+
+	return _container->compareEqual(*other._container);
+}
+
+void DynamicList::swap(DynamicList &other) {
+	if (this == &other)
+		return;
+
+	DynamicValueTypes::DynamicValueType tempType = _type;
+	_type = other._type;
+	other._type = tempType;
+
+	DynamicListContainerBase *tempContainer = _container;
+	_container = other._container;
+	other._container = tempContainer;
+}
+
+bool DynamicList::changeToType(DynamicValueTypes::DynamicValueType type) {
+	switch (type)
+	{
+	case DynamicValueTypes::kNull:
+		_container = new DynamicListContainer<void>();
+		break;
+	case DynamicValueTypes::kInteger:
+		_container = new DynamicListContainer<int32>();
+		break;
+	case DynamicValueTypes::kFloat:
+		_container = new DynamicListContainer<double>();
+		break;
+	case DynamicValueTypes::kPoint:
+		_container = new DynamicListContainer<Point16>();
+		break;
+	case DynamicValueTypes::kIntegerRange:
+		_container = new DynamicListContainer<IntRange>();
+		break;
+	case DynamicValueTypes::kBoolean:
+		_container = new DynamicListContainer<bool>();
+		break;
+	case DynamicValueTypes::kVector:
+		_container = new DynamicListContainer<AngleMagVector>();
+		break;
+	case DynamicValueTypes::kLabel:
+		_container = new DynamicListContainer<Label>();
+		break;
+	case DynamicValueTypes::kEvent:
+		_container = new DynamicListContainer<Event>();
+		break;
+	case DynamicValueTypes::kVariableReference:
+		_container = new DynamicListContainer<VarReference>();
+		break;
+	case DynamicValueTypes::kIncomingData:
+		_container = new DynamicListContainer<void>();
+		break;
+	case DynamicValueTypes::kString:
+		_container = new DynamicListContainer<Common::String>();
+		break;
+	case DynamicValueTypes::kList:
+		_container = new DynamicListContainer<DynamicList>();
+		break;
+	}
+
+	_type = type;
+
+	return true;
+}
+
+void DynamicList::clear() {
+	_type = DynamicValueTypes::kEmpty;
+	if (_container)
+		delete _container;
+	_container = nullptr;
+}
+
+void DynamicList::initFromOther(const DynamicList &other) {
+	assert(_container == nullptr);
+	assert(_type == DynamicValueTypes::kEmpty);
+
+	if (other._type != DynamicValueTypes::kEmpty) {
+		changeToType(other._type);
+		_container->setFrom(*other._container);
+	}
+}
+
+DynamicValue::DynamicValue() : _type(DynamicValueTypes::kNull) {
 }
 
-DynamicValue::DynamicValue(const DynamicValue &other) : _type(kTypeNull) {
+DynamicValue::DynamicValue(const DynamicValue &other) : _type(DynamicValueTypes::kNull) {
 	initFromOther(other);
 }
 
@@ -86,45 +475,45 @@ DynamicValue::~DynamicValue() {
 bool DynamicValue::load(const Data::InternalTypeTaggedValue &data, const Common::String &varSource, const Common::String &varString) {
 	switch (data.type) {
 	case Data::InternalTypeTaggedValue::kNull:
-		_type = kTypeNull;
+		_type = DynamicValueTypes::kNull;
 		break;
 	case Data::InternalTypeTaggedValue::kIncomingData:
-		_type = kTypeIncomingData;
+		_type = DynamicValueTypes::kIncomingData;
 		break;
 	case Data::InternalTypeTaggedValue::kInteger:
-		_type = kTypeInteger;
+		_type = DynamicValueTypes::kInteger;
 		_value.asInt = data.value.asInteger;
 		break;
 	case Data::InternalTypeTaggedValue::kString:
-		_type = kTypeString;
+		_type = DynamicValueTypes::kString;
 		_str = varString;
 		break;
 	case Data::InternalTypeTaggedValue::kPoint:
-		_type = kTypePoint;
+		_type = DynamicValueTypes::kPoint;
 		if (!_value.asPoint.load(data.value.asPoint))
 			return false;
 		break;
 	case Data::InternalTypeTaggedValue::kIntegerRange:
-		_type = KTypeIntegerRange;
+		_type = DynamicValueTypes::kIntegerRange;
 		if (!_value.asIntRange.load(data.value.asIntegerRange))
 			return false;
 		break;
 	case Data::InternalTypeTaggedValue::kFloat:
-		_type = kTypeFloat;
+		_type = DynamicValueTypes::kFloat;
 		_value.asFloat = data.value.asFloat.toDouble();
 		break;
 	case Data::InternalTypeTaggedValue::kBool:
-		_type = kTypeBoolean;
+		_type = DynamicValueTypes::kBoolean;
 		_value.asBool = (data.value.asBool != 0);
 		break;
 	case Data::InternalTypeTaggedValue::kVariableReference:
-		_type = kTypeVariableReference;
+		_type = DynamicValueTypes::kVariableReference;
 		_value.asVarReference.guid = data.value.asVariableReference.guid;
 		_value.asVarReference.source = &_str;
 		_str = varSource;
 		break;
 	case Data::InternalTypeTaggedValue::kLabel:
-		_type = kTypeLabel;
+		_type = DynamicValueTypes::kLabel;
 		if (!_value.asLabel.load(data.value.asLabel))
 			return false;
 		break;
@@ -139,48 +528,53 @@ bool DynamicValue::load(const Data::InternalTypeTaggedValue &data, const Common:
 bool DynamicValue::load(const Data::PlugInTypeTaggedValue& data) {
 	switch (data.type) {
 	case Data::PlugInTypeTaggedValue::kNull:
-		_type = kTypeNull;
+		_type = DynamicValueTypes::kNull;
 		break;
 	case Data::PlugInTypeTaggedValue::kIncomingData:
-		_type = kTypeIncomingData;
+		_type = DynamicValueTypes::kIncomingData;
 		break;
 	case Data::PlugInTypeTaggedValue::kInteger:
-		_type = kTypeInteger;
+		_type = DynamicValueTypes::kInteger;
 		_value.asInt = data.value.asInt;
 		break;
 	case Data::PlugInTypeTaggedValue::kIntegerRange:
-		_type = KTypeIntegerRange;
+		_type = DynamicValueTypes::kIntegerRange;
 		if (!_value.asIntRange.load(data.value.asIntRange))
 			return false;
 		break;
 	case Data::PlugInTypeTaggedValue::kFloat:
-		_type = kTypeFloat;
+		_type = DynamicValueTypes::kFloat;
 		_value.asFloat = data.value.asFloat.toDouble();
 		break;
 	case Data::PlugInTypeTaggedValue::kBoolean:
-		_type = kTypeBoolean;
+		_type = DynamicValueTypes::kBoolean;
 		_value.asBool = (data.value.asBoolean != 0);
 		break;
 	case Data::PlugInTypeTaggedValue::kEvent:
-		_type = kTypeEvent;
+		_type = DynamicValueTypes::kEvent;
 		if (!_value.asEvent.load(data.value.asEvent))
 			return false;
 		break;
 	case Data::PlugInTypeTaggedValue::kLabel:
-		_type = kTypeLabel;
+		_type = DynamicValueTypes::kLabel;
 		if (!_value.asLabel.load(data.value.asLabel))
 			return false;
 		break;
 	case Data::PlugInTypeTaggedValue::kString:
-		_type = kTypeString;
+		_type = DynamicValueTypes::kString;
 		_str = data.str;
 		break;
 	case Data::PlugInTypeTaggedValue::kVariableReference:
-		_type = kTypeVariableReference;
+		_type = DynamicValueTypes::kVariableReference;
 		_value.asVarReference.guid = data.value.asVarRefGUID;
 		_value.asVarReference.source = &_str;
 		_str.clear();	// Extra data doesn't seem to correlate to this
 		break;
+	case Data::PlugInTypeTaggedValue::kPoint:
+		_type = DynamicValueTypes::kPoint;
+		if (!_value.asPoint.load(data.value.asPoint))
+			return false;
+		break;
 	default:
 		assert(false);
 		return false;
@@ -190,98 +584,123 @@ bool DynamicValue::load(const Data::PlugInTypeTaggedValue& data) {
 }
 
 
-DynamicValue::Type DynamicValue::getType() const {
+DynamicValueTypes::DynamicValueType DynamicValue::getType() const {
 	return _type;
 }
 
 const int32 &DynamicValue::getInt() const {
-	assert(_type == kTypeInteger);
+	assert(_type == DynamicValueTypes::kInteger);
 	return _value.asInt;
 }
 
 const double &DynamicValue::getFloat() const {
-	assert(_type == kTypeFloat);
+	assert(_type == DynamicValueTypes::kFloat);
 	return _value.asFloat;
 }
 
 const Point16 &DynamicValue::getPoint() const {
-	assert(_type == kTypePoint);
+	assert(_type == DynamicValueTypes::kPoint);
 	return _value.asPoint;
 }
 
 const IntRange &DynamicValue::getIntRange() const {
-	assert(_type == KTypeIntegerRange);
+	assert(_type == DynamicValueTypes::kIntegerRange);
 	return _value.asIntRange;
 }
 
 const AngleMagVector &DynamicValue::getVector() const {
-	assert(_type == kTypeVector);
+	assert(_type == DynamicValueTypes::kVector);
 	return _value.asVector;
 }
 
 const Label &DynamicValue::getLabel() const {
-	assert(_type == kTypeLabel);
+	assert(_type == DynamicValueTypes::kLabel);
 	return _value.asLabel;
 }
 
 const Event &DynamicValue::getEvent() const {
-	assert(_type == kTypeEvent);
+	assert(_type == DynamicValueTypes::kEvent);
 	return _value.asEvent;
 }
 
 const VarReference &DynamicValue::getVarReference() const {
-	assert(_type == kTypeVariableReference);
+	assert(_type == DynamicValueTypes::kVariableReference);
 	return _value.asVarReference;
 }
 
 const Common::String &DynamicValue::getString() const {
-	assert(_type == kTypeString);
+	assert(_type == DynamicValueTypes::kString);
 	return _str;
 }
 
 const bool &DynamicValue::getBool() const {
-	assert(_type == kTypeBoolean);
+	assert(_type == DynamicValueTypes::kBoolean);
 	return _value.asBool;
 }
 
+const DynamicList &DynamicValue::getList() const {
+	assert(_type == DynamicValueTypes::kList);
+	return *_value.asList;
+}
+
+void DynamicValue::swap(DynamicValue &other) {
+	DynamicValueTypes::DynamicValueType tempType = _type;
+	_type = other._type;
+	other._type = tempType;
+
+	Common::String tempStr = _str;
+	_str = other._str;
+	other._str = tempStr;
+
+	ValueUnion tempValue;
+	memcpy(&tempValue, &_value, sizeof(ValueUnion));
+	memcpy(&_value, &other._value, sizeof(ValueUnion));
+	memcpy(&other._value, &tempValue, sizeof(ValueUnion));
+}
+
 DynamicValue &DynamicValue::operator=(const DynamicValue &other) {
 	if (this != &other) {
-		clear();
-		initFromOther(other);
+		DynamicValue temp(other);
+		swap(temp);
 	}
 
 	return *this;
 }
 
 bool DynamicValue::operator==(const DynamicValue &other) const {
+	if (this == &other)
+		return true;
+
 	if (_type != other._type)
 		return false;
 
 	switch (_type) {
-	case kTypeNull:
+	case DynamicValueTypes::kNull:
 		return true;
-	case kTypeInteger:
+	case DynamicValueTypes::kInteger:
 		return _value.asInt == other._value.asInt;
-	case kTypeFloat:
+	case DynamicValueTypes::kFloat:
 		return _value.asFloat == other._value.asFloat;
-	case kTypePoint:
+	case DynamicValueTypes::kPoint:
 		return _value.asPoint == other._value.asPoint;
-	case KTypeIntegerRange:
+	case DynamicValueTypes::kIntegerRange:
 		return _value.asIntRange == other._value.asIntRange;
-	case kTypeVector:
+	case DynamicValueTypes::kVector:
 		return _value.asVector == other._value.asVector;
-	case kTypeLabel:
+	case DynamicValueTypes::kLabel:
 		return _value.asLabel == other._value.asLabel;
-	case kTypeEvent:
+	case DynamicValueTypes::kEvent:
 		return _value.asEvent == other._value.asEvent;
-	case kTypeVariableReference:
+	case DynamicValueTypes::kVariableReference:
 		return _value.asVarReference == other._value.asVarReference;
-	case kTypeIncomingData:
+	case DynamicValueTypes::kIncomingData:
 		return true;
-	case kTypeString:
+	case DynamicValueTypes::kString:
 		return _str == other._str;
-	case kTypeBoolean:
+	case DynamicValueTypes::kBoolean:
 		return _value.asBool == other._value.asBool;
+	case DynamicValueTypes::kList:
+		return (*_value.asList) == (*other._value.asList);
 	default:
 		break;
 	}
@@ -291,55 +710,61 @@ bool DynamicValue::operator==(const DynamicValue &other) const {
 }
 
 void DynamicValue::clear() {
+	if (_type == DynamicValueTypes::kList)
+		delete _value.asList;
+
 	_str.clear();
-	_type = kTypeNull;
+	_type = DynamicValueTypes::kNull;
 }
 
 void DynamicValue::initFromOther(const DynamicValue& other) {
-	assert(_type == kTypeNull);
-
-	_type = other._type;
+	assert(_type == DynamicValueTypes::kNull);
 
 	switch (_type) {
-	case kTypeNull:
-	case kTypeIncomingData:
+	case DynamicValueTypes::kNull:
+	case DynamicValueTypes::kIncomingData:
 		break;
-	case kTypeInteger:
+	case DynamicValueTypes::kInteger:
 		_value.asInt = other._value.asInt;
 		break;
-	case kTypeFloat:
+	case DynamicValueTypes::kFloat:
 		_value.asFloat = other._value.asFloat;
 		break;
-	case kTypePoint:
+	case DynamicValueTypes::kPoint:
 		_value.asPoint = other._value.asPoint;
 		break;
-	case KTypeIntegerRange:
+	case DynamicValueTypes::kIntegerRange:
 		_value.asIntRange = other._value.asIntRange;
 		break;
-	case kTypeVector:
+	case DynamicValueTypes::kVector:
 		_value.asVector = other._value.asVector;
 		break;
-	case kTypeLabel:
+	case DynamicValueTypes::kLabel:
 		_value.asLabel = other._value.asLabel;
 		break;
-	case kTypeEvent:
+	case DynamicValueTypes::kEvent:
 		_value.asEvent = other._value.asEvent;
 		break;
-	case kTypeVariableReference:
+	case DynamicValueTypes::kVariableReference:
 		_value.asVarReference = other._value.asVarReference;
 		_str = other._str;
 		_value.asVarReference.source = &_str;
 		break;
-	case kTypeString:
+	case DynamicValueTypes::kString:
 		_str = other._str;
 		break;
-	case kTypeBoolean:
+	case DynamicValueTypes::kBoolean:
 		_value.asBool = other._value.asBool;
 		break;
+	case DynamicValueTypes::kList:
+		_value.asList = new DynamicList(*other._value.asList);
+		break;
 	default:
 		assert(false);
 		break;
 	}
+
+	_type = other._type;
 }
 
 MessengerSendSpec::MessengerSendSpec() : destination(0) {
@@ -455,6 +880,10 @@ const Common::Array<Common::SharedPtr<Structural> > &Structural::getChildren() c
 	return _children;
 }
 
+void Structural::addChild(const Common::SharedPtr<Structural>& child) {
+	_children.push_back(child);
+}
+
 const Common::Array<Common::SharedPtr<Modifier> > &Structural::getModifiers() const {
 	return _modifiers;
 }
@@ -522,7 +951,7 @@ const IPlugInModifierFactory *ProjectPlugInRegistry::findPlugInModifierFactory(c
 }
 
 Project::Project()
-	: _projectFormat(Data::kProjectFormatUnknown), _isBigEndian(false), _haveGlobalObjectInfo(false) {
+	: _projectFormat(Data::kProjectFormatUnknown), _isBigEndian(false), _haveGlobalObjectInfo(false), _haveProjectStructuralDef(false) {
 }
 
 Project::~Project() {
@@ -695,6 +1124,22 @@ void Project::loadBootStream(size_t streamIndex) {
 			case Data::DataObjectTypes::kGlobalObjectInfo:
 				loadGlobalObjectInfo(loaderStack, *static_cast<const Data::GlobalObjectInfo *>(dataObject.get()));
 				break;
+			case Data::DataObjectTypes::kProjectLabelMap:
+				loadLabelMap(*static_cast<const Data::ProjectLabelMap *>(dataObject.get()));
+				break;
+			case Data::DataObjectTypes::kProjectStructuralDef: {
+					if (_haveProjectStructuralDef)
+						error("Multiple project structural defs");
+
+					_haveProjectStructuralDef = true;
+
+					ChildLoaderContext loaderContext;
+					loaderContext.containerUnion.structural = this;
+					loaderContext.remainingCount = 0;
+					loaderContext.type = ChildLoaderContext::kTypeProject;
+
+					loaderStack.contexts.push_back(loaderContext);
+				} break;
 			case Data::DataObjectTypes::kStreamHeader:
 			case Data::DataObjectTypes::kUnknown19:
 				// Ignore
@@ -798,6 +1243,64 @@ Common::SharedPtr<Modifier> Project::loadModifierObject(ModifierLoaderContext &l
 	return modifier;
 }
 
+void Project::loadLabelMap(const Data::ProjectLabelMap &projectLabelMap) {
+	debug(1, "Loading label map...");
+
+	_labelSuperGroups.resize(projectLabelMap.numSuperGroups);
+
+	size_t totalLabels = 0;
+	for (size_t i = 0; i < projectLabelMap.numSuperGroups; i++) {
+		_labelSuperGroups[i].numTotalNodes = 0;
+		for (size_t j = 0; j < projectLabelMap.superGroups[i].numChildren; j++)
+			totalLabels += recursiveCountLabels(projectLabelMap.superGroups[i].tree[j]);
+	}
+
+	Common::Array<const Data::ProjectLabelMap::LabelTree *> treeQueue;
+	treeQueue.resize(totalLabels);
+	_labelTree.resize(totalLabels);
+
+	// Expand label tree into a breadth-first tree but cluster all super-groups
+	size_t insertionOffset = 0;
+	size_t dequeueOffset = 0;
+	for (size_t i = 0; i < projectLabelMap.numSuperGroups; i++) {
+		const Data::ProjectLabelMap::SuperGroup &dataSG = projectLabelMap.superGroups[i];
+		LabelSuperGroup &sg = _labelSuperGroups[i];
+
+		sg.name = dataSG.name;
+		sg.superGroupID = dataSG.id;
+
+		sg.firstRootNodeIndex = insertionOffset;
+
+		for (size_t j = 0; j < dataSG.numChildren; j++)
+			treeQueue[insertionOffset++] = &dataSG.tree[j];
+
+		while (dequeueOffset < insertionOffset) {
+			const Data::ProjectLabelMap::LabelTree &dataTree = *treeQueue[dequeueOffset];
+			LabelTree &labelTree = _labelTree[dequeueOffset];
+
+			labelTree.id = dataTree.id;
+			labelTree.name = dataTree.name;
+			dequeueOffset++;
+
+			labelTree.firstChildIndex = insertionOffset;
+			labelTree.numChildren = dataTree.numChildren;
+			for (size_t j = 0; j < dataTree.numChildren; j++)
+				treeQueue[insertionOffset++] = &dataTree.children[j];
+		}
+
+		sg.numTotalNodes = insertionOffset - sg.firstRootNodeIndex;
+	}
+
+	debug(1, "Loaded %i labels and %i supergroups", static_cast<int>(_labelTree.size()), static_cast<int>(_labelSuperGroups.size()));
+}
+
+size_t Project::recursiveCountLabels(const Data::ProjectLabelMap::LabelTree& tree) {
+	size_t numLabels = 1;	// For the node itself
+	for (size_t i = 0; i < tree.numChildren; i++)
+		numLabels += recursiveCountLabels(tree.children[i]);
+	return numLabels;
+}
+
 void Project::loadContextualObject(ChildLoaderStack &stack, const Data::DataObject &dataObject) {
 	ChildLoaderContext &topContext = stack.contexts.back();
 
@@ -813,7 +1316,67 @@ void Project::loadContextualObject(ChildLoaderStack &stack, const Data::DataObje
 			ModifierLoaderContext loaderContext(&stack);
 
 			container->appendModifier(loadModifierObject(loaderContext, dataObject));
-		}
+		} break;
+	case ChildLoaderContext::kTypeProject: {
+			Structural *project = topContext.containerUnion.structural;
+
+			if (dataObject.getType() == Data::DataObjectTypes::kSectionStructuralDef) {
+
+				const Data::SectionStructuralDef &sectionObject = static_cast<const Data::SectionStructuralDef &>(dataObject);
+
+				Common::SharedPtr<Structural> subsection(new Section());
+
+				project->addChild(subsection);
+
+				if (sectionObject.structuralFlags & Data::StructuralFlags::kNoMoreSiblings)
+					stack.contexts.pop_back();
+
+				if (sectionObject.structuralFlags & Data::StructuralFlags::kHasChildren) {
+					ChildLoaderContext loaderContext;
+					loaderContext.containerUnion.structural = subsection.get();
+					loaderContext.remainingCount = 0;
+					loaderContext.type = ChildLoaderContext::kTypeSection;
+
+					stack.contexts.push_back(loaderContext);
+				}
+			} else {
+				// Assume this is a modifier
+				ModifierLoaderContext loaderContext(&stack);
+
+				project->appendModifier(loadModifierObject(loaderContext, dataObject));
+			}
+		} break;
+	case ChildLoaderContext::kTypeSection: {
+			Structural *project = topContext.containerUnion.structural;
+
+			if (dataObject.getType() == Data::DataObjectTypes::kSubsectionStructuralDef) {
+
+				const Data::SubsectionStructuralDef &subsectionObject = static_cast<const Data::SubsectionStructuralDef &>(dataObject);
+
+				Common::SharedPtr<Structural> subsection(new Subsection());
+
+				project->addChild(subsection);
+
+				if (subsectionObject.structuralFlags & Data::StructuralFlags::kNoMoreSiblings)
+					stack.contexts.pop_back();
+
+				if (subsectionObject.structuralFlags & Data::StructuralFlags::kHasChildren) {
+					ChildLoaderContext loaderContext;
+					loaderContext.containerUnion.structural = subsection.get();
+					loaderContext.remainingCount = 0;
+					loaderContext.type = ChildLoaderContext::kTypeSubsection;
+
+					stack.contexts.push_back(loaderContext);
+				}
+			} else {
+				// Assume this is a modifier
+				ModifierLoaderContext loaderContext(&stack);
+
+				project->appendModifier(loadModifierObject(loaderContext, dataObject));
+			}
+		} break;
+	default:
+		error("Tried to load a contextual object outside of a context");
 		break;
 	}
 }
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 3c2547edae1..c00e89b1641 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -161,22 +161,195 @@ struct MessageFlags {
 	bool immediate : 1;
 };
 
-struct DynamicValue {
-	enum Type {
-		kTypeNull,
-		kTypeInteger,
-		kTypeFloat,
-		kTypePoint,
-		KTypeIntegerRange,
-		kTypeBoolean,
-		kTypeVector,
-		kTypeLabel,
-		kTypeEvent,
-		kTypeVariableReference,
-		kTypeIncomingData,
-		kTypeString,
+namespace DynamicValueTypes {
+	enum DynamicValueType {
+		kInvalid,
+
+		kNull,
+		kInteger,
+		kFloat,
+		kPoint,
+		kIntegerRange,
+		kBoolean,
+		kVector,
+		kLabel,
+		kEvent,
+		kVariableReference,
+		kIncomingData,
+		kString,
+		kList,
+
+		kEmpty,
 	};
+}
+
+struct DynamicValue;
+struct DynamicList;
+
+class DynamicListContainerBase {
+public:
+	virtual ~DynamicListContainerBase();
+	virtual bool setAtIndex(size_t index, const DynamicValue &dynValue) = 0;
+	virtual void setFrom(const DynamicListContainerBase &other) = 0;	// Only supports setting same type!
+	virtual const void *getConstArrayPtr() const = 0;
+	virtual size_t getSize() const = 0;
+	virtual bool compareEqual(const DynamicListContainerBase &other) const = 0;
+};
+
+struct DynamicListDefaultSetter {
+	static void defaultSet(int32 &value);
+	static void defaultSet(double &value);
+	static void defaultSet(Point16 &value);
+	static void defaultSet(IntRange &value);
+	static void defaultSet(bool &value);
+	static void defaultSet(AngleMagVector &value);
+	static void defaultSet(Label &value);
+	static void defaultSet(Event &value);
+	static void defaultSet(Common::String &value);
+	static void defaultSet(DynamicList &value);
+};
+
+struct DynamicListValueImporter {
+	static bool importValue(const DynamicValue &dynValue, const int32 *&outPtr);
+	static bool importValue(const DynamicValue &dynValue, const double *&outPtr);
+	static bool importValue(const DynamicValue &dynValue, const Point16 *&outPtr);
+	static bool importValue(const DynamicValue &dynValue, const IntRange *&outPtr);
+	static bool importValue(const DynamicValue &dynValue, const bool *&outPtr);
+	static bool importValue(const DynamicValue &dynValue, const AngleMagVector *&outPtr);
+	static bool importValue(const DynamicValue &dynValue, const Label *&outPtr);
+	static bool importValue(const DynamicValue &dynValue, const Event *&outPtr);
+	static bool importValue(const DynamicValue &dynValue, const Common::String *&outPtr);
+	static bool importValue(const DynamicValue &dynValue, const DynamicList *&outPtr);
+};
+
+template<class T>
+class DynamicListContainer : public DynamicListContainerBase {
+public:
+	bool setAtIndex(size_t index, const DynamicValue &dynValue) override;
+	void setFrom(const DynamicListContainerBase &other) override;
+	const void *getConstArrayPtr() const override;
+	size_t getSize() const override;
+	bool compareEqual(const DynamicListContainerBase &other) const override;
+
+private:
+	Common::Array<T> _array;
+};
+
+template<>
+class DynamicListContainer<void> : public DynamicListContainerBase {
+public:
+	DynamicListContainer();
+
+	bool setAtIndex(size_t index, const DynamicValue &dynValue) override;
+	void setFrom(const DynamicListContainerBase &other) override;
+	const void *getConstArrayPtr() const override;
+	size_t getSize() const override;
+	bool compareEqual(const DynamicListContainerBase &other) const override;
+
+public:
+	size_t _size;
+};
+
+template<>
+class DynamicListContainer<VarReference> : public DynamicListContainerBase {
+public:
+	bool setAtIndex(size_t index, const DynamicValue &dynValue) override;
+	void setFrom(const DynamicListContainerBase &other) override;
+	const void *getConstArrayPtr() const override;
+	size_t getSize() const override;
+	bool compareEqual(const DynamicListContainerBase &other) const override;
+
+private:
+	void rebuildStringPointers();
+
+	Common::Array<VarReference> _array;
+	Common::Array<Common::String> _strings;
+};
+
+template<class T>
+bool DynamicListContainer<T>::setAtIndex(size_t index, const DynamicValue &dynValue) {
+	const T *valuePtr = nullptr;
+	if (!DynamicListValueImporter::importValue(dynValue, valuePtr))
+		return false;
+
+	_array.reserve(index + 1);
+	if (_array.size() <= index) {
+		if (_array.size() < index) {
+			T defaultValue;
+			DynamicListDefaultSetter::defaultSet(defaultValue);
+			while (_array.size() < index) {
+				_array.push_back(defaultValue);
+			}
+		}
+		_array.push_back(*valuePtr);
+	} else {
+		_array[index] = *valuePtr;
+	}
 
+	return true;
+}
+
+template<class T>
+void DynamicListContainer<T>::setFrom(const DynamicListContainerBase &other) {
+	_array = static_cast<const DynamicListContainer<T> &>(other)._array;
+}
+
+template<class T>
+const void *DynamicListContainer<T>::getConstArrayPtr() const {
+	return &_array;
+}
+
+template<class T>
+size_t DynamicListContainer<T>::getSize() const {
+	return _array.size();
+}
+
+template<class T>
+bool DynamicListContainer<T>::compareEqual(const DynamicListContainerBase &other) const {
+	const DynamicListContainer<T> &otherTyped = static_cast<const DynamicListContainer<T> &>(other);
+	return _array == otherTyped._array;
+}
+
+struct DynamicList {
+	DynamicList();
+	DynamicList(const DynamicList &other);
+	~DynamicList();
+
+	DynamicValueTypes::DynamicValueType getType() const;
+
+	const Common::Array<int32> &getInt() const;
+	const Common::Array<double> &getFloat() const;
+	const Common::Array<Point16> &getPoint() const;
+	const Common::Array<IntRange> &getIntRange() const;
+	const Common::Array<AngleMagVector> &getVector() const;
+	const Common::Array<Label> &getLabel() const;
+	const Common::Array<Event> &getEvent() const;
+	const Common::Array<VarReference> &getVarReference() const;
+	const Common::Array<Common::String> &getString() const;
+	const Common::Array<bool> &getBool() const;
+
+	bool setAtIndex(size_t index, const DynamicValue &value);
+
+	DynamicList &operator=(const DynamicList &other);
+
+	bool operator==(const DynamicList &other) const;
+	inline bool operator!=(const DynamicList &other) const {
+		return !((*this) == other);
+	}
+
+	void swap(DynamicList &other);
+
+private:
+	void clear();
+	void initFromOther(const DynamicList &other);
+	bool changeToType(DynamicValueTypes::DynamicValueType type);
+
+	DynamicValueTypes::DynamicValueType _type;
+	DynamicListContainerBase *_container;
+};
+
+
+struct DynamicValue {
 	DynamicValue();
 	DynamicValue(const DynamicValue &other);
 	~DynamicValue();
@@ -184,7 +357,7 @@ struct DynamicValue {
 	bool load(const Data::InternalTypeTaggedValue &data, const Common::String &varSource, const Common::String &varString);
 	bool load(const Data::PlugInTypeTaggedValue &data);
 
-	Type getType() const;
+	DynamicValueTypes::DynamicValueType getType() const;
 
 	const int32 &getInt() const;
 	const double &getFloat() const;
@@ -196,6 +369,7 @@ struct DynamicValue {
 	const VarReference &getVarReference() const;
 	const Common::String &getString() const;
 	const bool &getBool() const;
+	const DynamicList &getList() const;
 
 	DynamicValue &operator=(const DynamicValue &other);
 
@@ -204,6 +378,8 @@ struct DynamicValue {
 		return !((*this) == other);
 	}
 
+	void swap(DynamicValue &other);
+
 private:
 	union ValueUnion {
 		double asFloat;
@@ -215,12 +391,13 @@ private:
 		Event asEvent;
 		Point16 asPoint;
 		bool asBool;
+		DynamicList *asList;
 	};
 
 	void clear();
 	void initFromOther(const DynamicValue &other);
 
-	Type _type;
+	DynamicValueTypes::DynamicValueType _type;
 	ValueUnion _value;
 	Common::String _str;
 };
@@ -342,6 +519,7 @@ public:
 	virtual ~Structural();
 
 	const Common::Array<Common::SharedPtr<Structural> > &getChildren() const;
+	void addChild(const Common::SharedPtr<Structural> &child);
 
 	const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const override;
 	void appendModifier(const Common::SharedPtr<Modifier> &modifier) override;
@@ -374,12 +552,17 @@ private:
 
 struct ChildLoaderContext {
 	enum Type {
+		kTypeUnknown,
 		kTypeModifierList,
-		kTypeStructuralList,
+		kTypeProject,
+		kTypeSection,
+		kTypeSubsection,
+		kTypeSceneRoots,
 	};
 
 	union ContainerUnion {
 		IModifierContainer *modifierContainer;
+		Structural *structural;
 	};
 
 	uint remainingCount;
@@ -414,6 +597,23 @@ public:
 	void loadFromDescription(const ProjectDescription &desc);
 
 private:
+	struct LabelSuperGroup {
+		size_t firstRootNodeIndex;
+		size_t numRootNodes;
+		size_t numTotalNodes;
+
+		uint32 superGroupID;
+		Common::String name;
+	};
+
+	struct LabelTree {
+		size_t firstChildIndex;
+		size_t numChildren;
+
+		uint32 id;
+		Common::String name;
+	};
+
 	struct Segment {
 		SegmentDescription desc;
 		Common::SharedPtr<Common::SeekableReadStream> rcStream;
@@ -452,9 +652,13 @@ private:
 	void loadGlobalObjectInfo(ChildLoaderStack &loaderStack, const Data::GlobalObjectInfo &globalObjectInfo);
 	void loadContextualObject(ChildLoaderStack &stack, const Data::DataObject &dataObject);
 	Common::SharedPtr<Modifier> loadModifierObject(ModifierLoaderContext &loaderContext, const Data::DataObject &dataObject);
+	void loadLabelMap(const Data::ProjectLabelMap &projectLabelMap);
+	static size_t recursiveCountLabels(const Data::ProjectLabelMap::LabelTree &tree);
 
 	Common::Array<Segment> _segments;
 	Common::Array<StreamDesc> _streams;
+	Common::Array<LabelTree> _labelTree;
+	Common::Array<LabelSuperGroup> _labelSuperGroups;
 	Data::ProjectFormat _projectFormat;
 	bool _isBigEndian;
 
@@ -466,6 +670,7 @@ private:
 	ProjectPresentationSettings _presentationSettings;
 
 	bool _haveGlobalObjectInfo;
+	bool _haveProjectStructuralDef;
 	SimpleModifierContainer _globalModifiers;
 
 	ProjectPlugInRegistry _plugInRegistry;


Commit: 4bb2ad12d1a308ac1982b1c67059de396bba4b41
    https://github.com/scummvm/scummvm/commit/4bb2ad12d1a308ac1982b1c67059de396bba4b41
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add remaining asset and modifier types to get Obsidian boot stream parsed

Changed paths:
  A engines/mtropolis/asset_factory.cpp
  A engines/mtropolis/asset_factory.h
  A engines/mtropolis/assets.cpp
  A engines/mtropolis/assets.h
  A engines/mtropolis/element_factory.cpp
  A engines/mtropolis/element_factory.h
  A engines/mtropolis/elements.cpp
  A engines/mtropolis/elements.h
  A engines/mtropolis/notes.txt
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/modifier_factory.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/module.mk
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/plugin/obsidian.cpp
    engines/mtropolis/plugin/obsidian.h
    engines/mtropolis/plugin/obsidian_data.cpp
    engines/mtropolis/plugin/obsidian_data.h
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/plugin/standard_data.cpp
    engines/mtropolis/plugin/standard_data.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/asset_factory.cpp b/engines/mtropolis/asset_factory.cpp
new file mode 100644
index 00000000000..7e2fbde2067
--- /dev/null
+++ b/engines/mtropolis/asset_factory.cpp
@@ -0,0 +1,70 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mtropolis/asset_factory.h"
+#include "mtropolis/assets.h"
+
+namespace MTropolis {
+
+AssetLoaderContext::AssetLoaderContext() {
+}
+
+template<typename TAsset, typename TAssetData>
+class AssetFactory : public IAssetFactory {
+public:
+	Common::SharedPtr<Asset> createAsset(AssetLoaderContext &context, const Data::DataObject &dataObject) const override;
+	static IAssetFactory *getInstance();
+
+private:
+	static AssetFactory<TAsset, TAssetData> _instance;
+};
+
+template<typename TAsset, typename TAssetData>
+Common::SharedPtr<Asset> AssetFactory<TAsset, TAssetData>::createAsset(AssetLoaderContext &context, const Data::DataObject &dataObject) const {
+	Common::SharedPtr<TAsset> asset(new TAsset());
+
+	if (!asset->load(context, static_cast<const TAssetData &>(dataObject)))
+		asset.reset();
+
+	return Common::SharedPtr<Asset>(asset);
+}
+
+template<typename TAsset, typename TAssetData>
+IAssetFactory *AssetFactory<TAsset, TAssetData>::getInstance() {
+	return &_instance;
+}
+
+template<typename TAsset, typename TAssetData>
+AssetFactory<TAsset, TAssetData> AssetFactory<TAsset, TAssetData>::_instance;
+
+IAssetFactory *getAssetFactoryForDataObjectType(const Data::DataObjectTypes::DataObjectType dataObjectType) {
+	switch (dataObjectType) {
+	case Data::DataObjectTypes::kColorTableAsset:
+		return AssetFactory<ColorTableAsset, Data::ColorTableAsset>::getInstance();
+	case Data::DataObjectTypes::kAudioAsset:
+		return AssetFactory<AudioAsset, Data::AudioAsset>::getInstance();
+
+	default:
+		return nullptr;
+	}
+}
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/asset_factory.h b/engines/mtropolis/asset_factory.h
new file mode 100644
index 00000000000..04ad2c38a50
--- /dev/null
+++ b/engines/mtropolis/asset_factory.h
@@ -0,0 +1,42 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_ASSET_FACTORY_H
+#define MTROPOLIS_ASSET_FACTORY_H
+
+#include "mtropolis/data.h"
+#include "mtropolis/runtime.h"
+
+namespace MTropolis {
+
+struct AssetLoaderContext {
+	AssetLoaderContext();
+};
+
+struct IAssetFactory {
+	virtual Common::SharedPtr<Asset> createAsset(AssetLoaderContext &context, const Data::DataObject &dataObject) const = 0;
+};
+
+IAssetFactory *getAssetFactoryForDataObjectType(Data::DataObjectTypes::DataObjectType dataObjectType);
+
+} // End of namespace MTropolis
+
+#endif
diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
new file mode 100644
index 00000000000..028a2814d8a
--- /dev/null
+++ b/engines/mtropolis/assets.cpp
@@ -0,0 +1,77 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mtropolis/assets.h"
+
+namespace MTropolis {
+
+Asset::Asset() : _assetID(0) {
+}
+
+Asset::~Asset() {
+}
+
+bool ColorTableAsset::load(AssetLoaderContext &context, const Data::ColorTableAsset &data) {
+	_assetID = data.assetID;
+	for (int i = 0; i < 256; i++) {
+		if (!_colors[i].load(data.colors[i]))
+			return false;
+	}
+
+	return true;
+}
+
+bool AudioAsset::load(AssetLoaderContext &context, const Data::AudioAsset &data) {
+	_sampleRate = data.sampleRate1;
+	_bitsPerSample = data.bitsPerSample;
+
+	switch (data.encoding1) {
+	case 0:
+		_encoding = kEncodingUncompressed;
+		break;
+	case 3:
+		_encoding = kEncodingMace3;
+		break;
+	case 4:
+		_encoding = kEncodingMace6;
+		break;
+	default:
+		return false;
+	}
+
+	_channels = data.channels;
+	// Hours Minutes Seconds Hundredths -> msec
+	// Maximum is 0x37a4f52e so this fits in 30 bits
+	_durationMSec = ((((data.codedDuration[0] * 60u) + data.codedDuration[1]) * 60u + data.codedDuration[2]) * 100u + data.codedDuration[3]) * 10u;
+	_filePosition = data.filePosition;
+	_size = data.size;
+	_cuePoints.resize(data.cuePoints.size());
+
+	for (size_t i = 0; i < _cuePoints.size(); i++) {
+		_cuePoints[i].cuePointID = data.cuePoints[i].cuePointID;
+		_cuePoints[i].position = data.cuePoints[i].position;
+	}
+
+	return true;
+}
+
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/assets.h b/engines/mtropolis/assets.h
new file mode 100644
index 00000000000..3c0a1b3a5c1
--- /dev/null
+++ b/engines/mtropolis/assets.h
@@ -0,0 +1,69 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_ASSETS_H
+#define MTROPOLIS_ASSETS_H
+
+#include "mtropolis/data.h"
+#include "mtropolis/runtime.h"
+
+namespace MTropolis {
+
+struct AssetLoaderContext;
+
+class ColorTableAsset : public Asset {
+public:
+	bool load(AssetLoaderContext &context, const Data::ColorTableAsset &data);
+
+private:
+	ColorRGB8 _colors[256];
+};
+
+class AudioAsset : public Asset {
+public:
+	struct CuePoint {
+		uint32 position;
+		uint32 cuePointID;
+	};
+
+	enum Encoding {
+		kEncodingUncompressed,
+		kEncodingMace3,
+		kEncodingMace6,
+	};
+
+	bool load(AssetLoaderContext &context, const Data::AudioAsset &data);
+
+private:
+	uint16 _sampleRate;
+	uint8 _bitsPerSample;
+	Encoding _encoding;
+	uint8 _channels;
+	uint32 _durationMSec;
+	uint32 _filePosition;
+	uint32 _size;
+
+	Common::Array<CuePoint> _cuePoints;
+};
+
+} // End of namespace MTropolis
+
+#endif
diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index edb91aa41e8..1a5be4766fa 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -29,6 +29,126 @@ namespace MTropolis {
 
 namespace Data {
 
+namespace DataObjectTypes {
+
+bool isValidSceneRootElement(DataObjectType type) {
+	switch (type) {
+	case kGraphicElement:
+	case kMovieElement:
+	case kMToonElement:
+	case kImageElement:
+		return true;
+	default:
+		return false;
+	}
+}
+
+bool isVisualElement(DataObjectType type) {
+	switch (type) {
+	case kGraphicElement:
+	case kMovieElement:
+	case kMToonElement:
+	case kImageElement:
+	case kTextLabelElement:
+		return true;
+	default:
+		return false;
+	}
+}
+
+bool isNonVisualElement(DataObjectType type) {
+	switch (type) {
+	case kSoundElement:
+		return true;
+	default:
+		return false;
+	}
+}
+
+bool isElement(DataObjectType type) {
+	switch (type) {
+	case kGraphicElement:
+	case kMovieElement:
+	case kMToonElement:
+	case kImageElement:
+	case kTextLabelElement:
+	case kSoundElement:
+		return true;
+	default:
+		return false;
+	}
+}
+
+bool isStructural(DataObjectType type) {
+	switch (type) {
+	case kProjectStructuralDef:
+	case kSectionStructuralDef:
+	case kSubsectionStructuralDef:
+		return true;
+	default:
+		return isElement(type);
+	}
+}
+
+bool isModifier(DataObjectType type) {
+	switch (type) {
+	case kAliasModifier:
+	case kChangeSceneModifier:
+	case kReturnModifier:
+	case kSoundEffectModifier:
+	case kDragMotionModifier:
+	case kPathMotionModifierV1:
+	case kPathMotionModifierV2:
+	case kVectorMotionModifier:
+	case kSceneTransitionModifier:
+	case kElementTransitionModifier:
+	case kSharedSceneModifier:
+	case kIfMessengerModifier:
+	case kBehaviorModifier:
+	case kMessengerModifier:
+	case kSetModifier:
+	case kTimerMessengerModifier:
+	case kCollisionDetectionMessengerModifier:
+	case kBoundaryDetectionMessengerModifier:
+	case kKeyboardMessengerModifier:
+	case kTextStyleModifier:
+	case kGraphicModifier:
+	case kImageEffectModifier:
+	case kMiniscriptModifier:
+	case kCursorModifierV1:
+	case kGradientModifier:
+	case kColorTableModifier:
+	case kSaveAndRestoreModifier:
+	case kCompoundVariableModifier:
+	case kBooleanVariableModifier:
+	case kIntegerVariableModifier:
+	case kIntegerRangeVariableModifier:
+	case kVectorVariableModifier:
+	case kPointVariableModifier:
+	case kFloatingPointVariableModifier:
+	case kStringVariableModifier:
+	case kPlugInModifier:
+	case kDebris:
+		return true;
+	default:
+		return false;
+	}
+}
+
+bool isAsset(DataObjectType type) {
+	switch (type) {
+	case kMovieAsset:
+	case kAudioAsset:
+	case kColorTableAsset:
+	case kImageAsset:
+	case kMToonAsset:
+		return true;
+	default:
+		return false;
+	}
+}
+
+} // End of namespace DataObjectTypes
 
 DataReader::DataReader(Common::SeekableReadStreamEndian &stream, ProjectFormat projectFormat) : _stream(stream), _projectFormat(projectFormat) {
 }
@@ -595,7 +715,7 @@ DataReadErrorCode ProjectStructuralDef::load(DataReader &reader) {
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!reader.readU32(unknown1) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid)
-		|| !reader.readU32(structuralFlags) || !reader.readU16(lengthOfName) || !reader.readTerminatedStr(name, lengthOfName))
+		|| !reader.readU32(otherFlags) || !reader.readU16(lengthOfName) || !reader.readTerminatedStr(name, lengthOfName))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
@@ -605,8 +725,8 @@ DataReadErrorCode SectionStructuralDef::load(DataReader &reader) {
 	if (_revision != 1)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!reader.readU32(unknown1) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid)
-		|| !reader.readU16(lengthOfName) || !reader.readU32(structuralFlags) || !reader.readU16(unknown4)
+	if (!reader.readU32(structuralFlags) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid)
+		|| !reader.readU16(lengthOfName) || !reader.readU32(otherFlags) || !reader.readU16(unknown4)
 		|| !reader.readU16(unknown4) || !reader.readU32(segmentID) || !reader.readTerminatedStr(name, lengthOfName))
 		return kDataReadErrorReadFailed;
 
@@ -617,19 +737,78 @@ DataReadErrorCode SubsectionStructuralDef::load(DataReader &reader) {
 	if (_revision != 0)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!reader.readU32(unknown1) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid)
-		|| !reader.readU16(lengthOfName) || !reader.readU32(structuralFlags) || !reader.readU16(sectionID)
+	if (!reader.readU32(structuralFlags) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid)
+		|| !reader.readU16(lengthOfName) || !reader.readU32(otherFlags) || !reader.readU16(sectionID)
 		|| !reader.readTerminatedStr(name, lengthOfName))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode GraphicElement::load(DataReader& reader) {
+	if (_revision != 1)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(structuralFlags) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid)
+		|| !reader.readU16(lengthOfName) || !reader.readU32(elementFlags) || !reader.readU16(layer)
+		|| !reader.readU16(sectionID) || !rect1.load(reader) || !rect2.load(reader)
+		|| !reader.readU32(streamLocator) || !reader.readBytes(unknown11) || !reader.readTerminatedStr(name, lengthOfName))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode ImageElement::load(DataReader &reader) {
+	if (_revision != 2)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(structuralFlags) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid)
+		|| !reader.readU16(lengthOfName) || !reader.readU32(elementFlags) || !reader.readU16(layer)
+		|| !reader.readU16(sectionID) || !rect1.load(reader) || !rect2.load(reader)
+		|| !reader.readU32(imageAssetID) || !reader.readU32(streamLocator) || !reader.readBytes(unknown7)
+		|| !reader.readTerminatedStr(name, lengthOfName))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode MovieElement::load(DataReader &reader) {
+	if (_revision != 2)
+		return kDataReadErrorUnsupportedRevision;
+	
+	if (!reader.readU32(structuralFlags) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid)
+		|| !reader.readU16(lengthOfName) || !reader.readU32(elementFlags) || !reader.readU16(layer)
+			|| !reader.readBytes(unknown3) || !reader.readU16(sectionID) || !reader.readBytes(unknown5)
+			|| !rect1.load(reader) || !rect2.load(reader) || !reader.readU32(assetID)
+			|| !reader.readU32(unknown7) || !reader.readU16(volume) || !reader.readU32(animationFlags)
+			|| !reader.readBytes(unknown10) || !reader.readBytes(unknown11) || !reader.readU32(streamLocator)
+			|| !reader.readBytes(unknown13) || !reader.readTerminatedStr(name, lengthOfName))
+			return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode MToonElement::load(DataReader &reader) {
+	if (_revision != 2 && _revision != 3)
+		return kDataReadErrorUnsupportedRevision;
+	
+	if (!reader.readU32(structuralFlags) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid)
+			|| !reader.readU16(lengthOfName) || !reader.readU32(elementFlags) || !reader.readU16(layer)
+			|| !reader.readU32(animationFlags) || !reader.readBytes(unknown4) || !reader.readU16(sectionID)
+			|| !rect1.load(reader) || !rect2.load(reader) || !reader.readU32(unknown5)
+			|| !reader.readU32(rateTimes10000) || !reader.readU32(streamLocator) || !reader.readU32(unknown6)
+			|| !reader.readTerminatedStr(name, lengthOfName))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode GlobalObjectInfo::load(DataReader &reader) {
 	if (_revision != 0)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!reader.readU32(persistFlags) || !reader.readU32(sizeIncludingTag) || !reader.readU16(numGlobalModifiers) || !reader.readBytes(unknown1))
+	if (!reader.readU32(persistFlags) || !reader.readU32(sizeIncludingTag) || !reader.readU16(numGlobalModifiers)
+		|| !reader.readBytes(unknown1))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
@@ -828,6 +1007,24 @@ DataReadErrorCode SetModifier::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode AliasModifier::load(DataReader& reader) {
+	if (_revision != 2)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(modifierFlags)
+		|| !reader.readU32(sizeIncludingTag)
+		|| !reader.readU16(aliasIndexPlusOne)
+		|| !reader.readU32(unknown1)
+		|| !reader.readU32(unknown2)
+		|| !reader.readU16(lengthOfName)
+		|| !editorLayoutPosition.load(reader)
+		|| !reader.readU32(guid)
+		|| !reader.readTerminatedStr(name, lengthOfName))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode ChangeSceneModifier::load(DataReader &reader) {
 	if (_revision != 0x3e9)
 		return kDataReadErrorUnsupportedRevision;
@@ -839,6 +1036,18 @@ DataReadErrorCode ChangeSceneModifier::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode SoundEffectModifier::load(DataReader &reader) {
+	if (_revision != 0x3e8)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !executeWhen.load(reader)
+		|| !terminateWhen.load(reader) || !reader.readU32(unknown2) || !reader.readBytes(unknown3)
+		|| !reader.readU32(assetID) || !reader.readBytes(unknown5))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode DragMotionModifier::load(DataReader &reader) {
 	if (_revision != 0x3e8)
 		return kDataReadErrorUnsupportedRevision;
@@ -1187,6 +1396,126 @@ DataReadErrorCode Debris::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode ColorTableAsset::load(DataReader &reader) {
+	if (_revision != 0)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(persistFlags) || !reader.readU32(sizeIncludingTag))
+		return kDataReadErrorReadFailed;
+
+	if (reader.getProjectFormat() == Data::kProjectFormatMacintosh) {
+		if (sizeIncludingTag != 0x0836)
+			return kDataReadErrorUnrecognized;
+	} else if (reader.getProjectFormat() == Data::kProjectFormatWindows) {
+		if (sizeIncludingTag != 0x0428)
+			return kDataReadErrorUnrecognized;
+	} else
+		return kDataReadErrorUnrecognized;
+
+	if (!reader.readBytes(unknown1) || !reader.readU32(assetID) || !reader.readU32(unknown2))
+		return kDataReadErrorReadFailed;
+
+	size_t numColors = 256;
+	if (reader.getProjectFormat() == Data::kProjectFormatMacintosh) {
+		if (!reader.skip(20))
+			return kDataReadErrorReadFailed;
+
+		uint8 clutHeader[8];
+		if (!reader.readBytes(clutHeader))
+			return kDataReadErrorReadFailed;
+
+		uint8 cdefBytes[256 * 8];
+		if (!reader.read(cdefBytes, numColors * 8))
+			return kDataReadErrorReadFailed;
+
+		for (size_t i = 0; i < numColors; i++) {
+			ColorRGB16 &cdef = colors[i];
+
+			const uint8 *rgb = cdefBytes + i * 8 + 2;
+			cdef.red = (rgb[0] << 8) | rgb[1];
+			cdef.green = (rgb[2] << 8) | rgb[5];
+			cdef.blue = (rgb[4] << 8) | rgb[6];
+		}
+	} else if (reader.getProjectFormat() == Data::kProjectFormatWindows) {
+		if (!reader.skip(14))
+			return kDataReadErrorReadFailed;
+
+		uint8 cdefBytes[256 * 4];
+		if (!reader.read(cdefBytes, numColors * 4))
+			return kDataReadErrorReadFailed;
+
+		for (size_t i = 0; i < numColors; i++) {
+			ColorRGB16 &cdef = colors[i];
+
+			cdef.red = cdefBytes[i * 4 + 2] * 0x101;
+			cdef.green = cdefBytes[i * 4 + 1] * 0x101;
+			cdef.blue = cdefBytes[i * 4 + 0] * 0x101;
+		}
+	} else
+		return kDataReadErrorUnrecognized;
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode AudioAsset::load(DataReader &reader) {
+	if (_revision != 2)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(persistFlags) || !reader.readU32(assetAndDataCombinedSize) || !reader.readBytes(unknown2)
+		|| !reader.readU32(assetID) || !reader.readBytes(unknown3))
+		return kDataReadErrorReadFailed;
+
+	haveMacPart = false;
+	haveWinPart = false;
+
+	if (reader.getProjectFormat() == Data::ProjectFormat::kProjectFormatMacintosh) {
+		haveMacPart = true;
+
+		if (!reader.readBytes(platform.mac.unknown4) || !reader.readU16(sampleRate1) || !reader.readBytes(platform.mac.unknown5)
+			|| !reader.readU8(bitsPerSample) || !reader.readU8(encoding1) || !reader.readU8(channels)
+			|| !reader.readBytes(codedDuration) || !reader.readBytes(platform.mac.unknown8)
+			|| !reader.readU16(sampleRate2))
+			return kDataReadErrorReadFailed;
+	} else if (reader.getProjectFormat() == Data::ProjectFormat::kProjectFormatWindows) {
+		haveWinPart = true;
+
+		if (!reader.readU16(sampleRate1) || !reader.readU8(bitsPerSample) || !reader.readBytes(platform.win.unknown9)
+			|| !reader.readU8(encoding1) || !reader.readU8(channels) || !reader.readBytes(codedDuration)
+			|| !reader.readBytes(platform.win.unknown11) || !reader.readU16(sampleRate2) || !reader.readBytes(platform.win.unknown12_1))
+			return kDataReadErrorReadFailed;
+	}
+	else
+		return kDataReadErrorUnrecognized;
+
+	if (!reader.readU32(cuePointDataSize) || !reader.readU16(numCuePoints) || !reader.readBytes(unknown14)
+		|| !reader.readU32(filePosition) || !reader.readU32(size))
+		return kDataReadErrorReadFailed;
+
+	if (numCuePoints * 14u != cuePointDataSize)
+		return kDataReadErrorUnrecognized;
+
+	cuePoints.resize(numCuePoints);
+	for (size_t i = 0; i < numCuePoints; i++)
+	{
+		CuePoint& cuePoint = cuePoints[i];
+		if (!reader.readBytes(cuePoint.unknown13) || !reader.readU32(cuePoint.unknown14) || !reader.readU32(cuePoint.position)
+			|| !reader.readU32(cuePoint.cuePointID))
+			return kDataReadErrorReadFailed;
+	}
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode AssetDataChunk::load(DataReader &reader) {
+	if (_revision != 0)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(unknown1) || !reader.readU32(sizeIncludingTag) || sizeIncludingTag < 14 || !reader.skip(sizeIncludingTag - 14))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 const IPlugInModifierDataFactory *PlugInModifierRegistry::findLoader(const char *modifierName) const {
 	Common::HashMap<Common::String, const IPlugInModifierDataFactory *>::const_iterator it = _loaders.find(modifierName);
 	if (it == _loaders.end())
@@ -1202,6 +1531,7 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 	uint32 type;
 	uint16 revision;
 	if (!reader.readU32(type) || !reader.readU16(revision)) {
+		warning("Failed to read data object header");
 		return kDataReadErrorReadFailed;
 	}
 
@@ -1239,6 +1569,26 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 	case DataObjectTypes::kSubsectionStructuralDef:
 		dataObject = new SubsectionStructuralDef();
 		break;
+
+	case DataObjectTypes::kGraphicElement:
+		dataObject = new GraphicElement();
+		break;
+	case DataObjectTypes::kMovieElement:
+		dataObject = new MovieElement();
+		break;
+	case DataObjectTypes::kMToonElement:
+		dataObject = new MToonElement();
+		break;
+	case DataObjectTypes::kImageElement:
+		dataObject = new ImageElement();
+		break;
+	case DataObjectTypes::kSoundElement:
+		//dataObject = new SoundElement();
+		break;
+	case DataObjectTypes::kTextLabelElement:
+		//dataObject = new TextLabelElement();
+		break;
+
 	case DataObjectTypes::kGlobalObjectInfo:
 		dataObject = new GlobalObjectInfo();
 		break;
@@ -1254,9 +1604,15 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 	case DataObjectTypes::kSetModifier:
 		dataObject = new SetModifier();
 		break;
+	case DataObjectTypes::kAliasModifier:
+		dataObject = new AliasModifier();
+		break;
 	case DataObjectTypes::kChangeSceneModifier:
 		dataObject = new ChangeSceneModifier();
 		break;
+	case DataObjectTypes::kSoundEffectModifier:
+		dataObject = new SoundEffectModifier();
+		break;
 	case DataObjectTypes::kDragMotionModifier:
 		dataObject = new DragMotionModifier();
 		break;
@@ -1320,18 +1676,31 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 	case DataObjectTypes::kGraphicModifier:
 		dataObject = new GraphicModifier();
 		break;
+
+	case DataObjectTypes::kColorTableAsset:
+		dataObject = new ColorTableAsset();
+		break;
+	case DataObjectTypes::kAudioAsset:
+		dataObject = new AudioAsset();
+		break;
+
+	case DataObjectTypes::kAssetDataChunk:
+		dataObject = new AssetDataChunk();
+		break;
+
 	default:
-		warning("Unrecognized data object type %x", static_cast<int>(type));
 		break;
 	}
 
 	if (dataObject == nullptr) {
+		warning("Unrecognized data object type %x", static_cast<int>(type));
 		return kDataReadErrorUnrecognized;
 	}
 
 	Common::SharedPtr<DataObject> sharedPtr(dataObject);
 	DataReadErrorCode errorCode = dataObject->load(static_cast<DataObjectTypes::DataObjectType>(type), revision, reader);
 	if (errorCode != kDataReadErrorNone) {
+		warning("Data object type %x failed to load", static_cast<int>(type));
 		outObject.reset();
 		return errorCode;
 	}
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index 3af07cf4883..97f48f2cd80 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -30,6 +30,9 @@
 #include "common/stream.h"
 
 // This contains defs related to parsing of mTropolis stored data into structured data objects.
+// This is separated from asset construction for a number of reasons, mainly that data parsing has
+// several quirky parses, and there are a lot of fields where, due to platform-specific byte
+// swaps, we know the size of the value but don't know what it means.
 namespace MTropolis {
 
 class PlugIn;
@@ -80,25 +83,17 @@ enum DataObjectType {
 	kSectionStructuralDef                = 0x3,
 	kSubsectionStructuralDef             = 0x21,
 
-	kGraphicStructuralDef                = 0x8,		// NYI
-	kMovieStructuralDef                  = 0x5,		// NYI
-	kMToonStructuralDef                  = 0x6,		// NYI
-	kImageStructuralDef                  = 0x7,		// NYI
-	kSoundStructuralDef                  = 0xa,		// NYI
-
+	kGraphicElement                      = 0x8,		// NYI
+	kMovieElement                        = 0x5,		// NYI
+	kMToonElement                        = 0x6,		// NYI
+	kImageElement                        = 0x7,		// NYI
+	kSoundElement                        = 0xa,		// NYI
 	kTextLabelElement                    = 0x15,	// NYI
 
-	kAlias                               = 0x27,	// NYI
-
-	kMovieAsset                          = 0x10,	// NYI
-	kSoundAsset                          = 0x11,	// NYI
-	kColorTableAsset                     = 0x1e,	// NYI
-	kImageAsset                          = 0xe,		// NYI
-	kMToonAsset                          = 0xf,		// NYI
-
+	kAliasModifier                       = 0x27,
 	kChangeSceneModifier                 = 0x136,
 	kReturnModifier                      = 0x140,	// NYI
-	kSoundEffectModifier                 = 0x1a4,	// NYI
+	kSoundEffectModifier                 = 0x1a4,
 	kDragMotionModifier                  = 0x208,
 	kPathMotionModifierV1                = 0x21c,	// NYI - Obsolete version
 	kPathMotionModifierV2                = 0x21b,	// NYI
@@ -131,12 +126,26 @@ enum DataObjectType {
 	kPointVariableModifier               = 0x326,
 	kFloatingPointVariableModifier       = 0x328,
 	kStringVariableModifier              = 0x329,
-
-	kDebris                              = 0xfffffffe,	// Deleted object
+	kDebris                              = 0xfffffffe,	// Deleted modifier in alias list
 	kPlugInModifier                      = 0xffffffff,
+
+	kMovieAsset                          = 0x10,	// NYI
+	kAudioAsset                          = 0x11,
+	kColorTableAsset                     = 0x1e,
+	kImageAsset                          = 0xe,		// NYI
+	kMToonAsset                          = 0xf,		// NYI
+
 	kAssetDataChunk                      = 0xffff,
 };
 
+bool isValidSceneRootElement(DataObjectType type);
+bool isVisualElement(DataObjectType type);
+bool isNonVisualElement(DataObjectType type);
+bool isStructural(DataObjectType type);
+bool isElement(DataObjectType type);
+bool isModifier(DataObjectType type);
+bool isAsset(DataObjectType type);
+
 } // End of namespace DataObjectTypes
 
 namespace StructuralFlags {
@@ -455,11 +464,15 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
-struct ProjectStructuralDef final : public DataObject {
-	uint32 unknown1; // Seems to always be 0x16
+struct StructuralDef : public DataObject {
+	uint32 structuralFlags;
+};
+
+struct ProjectStructuralDef : public DataObject {
+	uint32 unknown1; // Seems to always be 0x16 or 0x9
 	uint32 sizeIncludingTag;
 	uint32 guid;
-	uint32 structuralFlags;
+	uint32 otherFlags;
 	uint16 lengthOfName;
 
 	Common::String name;
@@ -468,12 +481,11 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
-struct SectionStructuralDef : public DataObject {
-	uint32 unknown1;
+struct SectionStructuralDef : public StructuralDef {
 	uint32 sizeIncludingTag;
 	uint32 guid;
 	uint16 lengthOfName;
-	uint32 structuralFlags;
+	uint32 otherFlags;
 	uint16 unknown4;
 	uint16 sectionID;
 	uint32 segmentID;
@@ -484,13 +496,121 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
-struct SubsectionStructuralDef final : public DataObject {
-	uint32 unknown1;
+struct SubsectionStructuralDef : public StructuralDef {
+	uint32 structuralFlags;
 	uint32 sizeIncludingTag;
 	uint32 guid;
 	uint16 lengthOfName;
-	uint32 structuralFlags;
+	uint32 otherFlags;
+	uint16 sectionID;
+
+	Common::String name;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+namespace ElementFlags {
+	enum ElementFlags {
+		kNotDirectToScreen	= 0x00001000,
+		kHidden				= 0x00008000,
+		kPaused				= 0x00010000,
+		kExpandedInEditor	= 0x00800000,
+		kCacheBitmap		= 0x02000000,
+		kSelectedInEditor	= 0x10000000,
+	};
+} // End of namespace ElementFlags
+
+namespace AnimationFlags {
+	enum AnimationFlags {
+		kAlternate      = 0x10000000,
+		kLoop           = 0x08000000,
+		kPlayEveryFrame = 0x02000000,
+	};
+}
+
+struct GraphicElement : public StructuralDef {
+	// Possible element flags: NotDirectToScreen, CacheBitmap, Hidden
+	uint32 sizeIncludingTag;
+	uint32 guid;
+	uint16 lengthOfName;
+	uint32 elementFlags;
+	uint16 layer;
+	uint16 sectionID;
+	Rect rect1;
+	Rect rect2;
+	uint32 streamLocator; // 1-based index, sometimes observed with 0x10000000 flag set, not sure of the meaning
+	uint8 unknown11[4];
+
+	Common::String name;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+struct ImageElement : public StructuralDef {
+	// Possible element flags: NotDirectToScreen, CacheBitmap, Hidden
+	uint32 sizeIncludingTag;
+	uint32 guid;
+	uint16 lengthOfName;
+	uint32 elementFlags;
+	uint16 layer;
+	uint16 sectionID;
+	Rect rect1;
+	Rect rect2;
+	uint32 imageAssetID;
+	uint32 streamLocator;
+	uint8 unknown7[4];
+
+	Common::String name;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+struct MovieElement : public StructuralDef {
+	// Possible flags: NotDirectToScreen, CacheBitmap, Hidden, Loop, Loop + Alternate, Paused
+	uint32 sizeIncludingTag;
+	uint32 guid;
+	uint16 lengthOfName;
+	uint32 elementFlags;
+	uint16 layer;
+	uint8 unknown3[44];
+	uint16 sectionID;
+	uint8 unknown5[2];
+	Rect rect1;
+	Rect rect2;
+	uint32 assetID;
+	uint32 unknown7;
+	uint16 volume;
+	uint32 animationFlags;
+	uint8 unknown10[4];
+	uint8 unknown11[4];
+	uint32 streamLocator;
+	uint8 unknown13[4];
+
+	Common::String name;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+struct MToonElement : public StructuralDef {
+	// Possible flags: NotDirectToScreen, CacheBitmap, Hidden, Loop, Paused, PlayEveryFrame (inverted as "Maintain Rate")
+	uint32 sizeIncludingTag;
+	uint32 guid;
+	uint16 lengthOfName;
+	uint32 elementFlags;
+	uint16 layer;
+	uint32 animationFlags;
+	uint8 unknown4[4];
 	uint16 sectionID;
+	Rect rect1;
+	Rect rect2;
+	uint32 unknown5;
+	uint32 rateTimes10000;
+	uint32 streamLocator;
+	uint32 unknown6;
 
 	Common::String name;
 
@@ -682,9 +802,23 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
-struct ChangeSceneModifier : public DataObject {
-	TypicalModifierHeader modHeader;
+struct AliasModifier : public DataObject {
+	uint32 modifierFlags;
+	uint32 sizeIncludingTag;
+	uint16 aliasIndexPlusOne;
+	uint32 unknown1;
+	uint32 unknown2;
+	uint16 lengthOfName;
+	uint32 guid;
+	Point editorLayoutPosition;
+
+	Common::String name;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
 
+struct ChangeSceneModifier : public DataObject {
 	enum ChangeSceneFlags {
 		kChangeSceneFlagNextScene       = 0x80000000,
 		kChangeSceneFlagPrevScene       = 0x40000000,
@@ -694,6 +828,7 @@ struct ChangeSceneModifier : public DataObject {
 		kChangeSceneFlagWrapAround      = 0x04000000,
 	};
 
+	TypicalModifierHeader modHeader;
 	uint32 changeSceneFlags;
 	Event executeWhen;
 	uint32 targetSectionGUID;
@@ -704,6 +839,22 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct SoundEffectModifier : public DataObject {
+	static const uint32 kSpecialAssetIDSystemBeep = 0xffffffffu;
+
+	TypicalModifierHeader modHeader;
+	uint8 unknown1[4];
+	Event executeWhen;
+	Event terminateWhen;
+	uint32 unknown2;
+	uint8 unknown3[4];
+	uint32 assetID;
+	uint8 unknown5[4];
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 struct DragMotionModifier : public DataObject {
 	TypicalModifierHeader modHeader;
 
@@ -1151,6 +1302,82 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct ColorTableAsset : public DataObject {
+	uint32 persistFlags;
+	uint32 sizeIncludingTag;
+	uint8 unknown1[4];
+	uint32 assetID;
+	uint32 unknown2; // Usually zero-fill but sometimes contains 0xb
+
+	ColorRGB16 colors[256];
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+struct AudioAsset : public DataObject {
+	struct MacPart {
+		uint8 unknown4[4];
+		uint8 unknown5[5];
+		uint8 unknown6[3];
+		uint8 unknown8[20];
+	};
+
+	struct WinPart {
+		uint8 unknown9[3];
+		uint8 unknown10[3];
+		uint8 unknown11[18];
+		uint8 unknown12_1[2];
+	};
+
+	union PlatformPart {
+		MacPart mac;
+		WinPart win;
+	};
+
+	struct CuePoint {
+		uint8 unknown13[2];
+		uint32 unknown14;
+		uint32 position;
+		uint32 cuePointID;
+	};
+
+	uint32 persistFlags;
+	uint32 assetAndDataCombinedSize;
+	uint8 unknown2[4];
+	uint32 assetID;
+	uint8 unknown3[20];
+	uint16 sampleRate1;
+	uint8 bitsPerSample;
+	uint8 encoding1;
+	uint8 channels;
+	uint8 codedDuration[4];
+	uint16 sampleRate2;
+	uint32 cuePointDataSize;
+	uint16 numCuePoints;
+	uint8 unknown14[4];
+	uint32 filePosition;
+	uint32 size;
+
+	Common::Array<CuePoint> cuePoints;
+
+	bool haveMacPart;
+	bool haveWinPart;
+	PlatformPart platform;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+struct AssetDataChunk : public DataObject {
+	uint32 unknown1;
+	uint32 sizeIncludingTag;
+	int64 filePosition;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 struct IPlugInModifierDataFactory {
 	virtual Common::SharedPtr<Data::PlugInModifierData> createModifierData() const = 0;
 	virtual PlugIn &getPlugIn() const = 0;
diff --git a/engines/mtropolis/element_factory.cpp b/engines/mtropolis/element_factory.cpp
new file mode 100644
index 00000000000..b8e9d03b92b
--- /dev/null
+++ b/engines/mtropolis/element_factory.cpp
@@ -0,0 +1,69 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mtropolis/element_factory.h"
+#include "mtropolis/elements.h"
+#include "mtropolis/runtime.h"
+
+namespace MTropolis {
+
+ElementLoaderContext::ElementLoaderContext() {
+}
+
+template<typename TElement, typename TElementData>
+class ElementFactory : public IElementFactory {
+public:
+	Common::SharedPtr<Element> createElement(ElementLoaderContext &context, const Data::DataObject &dataObject) const override;
+	static IElementFactory *getInstance();
+
+private:
+	static ElementFactory<TElement, TElementData> _instance;
+};
+
+template<typename TElement, typename TElementData>
+Common::SharedPtr<Element> ElementFactory<TElement, TElementData>::createElement(ElementLoaderContext &context, const Data::DataObject &dataObject) const {
+	Common::SharedPtr<TElement> element(new TElement());
+
+	if (!element->load(context, static_cast<const TElementData &>(dataObject)))
+		element.reset();
+
+	return Common::SharedPtr<Element>(element);
+}
+
+template<typename TElement, typename TElementData>
+IElementFactory *ElementFactory<TElement, TElementData>::getInstance() {
+	return &_instance;
+}
+
+template<typename TElement, typename TElementData>
+ElementFactory<TElement, TElementData> ElementFactory<TElement, TElementData>::_instance;
+
+IElementFactory *getElementFactoryForDataObjectType(const Data::DataObjectTypes::DataObjectType dataObjectType) {
+	switch (dataObjectType) {
+	case Data::DataObjectTypes::kGraphicElement:
+		return ElementFactory<GraphicElement, Data::GraphicElement>::getInstance();
+
+	default:
+		return nullptr;
+	}
+}
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/element_factory.h b/engines/mtropolis/element_factory.h
new file mode 100644
index 00000000000..581b9a1c731
--- /dev/null
+++ b/engines/mtropolis/element_factory.h
@@ -0,0 +1,42 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_ELEMENT_FACTORY_H
+#define MTROPOLIS_ELEMENT_FACTORY_H
+
+#include "mtropolis/data.h"
+#include "mtropolis/runtime.h"
+
+namespace MTropolis {
+
+struct ElementLoaderContext {
+	ElementLoaderContext();
+};
+
+struct IElementFactory {
+	virtual Common::SharedPtr<Element> createElement(ElementLoaderContext &context, const Data::DataObject &dataObject) const = 0;
+};
+
+IElementFactory *getElementFactoryForDataObjectType(Data::DataObjectTypes::DataObjectType dataObjectType);
+
+} // End of namespace MTropolis
+
+#endif
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
new file mode 100644
index 00000000000..4a10c4acbd1
--- /dev/null
+++ b/engines/mtropolis/elements.cpp
@@ -0,0 +1,42 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mtropolis/elements.h"
+
+namespace MTropolis {
+
+GraphicElement::GraphicElement() : _directToScreen(false), _cacheBitmap(false) {
+}
+
+GraphicElement::~GraphicElement() {
+}
+
+bool GraphicElement::load(ElementLoaderContext &context, const Data::GraphicElement &data) {
+	if (!loadCommon(data.name, data.guid, data.rect1, data.elementFlags, data.layer, data.streamLocator, data.sectionID))
+		return false;
+
+	_directToScreen = ((data.elementFlags & Data::ElementFlags::kNotDirectToScreen) == 0);
+	_cacheBitmap = ((data.elementFlags & Data::ElementFlags::kCacheBitmap) != 0);
+
+	return true;
+}
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
new file mode 100644
index 00000000000..5a9c20be913
--- /dev/null
+++ b/engines/mtropolis/elements.h
@@ -0,0 +1,46 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_ELEMENTS_H
+#define MTROPOLIS_ELEMENTS_H
+
+#include "mtropolis/data.h"
+#include "mtropolis/runtime.h"
+
+namespace MTropolis {
+
+struct ElementLoaderContext;
+
+class GraphicElement : public VisualElement {
+public:
+	GraphicElement();
+	~GraphicElement();
+
+	bool load(ElementLoaderContext &context, const Data::GraphicElement &data);
+
+private:
+	bool _directToScreen;
+	bool _cacheBitmap;
+};
+
+} // End of namespace MTropolis
+
+#endif
diff --git a/engines/mtropolis/modifier_factory.cpp b/engines/mtropolis/modifier_factory.cpp
index 185ab63a1ef..0d79bbc5048 100644
--- a/engines/mtropolis/modifier_factory.cpp
+++ b/engines/mtropolis/modifier_factory.cpp
@@ -65,8 +65,12 @@ IModifierFactory *getModifierFactoryForDataObjectType(const Data::DataObjectType
 		return ModifierFactory<BehaviorModifier, Data::BehaviorModifier>::getInstance();
 	case Data::DataObjectTypes::kMiniscriptModifier:
 		return ModifierFactory<MiniscriptModifier, Data::MiniscriptModifier>::getInstance();
+	case Data::DataObjectTypes::kAliasModifier:
+		return ModifierFactory<AliasModifier, Data::AliasModifier>::getInstance();
 	case Data::DataObjectTypes::kChangeSceneModifier:
 		return ModifierFactory<ChangeSceneModifier, Data::ChangeSceneModifier>::getInstance();
+	case Data::DataObjectTypes::kSoundEffectModifier:
+		return ModifierFactory<SoundEffectModifier, Data::SoundEffectModifier>::getInstance();
 	case Data::DataObjectTypes::kDragMotionModifier:
 		return ModifierFactory<DragMotionModifier, Data::DragMotionModifier>::getInstance();
 	case Data::DataObjectTypes::kVectorMotionModifier:
@@ -111,6 +115,7 @@ IModifierFactory *getModifierFactoryForDataObjectType(const Data::DataObjectType
 		return ModifierFactory<StringVariableModifier, Data::StringVariableModifier>::getInstance();
 
 	default:
+		warning("No modifier factory for type %x", static_cast<int>(dataObjectType));
 		return nullptr;
 	}
 }
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 6e968aa0637..55ced21977c 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -88,6 +88,17 @@ bool SetModifier::load(ModifierLoaderContext &context, const Data::SetModifier &
 	return true;
 }
 
+bool AliasModifier::load(ModifierLoaderContext &context, const Data::AliasModifier &data) {
+	_guid = data.guid;
+	if (!_modifierFlags.load(data.modifierFlags))
+		return false;
+	_name = data.name;
+
+	_aliasID = data.aliasIndexPlusOne;
+
+	return true;
+}
+
 bool ChangeSceneModifier::load(ModifierLoaderContext& context, const Data::ChangeSceneModifier& data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -111,6 +122,24 @@ bool ChangeSceneModifier::load(ModifierLoaderContext& context, const Data::Chang
 	return true;
 }
 
+bool SoundEffectModifier::load(ModifierLoaderContext &context, const Data::SoundEffectModifier &data) {
+	if (!loadTypicalHeader(data.modHeader))
+		return false;
+
+	if (!_executeWhen.load(data.executeWhen) || !_terminateWhen.load(data.executeWhen))
+		return false;
+
+	if (data.assetID == Data::SoundEffectModifier::kSpecialAssetIDSystemBeep) {
+		_soundType = kSoundTypeBeep;
+		_assetID = 0;
+	} else {
+		_soundType = kSoundTypeAudioAsset;
+		_assetID = data.assetID;
+	}
+
+	return true;
+}
+
 bool DragMotionModifier::load(ModifierLoaderContext &context, const Data::DragMotionModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 2ce8a10c99a..2c80fd1e593 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -72,6 +72,14 @@ private:
 	DynamicValue _target;
 };
 
+class AliasModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::AliasModifier &data);
+
+private:
+	uint32 _aliasID;
+};
+
 class ChangeSceneModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::ChangeSceneModifier &data);
@@ -93,6 +101,23 @@ private:
 	bool _addToWrapAround;
 };
 
+class SoundEffectModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::SoundEffectModifier &data);
+
+private:
+	enum SoundType {
+		kSoundTypeBeep,
+		kSoundTypeAudioAsset,
+	};
+
+	Event _executeWhen;
+	Event _terminateWhen;
+
+	SoundType _soundType;
+	uint32 _assetID;
+};
+
 class DragMotionModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::DragMotionModifier &data);
diff --git a/engines/mtropolis/module.mk b/engines/mtropolis/module.mk
index e44198f1243..cd6dd681604 100644
--- a/engines/mtropolis/module.mk
+++ b/engines/mtropolis/module.mk
@@ -1,9 +1,12 @@
 MODULE := engines/mtropolis
 
 MODULE_OBJS = \
+	asset_factory.o \
 	console.o \
 	data.o \
 	detection.o \
+	element_factory.o \
+	elements.o \
 	metaengine.o \
 	miniscript.o \
 	modifiers.o \
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index e03ee5b2875..6d83115bfe0 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -27,6 +27,7 @@
 #include "mtropolis/plugin/standard.h"
 
 #include "common/config-manager.h"
+#include "common/debug.h"
 #include "common/macresman.h"
 #include "common/ptr.h"
 #include "common/stuffit.h"
@@ -56,6 +57,8 @@ MacObsidianResources::MacObsidianResources() : _installerArchive(nullptr), _inst
 }
 
 void MacObsidianResources::setup() {
+	debug(1, "Opening Obsidian Mac installer package...");
+
 	if (!_installerResMan.open("Obsidian Installer"))
 		error("Failed to open Obsidian Installer");
 
@@ -68,8 +71,10 @@ void MacObsidianResources::setup() {
 	if (!_installerArchive)
 		error("Failed to open Obsidian Installer archive");
 
+	debug(1, "Reading data from installer...");
 	_segmentStreams[0] = _installerArchive->createReadStreamForMember("Obsidian Data 1");
 
+	debug("Opening data segments...");
 	for (int i = 0; i < 5; i++) {
 		char fileName[32];
 		sprintf(fileName, "Obsidian Data %i", (i + 2));
@@ -97,7 +102,6 @@ MacObsidianResources::~MacObsidianResources() {
 	delete _installerDataForkStream;
 }
 
-
 MTropolisEngine::MTropolisEngine(OSystem *syst, const MTropolisGameDescription *gameDesc) : Engine(syst), _gameDescription(gameDesc) {
 
 	if (gameDesc->gameID == GID_OBSIDIAN && _gameDescription->desc.platform == Common::kPlatformWindows) {
diff --git a/engines/mtropolis/notes.txt b/engines/mtropolis/notes.txt
new file mode 100644
index 00000000000..6fb11ad3173
--- /dev/null
+++ b/engines/mtropolis/notes.txt
@@ -0,0 +1,14 @@
+GUID resolution and structure loading:
+
+Loading of objects in the mTropolis backend is done in multiple stages due to
+the complex GUID resolution process, inline assets, and other things.  Here's
+
+The first step to loading is to load data objects.  Data objects are JUST data,
+but due to weird aspects of the data loading process (like the fact that asset
+defs can appear anywhere) it's separate.
+
+The second step is conversion of data objects into runtime objects and
+formation of the scene structure.
+
+The third step is "materialization" which performs two tasks: First, it clones
+all aliased non-variable modifiers.  Second, it remaps all GUIDs to objects.
diff --git a/engines/mtropolis/plugin/obsidian.cpp b/engines/mtropolis/plugin/obsidian.cpp
index aa7ac255c76..e723e9bea0a 100644
--- a/engines/mtropolis/plugin/obsidian.cpp
+++ b/engines/mtropolis/plugin/obsidian.cpp
@@ -34,12 +34,18 @@ bool RectShiftModifier::load(const PlugInModifierLoaderContext &context, const D
 	return true;
 }
 
-ObsidianPlugIn::ObsidianPlugIn() : _movementModifierFactory(this), _rectShiftModifierFactory(this) {
+bool TextWorkModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::TextWorkModifier &data) {
+	return true;
+}
+
+
+ObsidianPlugIn::ObsidianPlugIn() : _movementModifierFactory(this), _rectShiftModifierFactory(this), _textWorkModifierFactory(this) {
 }
 
 void ObsidianPlugIn::registerModifiers(IPlugInModifierRegistrar *registrar) const {
 	registrar->registerPlugInModifier("Movement", &_movementModifierFactory);
 	registrar->registerPlugInModifier("rectshift", &_rectShiftModifierFactory);
+	registrar->registerPlugInModifier("TextWork", &_textWorkModifierFactory);
 }
 
 } // End of namespace ObsidianPlugIn
diff --git a/engines/mtropolis/plugin/obsidian.h b/engines/mtropolis/plugin/obsidian.h
index bf848b54f2a..f6b9065bb7b 100644
--- a/engines/mtropolis/plugin/obsidian.h
+++ b/engines/mtropolis/plugin/obsidian.h
@@ -41,6 +41,11 @@ public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Obsidian::RectShiftModifier &data);
 };
 
+class TextWorkModifier : public Modifier {
+public:
+	bool load(const PlugInModifierLoaderContext &context, const Data::Obsidian::TextWorkModifier &data);
+};
+
 class ObsidianPlugIn : public MTropolis::PlugIn {
 public:
 	ObsidianPlugIn();
@@ -50,6 +55,7 @@ public:
 private:
 	PlugInModifierFactory<MovementModifier, Data::Obsidian::MovementModifier> _movementModifierFactory;
 	PlugInModifierFactory<RectShiftModifier, Data::Obsidian::RectShiftModifier> _rectShiftModifierFactory;
+	PlugInModifierFactory<TextWorkModifier, Data::Obsidian::TextWorkModifier> _textWorkModifierFactory;
 };
 
 } // End of namespace Obsidian
diff --git a/engines/mtropolis/plugin/obsidian_data.cpp b/engines/mtropolis/plugin/obsidian_data.cpp
index 1080035a65c..7aec2579487 100644
--- a/engines/mtropolis/plugin/obsidian_data.cpp
+++ b/engines/mtropolis/plugin/obsidian_data.cpp
@@ -58,6 +58,13 @@ DataReadErrorCode RectShiftModifier::load(PlugIn &plugIn, const PlugInModifier &
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode TextWorkModifier::load(PlugIn& plugIn, const PlugInModifier& prefix, DataReader& reader) {
+	if (prefix.plugInRevision != 1)
+		return kDataReadErrorUnsupportedRevision;
+
+	return kDataReadErrorNone;
+}
+
 } // End of namespace Obsidian
 
 } // End of namespace Data
diff --git a/engines/mtropolis/plugin/obsidian_data.h b/engines/mtropolis/plugin/obsidian_data.h
index 4c7bfb23a54..2fedd316d23 100644
--- a/engines/mtropolis/plugin/obsidian_data.h
+++ b/engines/mtropolis/plugin/obsidian_data.h
@@ -66,6 +66,10 @@ protected:
 	DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override;
 };
 
+struct TextWorkModifier : public PlugInModifierData {
+protected:
+	DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override;
+};
 
 } // End of namespace Obsidian
 
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 024ed767071..5ec7587ad69 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -186,6 +186,10 @@ bool ListVariableModifier::load(const PlugInModifierLoaderContext &context, cons
 	return true;
 }
 
+bool SysInfoModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::SysInfoModifier &data) {
+	return true;
+}
+
 StandardPlugInHacks::StandardPlugInHacks() : allowGarbledListModData(false) {
 }
 
@@ -195,7 +199,8 @@ StandardPlugIn::StandardPlugIn()
 	, _mediaCueModifierFactory(this)
 	, _objRefVarModifierFactory(this)
 	, _midiModifierFactory(this)
-	, _listVarModifierFactory(this) {
+	, _listVarModifierFactory(this)
+	, _sysInfoModifierFactory(this) {
 }
 
 void StandardPlugIn::registerModifiers(IPlugInModifierRegistrar *registrar) const {
@@ -205,6 +210,7 @@ void StandardPlugIn::registerModifiers(IPlugInModifierRegistrar *registrar) cons
 	registrar->registerPlugInModifier("ObjRefP", &_objRefVarModifierFactory);
 	registrar->registerPlugInModifier("MIDIModf", &_midiModifierFactory);
 	registrar->registerPlugInModifier("ListMod", &_listVarModifierFactory);
+	registrar->registerPlugInModifier("SysInfo", &_sysInfoModifierFactory);
 }
 
 const StandardPlugInHacks &StandardPlugIn::getHacks() const {
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index a40862adaf2..7a451272dce 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -124,6 +124,11 @@ private:
 	DynamicList _list;
 };
 
+class SysInfoModifier : public Modifier {
+public:
+	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::SysInfoModifier &data);
+};
+
 struct StandardPlugInHacks {
 	StandardPlugInHacks();
 
@@ -150,6 +155,7 @@ private:
 	PlugInModifierFactory<ObjectReferenceVariableModifier, Data::Standard::ObjectReferenceVariableModifier> _objRefVarModifierFactory;
 	PlugInModifierFactory<MidiModifier, Data::Standard::MidiModifier> _midiModifierFactory;
 	PlugInModifierFactory<ListVariableModifier, Data::Standard::ListVariableModifier> _listVarModifierFactory;
+	PlugInModifierFactory<SysInfoModifier, Data::Standard::SysInfoModifier> _sysInfoModifierFactory;
 
 	StandardPlugInHacks _hacks;
 };
diff --git a/engines/mtropolis/plugin/standard_data.cpp b/engines/mtropolis/plugin/standard_data.cpp
index e76f19be381..a70509800fc 100644
--- a/engines/mtropolis/plugin/standard_data.cpp
+++ b/engines/mtropolis/plugin/standard_data.cpp
@@ -170,6 +170,12 @@ DataReadErrorCode ListVariableModifier::load(PlugIn &plugIn, const PlugInModifie
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode SysInfoModifier::load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) {
+	if (prefix.plugInRevision != 0)
+		return kDataReadErrorUnsupportedRevision;
+
+	return kDataReadErrorNone;
+}
 
 } // End of namespace Standard
 
diff --git a/engines/mtropolis/plugin/standard_data.h b/engines/mtropolis/plugin/standard_data.h
index 7f219777eac..9863f492d86 100644
--- a/engines/mtropolis/plugin/standard_data.h
+++ b/engines/mtropolis/plugin/standard_data.h
@@ -172,6 +172,11 @@ protected:
 	DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override;
 };
 
+struct SysInfoModifier : public PlugInModifierData {
+protected:
+	DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override;
+};
+
 } // End of namespace Standard
 
 } // End of namespace Data
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 7a44e20b66f..0142e8436de 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -22,6 +22,8 @@
 #include "mtropolis/runtime.h"
 #include "mtropolis/data.h"
 #include "mtropolis/vthread.h"
+#include "mtropolis/asset_factory.h"
+#include "mtropolis/element_factory.h"
 #include "mtropolis/modifier_factory.h"
 
 #include "common/debug.h"
@@ -870,6 +872,9 @@ void SimpleModifierContainer::appendModifier(const Common::SharedPtr<Modifier> &
 	_modifiers.push_back(modifier);
 }
 
+Structural::Structural() : _guid(0) {
+}
+
 Structural::~Structural() {
 }
 
@@ -884,6 +889,10 @@ void Structural::addChild(const Common::SharedPtr<Structural>& child) {
 	_children.push_back(child);
 }
 
+const Common::String &Structural::getName() const {
+	return _name;
+}
+
 const Common::Array<Common::SharedPtr<Modifier> > &Structural::getModifiers() const {
 	return _modifiers;
 }
@@ -931,6 +940,9 @@ const Common::Array<Common::SharedPtr<Modifier> > &IModifierContainer::getModifi
 	return const_cast<IModifierContainer &>(*this).getModifiers();
 };
 
+ChildLoaderContext::ChildLoaderContext() : remainingCount(0), type(kTypeUnknown) {
+}
+
 ProjectPlugInRegistry::ProjectPlugInRegistry() {
 }
 
@@ -1062,6 +1074,12 @@ void Project::loadFromDescription(const ProjectDescription& desc) {
 	debug(1, "Loading boot stream");
 
 	loadBootStream(bootStreamIndex);
+
+	debug(1, "Boot stream loaded successfully, materializing project...");
+
+	error("TODO");
+
+	debug(1, "Materialized project OK, project is loaded!");
 }
 
 void Project::openSegmentStream(int segmentIndex) {
@@ -1098,11 +1116,12 @@ void Project::loadBootStream(size_t streamIndex) {
 	Data::DataReader reader(stream, _projectFormat);
 
 	ChildLoaderStack loaderStack;
+	AssetDefLoaderContext assetDefLoader;
 
 	const Data::PlugInModifierRegistry &plugInDataLoaderRegistry = _plugInRegistry.getDataLoaderRegistry();
 
 	size_t numObjectsLoaded = 0;
-	for (;;) {
+	while (stream.pos() != streamDesc.size) {
 		Common::SharedPtr<Data::DataObject> dataObject;
 		Data::loadDataObject(plugInDataLoaderRegistry, reader, dataObject);
 
@@ -1110,7 +1129,15 @@ void Project::loadBootStream(size_t streamIndex) {
 			error("Failed to load project boot data");
 		}
 
-		if (loaderStack.contexts.size() > 0) {
+		Data::DataObjectTypes::DataObjectType dataObjectType = dataObject->getType();
+
+		if (Data::DataObjectTypes::isAsset(dataObjectType)) {
+			// Asset defs can appear anywhere
+			loadAssetDef(assetDefLoader, *dataObject.get());
+		} else if (dataObjectType == Data::DataObjectTypes::kAssetDataChunk) {
+			// Ignore
+			continue;
+		} else if (loaderStack.contexts.size() > 0) {
 			loadContextualObject(loaderStack, *dataObject.get());
 		} else {
 			// Root-level objects
@@ -1303,6 +1330,7 @@ size_t Project::recursiveCountLabels(const Data::ProjectLabelMap::LabelTree& tre
 
 void Project::loadContextualObject(ChildLoaderStack &stack, const Data::DataObject &dataObject) {
 	ChildLoaderContext &topContext = stack.contexts.back();
+	const Data::DataObjectTypes::DataObjectType dataObjectType = dataObject.getType();
 
 	// The stack entry must always be popped before loading the object because the load process may descend into more children,
 	// such as when behaviors are nested.
@@ -1320,59 +1348,103 @@ void Project::loadContextualObject(ChildLoaderStack &stack, const Data::DataObje
 	case ChildLoaderContext::kTypeProject: {
 			Structural *project = topContext.containerUnion.structural;
 
-			if (dataObject.getType() == Data::DataObjectTypes::kSectionStructuralDef) {
+			if (dataObjectType == Data::DataObjectTypes::kSectionStructuralDef) {
 
 				const Data::SectionStructuralDef &sectionObject = static_cast<const Data::SectionStructuralDef &>(dataObject);
 
-				Common::SharedPtr<Structural> subsection(new Section());
+				Common::SharedPtr<Structural> section(new Section());
+				if (!static_cast<Section *>(section.get())->load(sectionObject))
+					error("Failed to load section");
 
-				project->addChild(subsection);
+				project->addChild(section);
 
-				if (sectionObject.structuralFlags & Data::StructuralFlags::kNoMoreSiblings)
-					stack.contexts.pop_back();
+				// For some reason all section objects have the "no more siblings" structural flag.
+				// There doesn't appear to be any indication of how many section objects there will
+				// be either.
+				//if (sectionObject.structuralFlags & Data::StructuralFlags::kNoMoreSiblings)
+				//	stack.contexts.pop_back();
 
 				if (sectionObject.structuralFlags & Data::StructuralFlags::kHasChildren) {
 					ChildLoaderContext loaderContext;
-					loaderContext.containerUnion.structural = subsection.get();
+					loaderContext.containerUnion.structural = section.get();
 					loaderContext.remainingCount = 0;
 					loaderContext.type = ChildLoaderContext::kTypeSection;
 
 					stack.contexts.push_back(loaderContext);
 				}
-			} else {
-				// Assume this is a modifier
+			} else if (Data::DataObjectTypes::isModifier(dataObjectType)) {
 				ModifierLoaderContext loaderContext(&stack);
-
 				project->appendModifier(loadModifierObject(loaderContext, dataObject));
+			} else {
+				error("Unexpected object type in this context");
 			}
 		} break;
 	case ChildLoaderContext::kTypeSection: {
-			Structural *project = topContext.containerUnion.structural;
+			Structural *section = topContext.containerUnion.structural;
 
 			if (dataObject.getType() == Data::DataObjectTypes::kSubsectionStructuralDef) {
-
 				const Data::SubsectionStructuralDef &subsectionObject = static_cast<const Data::SubsectionStructuralDef &>(dataObject);
 
 				Common::SharedPtr<Structural> subsection(new Subsection());
+				if (!static_cast<Subsection *>(subsection.get())->load(subsectionObject))
+					error("Failed to load subsection");
 
-				project->addChild(subsection);
+				section->addChild(subsection);
 
 				if (subsectionObject.structuralFlags & Data::StructuralFlags::kNoMoreSiblings)
 					stack.contexts.pop_back();
 
 				if (subsectionObject.structuralFlags & Data::StructuralFlags::kHasChildren) {
 					ChildLoaderContext loaderContext;
-					loaderContext.containerUnion.structural = subsection.get();
+					loaderContext.containerUnion.filteredElements.structural = subsection.get();
+					loaderContext.containerUnion.filteredElements.filterFunc = Data::DataObjectTypes::isValidSceneRootElement;
 					loaderContext.remainingCount = 0;
-					loaderContext.type = ChildLoaderContext::kTypeSubsection;
+					loaderContext.type = ChildLoaderContext::kTypeFilteredElements;
 
 					stack.contexts.push_back(loaderContext);
 				}
-			} else {
-				// Assume this is a modifier
+			} else if (Data::DataObjectTypes::isModifier(dataObjectType)) {
 				ModifierLoaderContext loaderContext(&stack);
+				section->appendModifier(loadModifierObject(loaderContext, dataObject));
+			} else {
+				error("Unexpected object type in this context");
+			}
+		} break;
+	case ChildLoaderContext::kTypeFilteredElements: {
+			Structural *container = topContext.containerUnion.filteredElements.structural;
 
-				project->appendModifier(loadModifierObject(loaderContext, dataObject));
+			if (topContext.containerUnion.filteredElements.filterFunc(dataObjectType)) {
+				const Data::StructuralDef &structuralDef = static_cast<const Data::StructuralDef &>(dataObject);
+
+				IElementFactory *elementFactory = getElementFactoryForDataObjectType(dataObjectType);
+				if (!elementFactory) {
+					error("No element factory defined for structural object");
+				}
+
+				ElementLoaderContext elementLoaderContext;
+				Common::SharedPtr<Element> element = elementFactory->createElement(elementLoaderContext, dataObject);
+
+				container->addChild(element);
+
+				if (structuralDef.structuralFlags & Data::StructuralFlags::kNoMoreSiblings)
+					stack.contexts.pop_back();
+
+				if (structuralDef.structuralFlags & Data::StructuralFlags::kHasChildren) {
+					ChildLoaderContext loaderContext;
+					// Visual elements can contain non-visual element children, but non-visual elements
+					// can only contain non-visual element children
+					loaderContext.containerUnion.filteredElements.filterFunc = element->isVisual() ? Data::DataObjectTypes::isElement : Data::DataObjectTypes::isNonVisualElement;
+					loaderContext.containerUnion.filteredElements.structural = container;
+					loaderContext.remainingCount = 0;
+					loaderContext.type = ChildLoaderContext::kTypeFilteredElements;
+
+					stack.contexts.push_back(loaderContext);
+				}
+			} else if (Data::DataObjectTypes::isModifier(dataObjectType)) {
+				ModifierLoaderContext loaderContext(&stack);
+				container->appendModifier(loadModifierObject(loaderContext, dataObject));
+			} else {
+				error("Unexpected object type in this context");
 			}
 		} break;
 	default:
@@ -1381,6 +1453,55 @@ void Project::loadContextualObject(ChildLoaderStack &stack, const Data::DataObje
 	}
 }
 
+void Project::loadAssetDef(AssetDefLoaderContext& context, const Data::DataObject& dataObject) {
+	assert(Data::DataObjectTypes::isAsset(dataObject.getType()));
+
+	IAssetFactory *factory = getAssetFactoryForDataObjectType(dataObject.getType());
+	if (!factory) {
+		error("Unimplemented asset type");
+		return;
+	}
+
+	AssetLoaderContext loaderContext;
+	context.assets.push_back(factory->createAsset(loaderContext, dataObject));
+}
+
+bool Section::load(const Data::SectionStructuralDef &data) {
+	_name = data.name;
+	_guid = data.guid;
+
+	return true;
+}
+
+bool Subsection::load(const Data::SubsectionStructuralDef &data) {
+	_name = data.name;
+	_guid = data.guid;
+
+	return true;
+}
+
+bool VisualElement::isVisual() const {
+	return true;
+}
+
+bool VisualElement::loadCommon(const Common::String &name, uint32 guid, const Data::Rect &rect, uint32 elementFlags, uint16 layer, uint32 streamLocator, uint16 sectionID) {
+	if (!_rect.load(rect))
+		return false;
+
+	_name = name;
+	_guid = guid;
+	_isHidden = ((elementFlags & Data::ElementFlags::kHidden) != 0);
+	_streamLocator = streamLocator;
+	_sectionID = sectionID;
+
+	return true;
+}
+
+bool NonVisualElement::isVisual() const {
+	return false;
+}
+
+
 ModifierFlags::ModifierFlags() : isLastModifier(false) {
 }
 
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index c00e89b1641..b7e7f3c442f 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -34,6 +34,7 @@
 
 namespace MTropolis {
 
+class Asset;
 class Project;
 class PlugIn;
 class Modifier;
@@ -516,17 +517,22 @@ private:
 class Structural : public IModifierContainer {
 
 public:
+	Structural();
 	virtual ~Structural();
 
 	const Common::Array<Common::SharedPtr<Structural> > &getChildren() const;
 	void addChild(const Common::SharedPtr<Structural> &child);
 
+	const Common::String &getName() const;
+
 	const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const override;
 	void appendModifier(const Common::SharedPtr<Modifier> &modifier) override;
 
-private:
+protected:
 	Common::Array<Common::SharedPtr<Structural> > _children;
 	Common::Array<Common::SharedPtr<Modifier> > _modifiers;
+	Common::String _name;
+	uint32 _guid;
 };
 
 struct ProjectPresentationSettings {
@@ -550,21 +556,32 @@ private:
 	int _segmentIndex;
 };
 
+struct AssetDefLoaderContext {
+	Common::Array<Common::SharedPtr<Asset> > assets;
+};
+
 struct ChildLoaderContext {
 	enum Type {
 		kTypeUnknown,
 		kTypeModifierList,
 		kTypeProject,
 		kTypeSection,
-		kTypeSubsection,
-		kTypeSceneRoots,
+		kTypeFilteredElements,
+	};
+
+	struct FilteredElements {
+		Structural *structural;
+		bool (*filterFunc)(Data::DataObjectTypes::DataObjectType dataObjectType);
 	};
 
 	union ContainerUnion {
 		IModifierContainer *modifierContainer;
 		Structural *structural;
+		FilteredElements filteredElements;
 	};
 
+	ChildLoaderContext();
+
 	uint remainingCount;
 	Type type;
 	ContainerUnion containerUnion;
@@ -650,6 +667,7 @@ private:
 	void loadPresentationSettings(const Data::PresentationSettings &presentationSettings);
 	void loadAssetCatalog(const Data::AssetCatalog &assetCatalog);
 	void loadGlobalObjectInfo(ChildLoaderStack &loaderStack, const Data::GlobalObjectInfo &globalObjectInfo);
+	void loadAssetDef(AssetDefLoaderContext &context, const Data::DataObject &dataObject);
 	void loadContextualObject(ChildLoaderStack &stack, const Data::DataObject &dataObject);
 	Common::SharedPtr<Modifier> loadModifierObject(ModifierLoaderContext &loaderContext, const Data::DataObject &dataObject);
 	void loadLabelMap(const Data::ProjectLabelMap &projectLabelMap);
@@ -680,12 +698,41 @@ private:
 };
 
 class Section : public Structural {
+public:
+	bool load(const Data::SectionStructuralDef &data);
 };
 
 class Subsection : public Structural {
+public:
+	bool load(const Data::SubsectionStructuralDef &data);
 };
 
-class Scene : public Structural {
+class Element : public Structural {
+public:
+	virtual bool isVisual() const = 0;
+
+protected:
+	uint32 _streamLocator;
+	uint16 _sectionID;
+};
+
+class VisualElement : public Element {
+public:
+	bool isVisual() const override;
+
+protected:
+	bool loadCommon(const Common::String &name, uint32 guid, const Data::Rect &rect, uint32 elementFlags, uint16 layer, uint32 streamLocator, uint16 sectionID);
+
+	bool _isHidden;
+	Rect16 _rect;
+	uint16 _layer;
+};
+
+class NonVisualElement : public Element {
+public:
+	bool isVisual() const override;
+
+	bool loadCommon(const Data::Rect &rect, const Common::String &str, uint32 elementFlags);
 };
 
 struct ModifierFlags {
@@ -708,6 +755,15 @@ protected:
 	bool loadTypicalHeader(const Data::TypicalModifierHeader &typicalHeader);
 };
 
+class Asset {
+public:
+	Asset();
+	virtual ~Asset();
+
+protected:
+	uint32 _assetID;
+};
+
 } // End of namespace MTropolis
 
 #endif


Commit: d1f74fa80c7b96d7ce1887be180e8289688bf4b7
    https://github.com/scummvm/scummvm/commit/d1f74fa80c7b96d7ce1887be180e8289688bf4b7
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add fault handling to VThread

Changed paths:
    engines/mtropolis/vthread.cpp
    engines/mtropolis/vthread.h


diff --git a/engines/mtropolis/vthread.cpp b/engines/mtropolis/vthread.cpp
index 45a0761e6c0..44513d61f7a 100644
--- a/engines/mtropolis/vthread.cpp
+++ b/engines/mtropolis/vthread.cpp
@@ -26,7 +26,7 @@ namespace MTropolis {
 VThreadTaskData::~VThreadTaskData() {
 }
 
-VThread::VThread() : _stackUnalignedBase(nullptr), _stackAlignedBase(nullptr), _size(0), _alignment(1), _used(0) {
+VThread::VThread() : _faultID(nullptr), _stackUnalignedBase(nullptr), _stackAlignedBase(nullptr), _size(0), _alignment(1), _used(0) {
 }
 
 
@@ -46,10 +46,16 @@ VThreadState VThread::step() {
 	void *dataPtr;
 	void *framePtr;
 	while (popFrame(dataPtr, framePtr)) {
+		VThreadStackFrame *frame = static_cast<VThreadStackFrame *>(framePtr);
+		bool isHandling = (frame->faultID == _faultID);
 		static_cast<VThreadStackFrame *>(framePtr)->~VThreadStackFrame();
-		VThreadState state = static_cast<VThreadTaskData *>(dataPtr)->destructAndRunTask();
-		if (state != kVThreadReturn)
-			return state;
+		if (isHandling) {
+			VThreadState state = static_cast<VThreadTaskData *>(dataPtr)->destructAndRunTask();
+			if (state != kVThreadReturn)
+				return state;
+		} else {
+			static_cast<VThreadTaskData *>(dataPtr)->~VThreadTaskData();
+		}
 	}
 
 	return kVThreadReturn;
diff --git a/engines/mtropolis/vthread.h b/engines/mtropolis/vthread.h
index 30973cc5be7..4982096111e 100644
--- a/engines/mtropolis/vthread.h
+++ b/engines/mtropolis/vthread.h
@@ -36,6 +36,17 @@ enum VThreadState {
 	kVThreadError,
 };
 
+struct VThreadFaultIdentifier {
+};
+
+template<typename T>
+struct VThreadFaultIdentifierSingleton {
+	static VThreadFaultIdentifier _identifier;
+};
+
+template<typename T>
+VThreadFaultIdentifier VThreadFaultIdentifierSingleton<T>::_identifier;
+
 class VThreadTaskData {
 
 public:
@@ -43,11 +54,15 @@ public:
 
 	virtual VThreadState destructAndRunTask() = 0;
 	virtual void relocateTo(void *newPosition) = 0;
+
+	virtual bool isFaultHandler() = 0;
+	virtual bool handlesFault() = 0;
 };
 
 struct VThreadStackFrame {
 	size_t taskDataOffset;	// Offset to VThreadTaskData
 	size_t prevFrameOffset;
+	VThreadFaultIdentifier *faultID;
 
 #ifdef MTROPOLIS_DEBUG_VTHREAD_STACKS
 	VThreadTaskData *data;
@@ -59,7 +74,7 @@ template<typename TClass, typename TData>
 class VThreadMethodData : public VThreadTaskData {
 
 public:
-	VThreadMethodData(TClass *target, VThreadState (TClass::*method)(const TData &data));
+	VThreadMethodData(VThreadFaultIdentifier *faultID, TClass *target, VThreadState (TClass::*method)(const TData &data));
 	VThreadMethodData(const VThreadMethodData &other);
 
 #if __cplusplus >= 201103L
@@ -72,6 +87,7 @@ public:
 	TData &getData();
 
 private:
+	VThreadFaultIdentifier *_faultID;
 	TClass *_target;
 	VThreadState (TClass::*_method)(const TData &data);
 	TData _data;
@@ -81,11 +97,11 @@ template<typename TData>
 class VThreadFunctionData : public VThreadTaskData {
 
 public:
-	explicit VThreadFunctionData(VThreadState (*func)(const TData &data));
+	explicit VThreadFunctionData(VThreadFaultIdentifier *faultID, VThreadState (*func)(const TData &data));
 	VThreadFunctionData(const VThreadFunctionData &other);
 
 #if __cplusplus >= 201103L
-	VThreadFunctionData(VThreadMethodData &&other);
+	VThreadFunctionData(VThreadFunctionData &&other);
 #endif
 
 	VThreadState destructAndRunTask() override;
@@ -94,6 +110,7 @@ public:
 	TData &getData();
 
 private:
+	VThreadFaultIdentifier *_faultID;
 	VThreadState (*_func)(const TData &data);
 	TData _data;
 };
@@ -110,9 +127,20 @@ public:
 	template<typename TData>
 	TData *pushTask(VThreadState (*func)(const TData &data));
 
+	template<typename TFaultType, typename TClass, typename TData>
+	TData *pushFaultHandler(TClass *obj, VThreadState (TClass::*method)(const TData &data));
+
+	template<typename TFaultType, typename TData>
+	TData *pushFaultHandler(VThreadState (*func)(const TData &data));
+
 	VThreadState step();
 
 private:
+	template<typename TClass, typename TData>
+	TData *pushTaskWithFaultHandler(VThreadFaultIdentifier *faultID, TClass *obj, VThreadState (TClass::*method)(const TData &data));
+
+	template<typename TData>
+	TData *pushTaskWithFaultHandler(VThreadFaultIdentifier *faultID, VThreadState (*func)(const TData &data));
 
 	void reserveFrame(size_t size, size_t alignment, void *&outFramePtr, void *&outUnadjustedDataPtr, size_t &outPrevFrameOffset);
 	bool popFrame(void *&dataPtr, void *&outFramePtr);
@@ -122,20 +150,24 @@ private:
 	size_t _size;
 	size_t _alignment;
 	size_t _used;
+	VThreadFaultIdentifier *_faultID;
 };
 
 template<typename TClass, typename TData>
-VThreadMethodData<TClass, TData>::VThreadMethodData(TClass *target, VThreadState (TClass::*method)(const TData &data)) : _target(target), _method(method) {
+VThreadMethodData<TClass, TData>::VThreadMethodData(VThreadFaultIdentifier *faultID, TClass *target, VThreadState (TClass::*method)(const TData &data))
+	: _faultID(faultID), _target(target), _method(method) {
 }
 
 template<typename TClass, typename TData>
-VThreadMethodData<TClass, TData>::VThreadMethodData(const VThreadMethodData& other) : _target(other._target), _method(other._method), _data(other._data) {
+VThreadMethodData<TClass, TData>::VThreadMethodData(const VThreadMethodData& other)
+	: _faultID(other._faultID), _target(other._target), _method(other._method), _data(other._data) {
 }
 
 #if __cplusplus >= 201103L
 
 template<typename TClass, typename TData>
-VThreadMethodData<TClass, TData>::VThreadMethodData(VThreadMethodData &&other) : _target(other._target), _method(other._method), _data(static_cast<TData &&>(*static_cast<TData *>(other._data))) {
+VThreadMethodData<TClass, TData>::VThreadMethodData(VThreadMethodData &&other)
+	: _faultID(other.faultID), _target(other._target), _method(other._method), _data(static_cast<TData &&>(*static_cast<TData *>(other._data))) {
 }
 
 #endif
@@ -173,17 +205,20 @@ TData &VThreadMethodData<TClass, TData>::getData() {
 }
 
 template<typename TData>
-VThreadFunctionData<TData>::VThreadFunctionData(VThreadState (*func)(const TData &data)) : _func(func) {
+VThreadFunctionData<TData>::VThreadFunctionData(VThreadFaultIdentifier *faultID, VThreadState (*func)(const TData &data))
+	: _faultID(faultID), _func(func) {
 }
 
 template<typename TData>
-VThreadFunctionData<TData>::VThreadFunctionData(const VThreadFunctionData &other) : _func(other._func), _data(other._data) {
+VThreadFunctionData<TData>::VThreadFunctionData(const VThreadFunctionData &other)
+	: _faultID(other._faultID), _func(other._func), _data(other._data) {
 }
 
 #if __cplusplus >= 201103L
 
 template<typename TData>
-VThreadFunctionData<TData>::VThreadFunctionData(VThreadMethodData &&other) : func(other._func), data(static_cast<TData &&>(other._data)) {
+VThreadFunctionData<TData>::VThreadFunctionData(VThreadFunctionData &&other)
+	: _faultID(other._faultID), _func(other._func), _data(static_cast<TData &&>(other._data)) {
 }
 
 #endif
@@ -221,6 +256,27 @@ TData &VThreadFunctionData<TData>::getData() {
 
 template<typename TClass, typename TData>
 TData *VThread::pushTask(TClass *obj, VThreadState (TClass::*method)(const TData &data)) {
+	return this->pushTaskWithFaultHandler(nullptr, obj, method);
+}
+
+template<typename TData>
+TData *VThread::pushTask(VThreadState (*func)(const TData &data)) {
+	return this->pushTaskWithFaultHandler(nullptr, func);
+}
+
+template<typename TFaultType, typename TClass, typename TData>
+TData *VThread::pushFaultHandler(TClass *obj, VThreadState (TClass::*method)(const TData &data)) {
+	return this->pushTaskWithFaultHandler(&VThreadFaultIdentifierSingleton<TFaultType>::_identifier, obj, method);
+}
+
+template<typename TFaultType, typename TData>
+TData *VThread::pushFaultHandler(VThreadState (*func)(const TData &data)) {
+	return this->pushTaskWithFaultHandler(&VThreadFaultIdentifierSingleton<TFaultType>::_identifier, func);
+}
+
+
+template<typename TClass, typename TData>
+TData *VThread::pushTaskWithFaultHandler(VThreadFaultIdentifier *faultID, TClass *obj, VThreadState (TClass::*method)(const TData &data)) {
 	typedef VThreadMethodData<TClass, TData> FrameData_t; 
 
 	const size_t frameAlignment = AlignmentHelper<VThreadStackFrame>::getAlignment();
@@ -235,6 +291,7 @@ TData *VThread::pushTask(TClass *obj, VThreadState (TClass::*method)(const TData
 
 	VThreadStackFrame *frame = new (framePtr) VThreadStackFrame();
 	frame->prevFrameOffset = prevFrameOffset;
+	frame->faultID = faultID;
 
 	FrameData_t *frameData = new (dataPtr) FrameData_t(obj, method);
 	frame->taskDataOffset = reinterpret_cast<char *>(static_cast<VThreadTaskData *>(frameData)) - static_cast<char *>(_stackAlignedBase);
@@ -248,7 +305,7 @@ TData *VThread::pushTask(TClass *obj, VThreadState (TClass::*method)(const TData
 }
 
 template<typename TData>
-TData *VThread::pushTask(VThreadState (*func)(const TData &data)) {
+TData *VThread::pushTaskWithFaultHandler(VThreadFaultIdentifier *faultID, VThreadState (*func)(const TData &data)) {
 	typedef VThreadFunctionData<TData> FrameData_t;
 
 	const size_t frameAlignment = AlignmentHelper<VThreadStackFrame>::getAlignment();
@@ -262,6 +319,7 @@ TData *VThread::pushTask(VThreadState (*func)(const TData &data)) {
 
 	VThreadStackFrame *frame = new (framePtr) VThreadStackFrame();
 	frame->prevFrameOffset = prevFrameOffset;
+	frame->faultID = faultID;
 
 	FrameData_t *frameData = new (dataPtr) FrameData_t(func);
 	frame->taskDataOffset = reinterpret_cast<char *>(static_cast<VThreadTaskData *>(frameData)) - static_cast<char *>(_stackAlignedBase);


Commit: b6189a7bea87c2f2d2b0a8a92b540a16be3b9d0d
    https://github.com/scummvm/scummvm/commit/b6189a7bea87c2f2d2b0a8a92b540a16be3b9d0d
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add a bunch of object materialization stubs, and notes.  Boot stream loading for Obsidian completed.

Changed paths:
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/miniscript.h
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/notes.txt
    engines/mtropolis/plugin/obsidian.cpp
    engines/mtropolis/plugin/obsidian.h
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index ec6838d2264..b2b917aeec0 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -31,9 +31,11 @@ namespace MTropolis {
 MiniscriptInstruction::~MiniscriptInstruction() {
 }
 
-MiniscriptProgram::MiniscriptProgram(const Common::SharedPtr<Common::Array<uint8> > &programData, const Common::Array<MiniscriptInstruction *> &instructions,
-									 const Common::Array<LocalRef> &localRefs, const Common::Array<Attribute> &attributes)
-	: _programData(programData), _instructions(instructions), _localRefs(localRefs), _attributes(attributes) {
+MiniscriptReferences::MiniscriptReferences(const Common::Array<LocalRef> &localRefs, const Common::Array<GlobalRef> &globalRefs) : _localRefs(localRefs), _globalRefs(globalRefs) {
+}
+
+MiniscriptProgram::MiniscriptProgram(const Common::SharedPtr<Common::Array<uint8> > &programData, const Common::Array<MiniscriptInstruction *> &instructions, const Common::Array<Attribute> &attributes)
+	: _programData(programData), _instructions(instructions), _attributes(attributes) {
 }
 
 MiniscriptProgram::~MiniscriptProgram() {
@@ -222,15 +224,20 @@ template<class T>
 MiniscriptInstructionFactory<T> MiniscriptInstructionFactory<T>::_instance;
 
 
-Common::SharedPtr<MiniscriptProgram> MiniscriptParser::parse(const Data::MiniscriptProgram &program) {
-	Common::Array<MiniscriptProgram::LocalRef> localRefs;
+bool MiniscriptParser::parse(const Data::MiniscriptProgram &program, Common::SharedPtr<MiniscriptProgram> &outProgram, Common::SharedPtr<MiniscriptReferences> &outReferences) {
+	Common::Array<MiniscriptReferences::LocalRef> localRefs;
+	Common::Array<MiniscriptReferences::GlobalRef> globalRefs;
+	Common::HashMap<uint32, size_t> globalGUIDToGlobalRefIndex;
 	Common::Array<MiniscriptProgram::Attribute> attributes;
 	Common::SharedPtr<Common::Array<uint8> > programDataPtr;
 	Common::Array<MiniscriptInstruction *> miniscriptInstructions;
 
 	// If the program is empty then just return an empty program
-	if (program.bytecode.size() == 0 || program.numOfInstructions == 0)
-		return Common::SharedPtr<MiniscriptProgram>(new MiniscriptProgram(programDataPtr, miniscriptInstructions, localRefs, attributes));
+	if (program.bytecode.size() == 0 || program.numOfInstructions == 0) {
+		outProgram = Common::SharedPtr<MiniscriptProgram>(new MiniscriptProgram(programDataPtr, miniscriptInstructions, attributes));
+		outReferences = Common::SharedPtr<MiniscriptReferences>(new MiniscriptReferences(localRefs, globalRefs));
+		return true;
+	}
 
 	localRefs.resize(program.localRefs.size());
 	for (size_t i = 0; i < program.localRefs.size(); i++) {
@@ -261,15 +268,15 @@ Common::SharedPtr<MiniscriptProgram> MiniscriptParser::parse(const Data::Miniscr
 		InstructionData &rawInstruction = rawInstructions[i];
 		uint16 instrSize;
 		if (!reader.readU16(rawInstruction.opcode) || !reader.readU16(rawInstruction.flags) || !reader.readU16(instrSize))
-			return nullptr;
+			return false;
 
 		if (instrSize < 6)
-			return nullptr;
+			return false;
 
 		if (instrSize > 6) {
 			rawInstruction.contents.resize(instrSize - 6);
 			if (!reader.read(&rawInstruction.contents[0], instrSize - 6))
-				return nullptr;
+				return false;
 		}
 	}
 
@@ -286,7 +293,7 @@ Common::SharedPtr<MiniscriptProgram> MiniscriptParser::parse(const Data::Miniscr
 		rawInstruction.instrFactory = factory;
 
 		if (!factory)
-			return nullptr;
+			return false;
 
 		size_t compiledSize = 0;
 		size_t compiledAlignment = 0;
@@ -328,12 +335,35 @@ Common::SharedPtr<MiniscriptProgram> MiniscriptParser::parse(const Data::Miniscr
 				miniscriptInstructions[i - 1 - di]->~MiniscriptInstruction();
 			}
 
-			return nullptr;
+			return false;
+		}
+
+		// Allocate the static GUID in the reference set
+		if (rawInstruction.opcode == 0x192) {
+			MiniscriptInstructions::PushGlobal *instr = static_cast<MiniscriptInstructions::PushGlobal *>(miniscriptInstructions[i]);
+			uint32 staticGUID = instr->getStaticGUID();
+			Common::HashMap<uint32, size_t>::const_iterator refIt = globalGUIDToGlobalRefIndex.find(staticGUID);
+			if (refIt == globalGUIDToGlobalRefIndex.end()) {
+				const size_t index = globalRefs.size();
+
+				MiniscriptReferences::GlobalRef globalRef;
+				globalRef.guid = staticGUID;
+
+				globalRefs.push_back(globalRef);
+				globalGUIDToGlobalRefIndex[staticGUID] = index;
+
+				instr->setReferenceSetIndex(index);
+			} else {
+				instr->setReferenceSetIndex(refIt->_value);
+			}
 		}
 	}
 
 	// Done
-	return Common::SharedPtr<MiniscriptProgram>(new MiniscriptProgram(programDataPtr, miniscriptInstructions, localRefs, attributes));
+	outProgram = Common::SharedPtr<MiniscriptProgram>(new MiniscriptProgram(programDataPtr, miniscriptInstructions, attributes));
+	outReferences = Common::SharedPtr<MiniscriptReferences>(new MiniscriptReferences(localRefs, globalRefs));
+
+	return true;
 }
 
 IMiniscriptInstructionFactory* MiniscriptParser::resolveOpcode(uint16 opcode) {
@@ -436,7 +466,19 @@ PushValue::PushValue(DataType dataType, const void *value) {
 	}
 }
 
-PushGlobal::PushGlobal(uint32 globalID) : _globalID(globalID) {
+PushGlobal::PushGlobal(uint32 guid) : _guid(guid), _refSetIndex(0) {
+}
+
+uint32 PushGlobal::getStaticGUID() const {
+	return _guid;
+}
+
+void PushGlobal::setReferenceSetIndex(size_t refSetIndex) {
+	_refSetIndex = refSetIndex;
+}
+
+size_t PushGlobal::getReferenceSetIndex() const {
+	return _refSetIndex;
 }
 
 PushString::PushString(const Common::String &str) : _str(str) {
diff --git a/engines/mtropolis/miniscript.h b/engines/mtropolis/miniscript.h
index 8943d732ba1..830791c3ae7 100644
--- a/engines/mtropolis/miniscript.h
+++ b/engines/mtropolis/miniscript.h
@@ -35,31 +35,43 @@ public:
 	virtual ~MiniscriptInstruction();
 };
 
-class MiniscriptProgram {
+class MiniscriptReferences {
 public:
 	struct LocalRef {
 		uint32 guid;
 		Common::String name;
 	};
 
+	struct GlobalRef {
+		uint32 guid;
+	};
+
+	MiniscriptReferences(const Common::Array<LocalRef> &localRefs, const Common::Array<GlobalRef> &globalRefs);
+
+private:
+	Common::Array<LocalRef> _localRefs;
+	Common::Array<GlobalRef> _globalRefs;
+};
+
+class MiniscriptProgram {
+public:
+
 	struct Attribute {
 		Common::String name;
 	};
 
-	MiniscriptProgram(const Common::SharedPtr<Common::Array<uint8> > &programData, const Common::Array<MiniscriptInstruction *> &instructions,
-		const Common::Array<LocalRef> &localRefs, const Common::Array<Attribute> &attributes);
+	MiniscriptProgram(const Common::SharedPtr<Common::Array<uint8> > &programData, const Common::Array<MiniscriptInstruction *> &instructions, const Common::Array<Attribute> &attributes);
 	~MiniscriptProgram();
 
 private:
 	Common::SharedPtr<Common::Array<uint8> > _programData;
 	Common::Array<MiniscriptInstruction *> _instructions;
-	Common::Array<LocalRef> _localRefs;
 	Common::Array<Attribute> _attributes;
 };
 
 class MiniscriptParser {
 public:
-	static Common::SharedPtr<MiniscriptProgram> parse(const Data::MiniscriptProgram &programData);
+	static bool parse(const Data::MiniscriptProgram &programData, Common::SharedPtr<MiniscriptProgram> &outProgram, Common::SharedPtr<MiniscriptReferences> &outReferences);
 
 	static IMiniscriptInstructionFactory *resolveOpcode(uint16 opcode);
 };
@@ -219,10 +231,15 @@ namespace MiniscriptInstructions {
 
 	class PushGlobal : public UnimplementedInstruction {
 	public:
-		explicit PushGlobal(uint32 globalID);
+		explicit PushGlobal(uint32 guid);
+
+		uint32 getStaticGUID() const;
+		void setReferenceSetIndex(size_t refSetIndex);
+		size_t getReferenceSetIndex() const;
 
 	private:
-		uint32 _globalID;
+		uint32 _guid;
+		size_t _refSetIndex;
 	};
 
 	class PushString : public UnimplementedInstruction {
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 55ced21977c..682c2536875 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -55,19 +55,38 @@ void BehaviorModifier::appendModifier(const Common::SharedPtr<Modifier> &modifie
 	_children.push_back(modifier);
 }
 
+Common::SharedPtr<Modifier> BehaviorModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new BehaviorModifier(*this));
+}
+
+void BehaviorModifier::visitInternalReferences(IStructuralReferenceVisitor* visitor) {
+	for (Common::Array<Common::SharedPtr<Modifier> >::iterator it = _children.begin(), itEnd = _children.end(); it != itEnd; ++it) {
+		visitor->visitChildModifierRef(*it);
+	}
+}
+
 
 // Miniscript modifier
 bool MiniscriptModifier::load(ModifierLoaderContext &context, const Data::MiniscriptModifier &data) {
 	if (!_enableWhen.load(data.enableWhen))
 		return false;
 
-	_program = MiniscriptParser::parse(data.program);
-	if (!_program)
+	if (!MiniscriptParser::parse(data.program, _program, _references))
 		return false;
 
 	return true;
 }
 
+Common::SharedPtr<Modifier> MiniscriptModifier::shallowClone() const {
+	MiniscriptModifier *clonePtr = new MiniscriptModifier(*this);
+	Common::SharedPtr<Modifier> clone(clonePtr);
+
+	// Keep the Miniscript program (which is static), but clone the references
+	clonePtr->_references.reset(new MiniscriptReferences(*_references));
+
+	return clone;
+}
+
 bool MessengerModifier::load(ModifierLoaderContext &context, const Data::MessengerModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -78,6 +97,10 @@ bool MessengerModifier::load(ModifierLoaderContext &context, const Data::Messeng
 	return true;
 }
 
+Common::SharedPtr<Modifier> MessengerModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new MessengerModifier(*this));
+}
+
 bool SetModifier::load(ModifierLoaderContext &context, const Data::SetModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -88,6 +111,10 @@ bool SetModifier::load(ModifierLoaderContext &context, const Data::SetModifier &
 	return true;
 }
 
+Common::SharedPtr<Modifier> SetModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new SetModifier(*this));
+}
+
 bool AliasModifier::load(ModifierLoaderContext &context, const Data::AliasModifier &data) {
 	_guid = data.guid;
 	if (!_modifierFlags.load(data.modifierFlags))
@@ -99,6 +126,14 @@ bool AliasModifier::load(ModifierLoaderContext &context, const Data::AliasModifi
 	return true;
 }
 
+Common::SharedPtr<Modifier> AliasModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new AliasModifier(*this));
+}
+
+uint32 AliasModifier::getAliasID() const {
+	return _aliasID;
+}
+
 bool ChangeSceneModifier::load(ModifierLoaderContext& context, const Data::ChangeSceneModifier& data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -122,6 +157,10 @@ bool ChangeSceneModifier::load(ModifierLoaderContext& context, const Data::Chang
 	return true;
 }
 
+Common::SharedPtr<Modifier> ChangeSceneModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new ChangeSceneModifier(*this));
+}
+
 bool SoundEffectModifier::load(ModifierLoaderContext &context, const Data::SoundEffectModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -140,6 +179,10 @@ bool SoundEffectModifier::load(ModifierLoaderContext &context, const Data::Sound
 	return true;
 }
 
+Common::SharedPtr<Modifier> SoundEffectModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new SoundEffectModifier(*this));
+}
+
 bool DragMotionModifier::load(ModifierLoaderContext &context, const Data::DragMotionModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -176,6 +219,10 @@ bool DragMotionModifier::load(ModifierLoaderContext &context, const Data::DragMo
 	return true;
 }
 
+Common::SharedPtr<Modifier> DragMotionModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new DragMotionModifier(*this));
+}
+
 bool VectorMotionModifier::load(ModifierLoaderContext &context, const Data::VectorMotionModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -186,6 +233,10 @@ bool VectorMotionModifier::load(ModifierLoaderContext &context, const Data::Vect
 	return true;
 }
 
+Common::SharedPtr<Modifier> VectorMotionModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new VectorMotionModifier(*this));
+}
+
 bool SceneTransitionModifier::load(ModifierLoaderContext &context, const Data::SceneTransitionModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -201,6 +252,10 @@ bool SceneTransitionModifier::load(ModifierLoaderContext &context, const Data::S
 	return true;
 }
 
+Common::SharedPtr<Modifier> SceneTransitionModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new SceneTransitionModifier(*this));
+}
+
 bool ElementTransitionModifier::load(ModifierLoaderContext &context, const Data::ElementTransitionModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -216,6 +271,10 @@ bool ElementTransitionModifier::load(ModifierLoaderContext &context, const Data:
 	return true;
 }
 
+Common::SharedPtr<Modifier> ElementTransitionModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new ElementTransitionModifier(*this));
+}
+
 bool IfMessengerModifier::load(ModifierLoaderContext &context, const Data::IfMessengerModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -223,13 +282,22 @@ bool IfMessengerModifier::load(ModifierLoaderContext &context, const Data::IfMes
 	if (!_when.load(data.when) || !_sendSpec.load(data.send, data.messageFlags, data.with, data.withSource, data.withString, data.destination))
 		return false;
 
-	_program = MiniscriptParser::parse(data.program);
-	if (!_program)
+	if (!MiniscriptParser::parse(data.program, _program, _references))
 		return false;
 
 	return true;
 }
 
+Common::SharedPtr<Modifier> IfMessengerModifier::shallowClone() const {
+	IfMessengerModifier *clonePtr = new IfMessengerModifier(*this);
+	Common::SharedPtr<Modifier> clone(clonePtr);
+
+	// Keep the Miniscript program (which is static), but clone the references
+	clonePtr->_references.reset(new MiniscriptReferences(*_references));
+
+	return clone;
+}
+
 bool TimerMessengerModifier::load(ModifierLoaderContext &context, const Data::TimerMessengerModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -246,6 +314,10 @@ bool TimerMessengerModifier::load(ModifierLoaderContext &context, const Data::Ti
 	return true;
 }
 
+Common::SharedPtr<Modifier> TimerMessengerModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new TimerMessengerModifier(*this));
+}
+
 bool BoundaryDetectionMessengerModifier::load(ModifierLoaderContext &context, const Data::BoundaryDetectionMessengerModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -267,6 +339,10 @@ bool BoundaryDetectionMessengerModifier::load(ModifierLoaderContext &context, co
 	return true;
 }
 
+Common::SharedPtr<Modifier> BoundaryDetectionMessengerModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new BoundaryDetectionMessengerModifier(*this));
+}
+
 bool CollisionDetectionMessengerModifier::load(ModifierLoaderContext &context, const Data::CollisionDetectionMessengerModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -300,6 +376,10 @@ bool CollisionDetectionMessengerModifier::load(ModifierLoaderContext &context, c
 	return true;
 }
 
+Common::SharedPtr<Modifier> CollisionDetectionMessengerModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new CollisionDetectionMessengerModifier(*this));
+}
+
 bool KeyboardMessengerModifier::load(ModifierLoaderContext &context, const Data::KeyboardMessengerModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -343,6 +423,10 @@ bool KeyboardMessengerModifier::load(ModifierLoaderContext &context, const Data:
 	return true;
 }
 
+Common::SharedPtr<Modifier> KeyboardMessengerModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new KeyboardMessengerModifier(*this));
+}
+
 TextStyleModifier::StyleFlags::StyleFlags() : bold(false), italic(false), underline(false), outline(false), shadow(false), condensed(false), expanded(false) {
 }
 
@@ -372,6 +456,10 @@ bool TextStyleModifier::load(ModifierLoaderContext &context, const Data::TextSty
 	return true;
 }
 
+Common::SharedPtr<Modifier> TextStyleModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new TextStyleModifier(*this));
+}
+
 bool GraphicModifier::load(ModifierLoaderContext& context, const Data::GraphicModifier& data) {
 	if (!loadTypicalHeader(data.modHeader) || !_applyWhen.load(data.applyWhen) || !_removeWhen.load(data.removeWhen)
 		|| !_foreColor.load(data.foreColor) || !_backColor.load(data.backColor)
@@ -394,6 +482,10 @@ bool GraphicModifier::load(ModifierLoaderContext& context, const Data::GraphicMo
 	return true;
 }
 
+Common::SharedPtr<Modifier> GraphicModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new GraphicModifier(*this));
+}
+
 bool CompoundVariableModifier::load(ModifierLoaderContext &context, const Data::CompoundVariableModifier &data) {
 	if (data.numChildren > 0) {
 		ChildLoaderContext loaderContext;
@@ -420,6 +512,15 @@ void CompoundVariableModifier::appendModifier(const Common::SharedPtr<Modifier>&
 	_children.push_back(modifier);
 }
 
+void CompoundVariableModifier::visitInternalReferences(IStructuralReferenceVisitor *visitor) {
+	for (Common::Array<Common::SharedPtr<Modifier> >::iterator it = _children.begin(), itEnd = _children.end(); it != itEnd; ++it) {
+		visitor->visitChildModifierRef(*it);
+	}
+}
+
+Common::SharedPtr<Modifier> CompoundVariableModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new CompoundVariableModifier(*this));
+}
 
 bool BooleanVariableModifier::load(ModifierLoaderContext &context, const Data::BooleanVariableModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
@@ -430,6 +531,10 @@ bool BooleanVariableModifier::load(ModifierLoaderContext &context, const Data::B
 	return true;
 }
 
+Common::SharedPtr<Modifier> BooleanVariableModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new BooleanVariableModifier(*this));
+}
+
 bool IntegerVariableModifier::load(ModifierLoaderContext& context, const Data::IntegerVariableModifier& data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -439,6 +544,10 @@ bool IntegerVariableModifier::load(ModifierLoaderContext& context, const Data::I
 	return true;
 }
 
+Common::SharedPtr<Modifier> IntegerVariableModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new IntegerVariableModifier(*this));
+}
+
 bool IntegerRangeVariableModifier::load(ModifierLoaderContext& context, const Data::IntegerRangeVariableModifier& data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -449,6 +558,10 @@ bool IntegerRangeVariableModifier::load(ModifierLoaderContext& context, const Da
 	return true;
 }
 
+Common::SharedPtr<Modifier> IntegerRangeVariableModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new IntegerRangeVariableModifier(*this));
+}
+
 bool VectorVariableModifier::load(ModifierLoaderContext &context, const Data::VectorVariableModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -459,6 +572,10 @@ bool VectorVariableModifier::load(ModifierLoaderContext &context, const Data::Ve
 	return true;
 }
 
+Common::SharedPtr<Modifier> VectorVariableModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new VectorVariableModifier(*this));
+}
+
 bool PointVariableModifier::load(ModifierLoaderContext &context, const Data::PointVariableModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -469,6 +586,10 @@ bool PointVariableModifier::load(ModifierLoaderContext &context, const Data::Poi
 	return true;
 }
 
+Common::SharedPtr<Modifier> PointVariableModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new PointVariableModifier(*this));
+}
+
 bool FloatingPointVariableModifier::load(ModifierLoaderContext &context, const Data::FloatingPointVariableModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -478,6 +599,10 @@ bool FloatingPointVariableModifier::load(ModifierLoaderContext &context, const D
 	return true;
 }
 
+Common::SharedPtr<Modifier> FloatingPointVariableModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new FloatingPointVariableModifier(*this));
+}
+
 bool StringVariableModifier::load(ModifierLoaderContext &context, const Data::StringVariableModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -487,4 +612,8 @@ bool StringVariableModifier::load(ModifierLoaderContext &context, const Data::St
 	return true;
 }
 
+Common::SharedPtr<Modifier> StringVariableModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new StringVariableModifier(*this));
+}
+
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 2c80fd1e593..9b18c48b688 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -29,6 +29,7 @@ namespace MTropolis {
 
 struct ModifierLoaderContext;
 class MiniscriptProgram;
+class MiniscriptReferences;
 
 class BehaviorModifier : public Modifier, public IModifierContainer {
 public:
@@ -38,7 +39,11 @@ public:
 	void appendModifier(const Common::SharedPtr<Modifier> &modifier) override;
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+	void visitInternalReferences(IStructuralReferenceVisitor *visitor) override;
+
 	Common::Array<Common::SharedPtr<Modifier> > _children;
+
 	Event _enableWhen;
 	Event _disableWhen;
 };
@@ -48,9 +53,12 @@ public:
 	bool load(ModifierLoaderContext &context, const Data::MiniscriptModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	Event _enableWhen;
 
 	Common::SharedPtr<MiniscriptProgram> _program;
+	Common::SharedPtr<MiniscriptReferences> _references;
 };
 
 class MessengerModifier : public Modifier {
@@ -58,6 +66,8 @@ public:
 	bool load(ModifierLoaderContext &context, const Data::MessengerModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	Event _when;
 	MessengerSendSpec _sendSpec;
 };
@@ -67,6 +77,8 @@ public:
 	bool load(ModifierLoaderContext &context, const Data::SetModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	Event _executeWhen;
 	DynamicValue _source;
 	DynamicValue _target;
@@ -75,8 +87,11 @@ private:
 class AliasModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::AliasModifier &data);
+	uint32 getAliasID() const;
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	uint32 _aliasID;
 };
 
@@ -85,6 +100,8 @@ public:
 	bool load(ModifierLoaderContext &context, const Data::ChangeSceneModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	enum SceneSelectionType {
 		kSceneSelectionTypeNext,
 		kSceneSelectionTypePrevious,
@@ -106,6 +123,8 @@ public:
 	bool load(ModifierLoaderContext &context, const Data::SoundEffectModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	enum SoundType {
 		kSoundTypeBeep,
 		kSoundTypeAudioAsset,
@@ -123,6 +142,8 @@ public:
 	bool load(ModifierLoaderContext &context, const Data::DragMotionModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	Event _enableWhen;
 	Event _disableWhen;
 
@@ -142,6 +163,8 @@ public:
 	bool load(ModifierLoaderContext &context, const Data::VectorMotionModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	Event _enableWhen;
 	Event _disableWhen;
 
@@ -170,6 +193,8 @@ public:
 	};
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	Event _enableWhen;
 	Event _disableWhen;
 
@@ -196,6 +221,8 @@ public:
 	};
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	Event _enableWhen;
 	Event _disableWhen;
 
@@ -210,10 +237,13 @@ public:
 	bool load(ModifierLoaderContext &context, const Data::IfMessengerModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	Event _when;
 	MessengerSendSpec _sendSpec;
 
 	Common::SharedPtr<MiniscriptProgram> _program;
+	Common::SharedPtr<MiniscriptReferences> _references;
 };
 
 class TimerMessengerModifier : public Modifier {
@@ -221,6 +251,8 @@ public:
 	bool load(ModifierLoaderContext &context, const Data::TimerMessengerModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	Event _executeWhen;
 	Event _terminateWhen;
 	MessengerSendSpec _sendSpec;
@@ -233,6 +265,8 @@ public:
 	bool load(ModifierLoaderContext &context, const Data::BoundaryDetectionMessengerModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	enum ExitTriggerMode {
 		kExitTriggerExiting,
 		kExitTriggerOnceExited,
@@ -259,6 +293,8 @@ public:
 	bool load(ModifierLoaderContext &context, const Data::CollisionDetectionMessengerModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	enum DetectionMode {
 		kDetectionModeFirstContact,
 		kDetectionModeWhileInContact,
@@ -282,6 +318,8 @@ public:
 	bool load(ModifierLoaderContext &context, const Data::KeyboardMessengerModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	Event _send;
 
 	enum KeyCodeType {
@@ -339,6 +377,9 @@ public:
 		bool load(uint8 dataStyleFlags);
 	};
 
+private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	uint16 _macFontID;
 	uint16 _size;
 	ColorRGB8 _textColor;
@@ -377,6 +418,8 @@ public:
 	};
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	Event _applyWhen;
 	Event _removeWhen;
 	InkMode _inkMode;
@@ -392,70 +435,87 @@ private:
 	Common::Array<Point16> _polyPoints;
 };
 
-class CompoundVariableModifier : public Modifier, public IModifierContainer {
+class CompoundVariableModifier : public VariableModifier, public IModifierContainer {
 public:
 	bool load(ModifierLoaderContext &context, const Data::CompoundVariableModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const override;
 	void appendModifier(const Common::SharedPtr<Modifier> &modifier) override;
+	void visitInternalReferences(IStructuralReferenceVisitor *visitor) override;
 
 	Common::Array<Common::SharedPtr<Modifier> > _children;
 };
 
-class BooleanVariableModifier : public Modifier {
+class BooleanVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::BooleanVariableModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	bool _value;
 };
 
-class IntegerVariableModifier : public Modifier {
+class IntegerVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::IntegerVariableModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	int32 _value;
 };
 
-class IntegerRangeVariableModifier : public Modifier {
+class IntegerRangeVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::IntegerRangeVariableModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	IntRange _range;
 };
 
-class VectorVariableModifier : public Modifier {
+class VectorVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::VectorVariableModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	AngleMagVector _vector;
 };
 
-class PointVariableModifier : public Modifier {
+class PointVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::PointVariableModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	Point16 _value;
 };
 
-class FloatingPointVariableModifier : public Modifier {
+class FloatingPointVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::FloatingPointVariableModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	double _value;
 };
 
-class StringVariableModifier : public Modifier {
+class StringVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::StringVariableModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	Common::String _value;
 };
 
diff --git a/engines/mtropolis/notes.txt b/engines/mtropolis/notes.txt
index 6fb11ad3173..879122baccf 100644
--- a/engines/mtropolis/notes.txt
+++ b/engines/mtropolis/notes.txt
@@ -1,7 +1,7 @@
 GUID resolution and structure loading:
 
 Loading of objects in the mTropolis backend is done in multiple stages due to
-the complex GUID resolution process, inline assets, and other things.  Here's
+the complex GUID resolution process, inline assets, and other things.
 
 The first step to loading is to load data objects.  Data objects are JUST data,
 but due to weird aspects of the data loading process (like the fact that asset
@@ -10,5 +10,51 @@ defs can appear anywhere) it's separate.
 The second step is conversion of data objects into runtime objects and
 formation of the scene structure.
 
-The third step is "materialization" which performs two tasks: First, it clones
-all aliased non-variable modifiers.  Second, it remaps all GUIDs to objects.
+The third step is "materialization" which converts loaded objects into a
+ready-to-use state.  This involves replacing any non-variable aliases with a
+copy of the original, assigning new GUIDs to the new objects, and remapping
+any internal references to the corresponding visible objects.
+
+Objects are only ever materialized once.  Aliasable variables in the global
+modifier table are materialized when the project is loaded, while everything
+else is materialized when it's imported into the project.
+
+Objects cloned from already-materialized objects should NOT be materialized
+again, instead they should be fixed up using an object reference remap table,
+shallowClone, and visitInternalReferences.  Cloning is not currently supported
+because Obsidian doesn't use it.
+
+
+Scene transitions:
+
+mTropolis Player's handling of scene transitions is very buggy/quirky.
+Basically, there is a feature called "add to destination scene" (ATDS) which
+loads the target scene on top of a stack of scenes and adds it to the return
+list, which is intended for dialogs and such.
+
+Unfortunately, it only works properly in the straightforward case, and
+transitions are done in a way that basically does the exact actions required
+for it to work in the typical case but are broken in edge cases.
+
+Basically scenes are ordered in a stack, and there is a scene return list
+separate from the scene stack.  The shared scene is always at the bottom of
+the stack.
+
+On forward scene transitions:
+ - The shared scene is changed to the shared scene of the new scene
+ - If not doing an ATDS-type transfer, then the current scene is unloaded
+ - If the target scene is not loaded, then it is loaded at the top of the stack
+
+On return transitions:
+ - The active scene is unloaded
+ - If returning from a non-ATDS transfer, then the return scene is loaded and its
+   shared scene is made active.
+
+This has a bunch of confirmed broken cases:
+- Returning from an ATDS transfer into a different shared scene will not reset
+  the shared scene on return.
+- Transitioning into an ATDS scene that's already in the stack will cause it to
+  be removed on returning.  This can even cause no scenes to be loaded.
+- Doing a regular transition out of ATDS stacked scenes and then returning will
+  cause only the top scene to be loaded.
+- Probably a bunch of other cases.
diff --git a/engines/mtropolis/plugin/obsidian.cpp b/engines/mtropolis/plugin/obsidian.cpp
index e723e9bea0a..7775098e327 100644
--- a/engines/mtropolis/plugin/obsidian.cpp
+++ b/engines/mtropolis/plugin/obsidian.cpp
@@ -30,14 +30,25 @@ bool MovementModifier::load(const PlugInModifierLoaderContext &context, const Da
 	return true;
 }
 
+Common::SharedPtr<Modifier> MovementModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new MovementModifier(*this));
+}
+
 bool RectShiftModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::RectShiftModifier &data) {
 	return true;
 }
 
+Common::SharedPtr<Modifier> RectShiftModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new RectShiftModifier(*this));
+}
+
 bool TextWorkModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::TextWorkModifier &data) {
 	return true;
 }
 
+Common::SharedPtr<Modifier> TextWorkModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new TextWorkModifier(*this));
+}
 
 ObsidianPlugIn::ObsidianPlugIn() : _movementModifierFactory(this), _rectShiftModifierFactory(this), _textWorkModifierFactory(this) {
 }
diff --git a/engines/mtropolis/plugin/obsidian.h b/engines/mtropolis/plugin/obsidian.h
index f6b9065bb7b..c63dd37f172 100644
--- a/engines/mtropolis/plugin/obsidian.h
+++ b/engines/mtropolis/plugin/obsidian.h
@@ -34,16 +34,25 @@ namespace Obsidian {
 class MovementModifier : public Modifier {
 public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Obsidian::MovementModifier &data);
+
+private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
 };
 
 class RectShiftModifier : public Modifier {
 public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Obsidian::RectShiftModifier &data);
+
+private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
 };
 
 class TextWorkModifier : public Modifier {
 public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Obsidian::TextWorkModifier &data);
+
+private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
 };
 
 class ObsidianPlugIn : public MTropolis::PlugIn {
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 5ec7587ad69..eccb6d8716d 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -35,10 +35,18 @@ bool CursorModifier::load(const PlugInModifierLoaderContext &context, const Data
 	return true;
 }
 
+Common::SharedPtr<Modifier> CursorModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new CursorModifier(*this));
+}
+
 bool STransCtModifier::load(const PlugInModifierLoaderContext& context, const Data::Standard::STransCtModifier& data) {
 	return true;
 }
 
+Common::SharedPtr<Modifier> STransCtModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new STransCtModifier(*this));
+}
+
 bool MediaCueMessengerModifier::load(const PlugInModifierLoaderContext& context, const Data::Standard::MediaCueMessengerModifier& data) {
 	if (data.enableWhen.type != Data::PlugInTypeTaggedValue::kEvent)
 		return false;
@@ -70,6 +78,10 @@ bool MediaCueMessengerModifier::load(const PlugInModifierLoaderContext& context,
 	return true;
 }
 
+Common::SharedPtr<Modifier> MediaCueMessengerModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new MediaCueMessengerModifier(*this));
+}
+
 bool ObjectReferenceVariableModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::ObjectReferenceVariableModifier &data) {
 	if (data.setToSourceParentWhen.type != Data::PlugInTypeTaggedValue::kEvent)
 		return false;
@@ -84,6 +96,10 @@ bool ObjectReferenceVariableModifier::load(const PlugInModifierLoaderContext &co
 	return true;
 }
 
+Common::SharedPtr<Modifier> ObjectReferenceVariableModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new ObjectReferenceVariableModifier(*this));
+}
+
 bool MidiModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::MidiModifier &data) {
 	if (data.executeWhen.type != Data::PlugInTypeTaggedValue::kEvent)
 		return false;
@@ -126,6 +142,10 @@ bool MidiModifier::load(const PlugInModifierLoaderContext &context, const Data::
 	return true;
 }
 
+Common::SharedPtr<Modifier> MidiModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new MidiModifier(*this));
+}
+
 bool ListVariableModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::ListVariableModifier &data) {
 	if (!data.havePersistentData || data.numValues == 0)
 		return true;	// If the list is empty then we don't care, the actual value type is irrelevant because it can be reassigned
@@ -186,10 +206,18 @@ bool ListVariableModifier::load(const PlugInModifierLoaderContext &context, cons
 	return true;
 }
 
+Common::SharedPtr<Modifier> ListVariableModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new ListVariableModifier(*this));
+}
+
 bool SysInfoModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::SysInfoModifier &data) {
 	return true;
 }
 
+Common::SharedPtr<Modifier> SysInfoModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new SysInfoModifier(*this));
+}
+
 StandardPlugInHacks::StandardPlugInHacks() : allowGarbledListModData(false) {
 }
 
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index 7a451272dce..4b0c3d8ada0 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -38,6 +38,8 @@ public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::CursorModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	Event _applyWhen;
 	Event _removeWhen;
 	uint32 _cursorID;
@@ -47,6 +49,9 @@ private:
 class STransCtModifier : public Modifier {
 public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::STransCtModifier &data);
+
+private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
 };
 
 class MediaCueMessengerModifier : public Modifier {
@@ -54,6 +59,8 @@ public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::MediaCueMessengerModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	enum TriggerTiming {
 		kTriggerTimingStart = 0,
 		kTriggerTimingDuring = 1,
@@ -66,11 +73,13 @@ private:
 	MessengerSendSpec _send;
 };
 
-class ObjectReferenceVariableModifier : public Modifier {
+class ObjectReferenceVariableModifier : public VariableModifier {
 public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::ObjectReferenceVariableModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	Event _setToSourceParentWhen;
 	Common::String _objectPath;
 };
@@ -80,6 +89,8 @@ public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::MidiModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	struct FilePart {
 		bool loop;
 		bool overrideTempo;
@@ -116,17 +127,22 @@ private:
 	Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> _embeddedFile;
 };
 
-class ListVariableModifier : public Modifier {
+class ListVariableModifier : public VariableModifier {
 public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::ListVariableModifier &data);
 
 private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
 	DynamicList _list;
 };
 
 class SysInfoModifier : public Modifier {
 public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::SysInfoModifier &data);
+
+private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
 };
 
 struct StandardPlugInHacks {
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 0142e8436de..7de22447f2c 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -25,6 +25,7 @@
 #include "mtropolis/asset_factory.h"
 #include "mtropolis/element_factory.h"
 #include "mtropolis/modifier_factory.h"
+#include "mtropolis/modifiers.h"
 
 #include "common/debug.h"
 #include "common/file.h"
@@ -33,7 +34,41 @@
 
 namespace MTropolis {
 
-bool Point16::load(const Data::Point& point) {
+class ModifierChildMaterializer : public IStructuralReferenceVisitor {
+public:
+	ModifierChildMaterializer(Runtime *runtime, ObjectLinkingScope *outerScope);
+
+	void visitChildStructuralRef(Common::SharedPtr<Structural> &structural) override;
+	void visitChildModifierRef(Common::SharedPtr<Modifier> &modifier) override;
+	void visitWeakStructuralRef(Common::SharedPtr<Structural> &structural) override;
+	void visitWeakModifierRef(Common::SharedPtr<Modifier> &modifier) override;
+
+private:
+	Runtime *_runtime;
+	ObjectLinkingScope *_outerScope;
+};
+
+ModifierChildMaterializer::ModifierChildMaterializer(Runtime *runtime, ObjectLinkingScope *outerScope)
+	: _runtime(runtime), _outerScope(outerScope) {
+}
+
+void ModifierChildMaterializer::visitChildStructuralRef(Common::SharedPtr<Structural> &structural) {
+	assert(false);
+}
+
+void ModifierChildMaterializer::visitChildModifierRef(Common::SharedPtr<Modifier> &modifier) {
+	modifier->materialize(_runtime, _outerScope);
+}
+
+void ModifierChildMaterializer::visitWeakStructuralRef(Common::SharedPtr<Structural> &structural) {
+	assert(false);
+}
+
+void ModifierChildMaterializer::visitWeakModifierRef(Common::SharedPtr<Modifier> &modifier) {
+	// Do nothing
+}
+
+bool Point16::load(const Data::Point &point) {
 	x = point.x;
 	y = point.y;
 
@@ -872,7 +907,26 @@ void SimpleModifierContainer::appendModifier(const Common::SharedPtr<Modifier> &
 	_modifiers.push_back(modifier);
 }
 
-Structural::Structural() : _guid(0) {
+
+RuntimeObject::RuntimeObject() : _guid(0), _runtimeGUID(0) {
+}
+
+RuntimeObject::~RuntimeObject() {
+}
+
+uint32 RuntimeObject::getStaticGUID() const {
+	return _guid;
+}
+
+uint32 RuntimeObject::getRuntimeGUID() const {
+	return _runtimeGUID;
+}
+
+void RuntimeObject::setRuntimeGUID(uint32 runtimeGUID) {
+	_runtimeGUID = runtimeGUID;
+}
+
+Structural::Structural() {
 }
 
 Structural::~Structural() {
@@ -901,7 +955,103 @@ void Structural::appendModifier(const Common::SharedPtr<Modifier> &modifier) {
 	_modifiers.push_back(modifier);
 }
 
-Runtime::Runtime() {
+void Structural::materializeSelfAndDescendents(Runtime *runtime, ObjectLinkingScope *outerScope) {
+	linkInternalReferences(outerScope);
+	setRuntimeGUID(runtime->allocateRuntimeGUID());
+
+	materializeDescendents(runtime, outerScope);
+}
+
+void Structural::materializeDescendents(Runtime *runtime, ObjectLinkingScope *outerScope) {
+	// Materialization is the step after objects are fully constructed and filled with data.
+	// It does three things, recursively:
+	// - Assigns all objects a new runtime GUID
+	// - Clones any non-variable aliased modifiers
+	// - Links any static GUIDs to in-scope visible objects
+	// Objects are only ever materialized once
+	ObjectLinkingScope tempModifierScope;
+	ObjectLinkingScope tempStructuralScope;
+	ObjectLinkingScope *modifierScope = this->getPersistentModifierScope();
+	ObjectLinkingScope *structuralScope = this->getPersistentStructuralScope();
+
+	if (!modifierScope)
+		modifierScope = &tempModifierScope;
+	if (!structuralScope)
+		structuralScope = &tempStructuralScope;
+
+	modifierScope->setParent(outerScope);
+
+	for (Common::Array<Common::SharedPtr<Modifier> >::iterator it = _modifiers.begin(), itEnd = _modifiers.end(); it != itEnd; ++it) {
+		Modifier *modifier = it->get();
+		uint32 modifierGUID = modifier->getStaticGUID();
+		if (modifier->isAlias() && !modifier->isVariable()) {
+			Common::SharedPtr<Modifier> templateModifier = runtime->getProject()->resolveAlias(static_cast<AliasModifier *>(modifier)->getAliasID());
+			if (!templateModifier) {
+				error("Failed to resolve alias");
+			}
+
+			Common::SharedPtr<Modifier> clonedModifier = templateModifier->shallowClone();
+			clonedModifier->setName(modifier->getName());
+
+			(*it) = clonedModifier;
+			modifier = clonedModifier.get();
+		}
+		modifierScope->addObject(modifierGUID, *it);
+	}
+
+	for (Common::Array<Common::SharedPtr<Modifier> >::const_iterator it = _modifiers.begin(), itEnd = _modifiers.end(); it != itEnd; ++it) {
+		Modifier *modifier = it->get();
+		modifier->materialize(runtime, modifierScope);
+	}
+
+	structuralScope->setParent(modifierScope);
+
+	const Common::Array<Common::SharedPtr<Structural> > &children = this->getChildren();
+	for (Common::Array<Common::SharedPtr<Structural> >::const_iterator it = children.begin(), itEnd = children.end(); it != itEnd; ++it) {
+		Structural *child = it->get();
+		structuralScope->addObject(child->getStaticGUID(), *it);
+	}
+
+	for (Common::Array<Common::SharedPtr<Structural> >::const_iterator it = children.begin(), itEnd = children.end(); it != itEnd; ++it) {
+		Structural *child = it->get();
+		child->materializeSelfAndDescendents(runtime, structuralScope);
+	}
+}
+
+void Structural::linkInternalReferences(ObjectLinkingScope *scope) {
+}
+
+ObjectLinkingScope *Structural::getPersistentStructuralScope() {
+	return nullptr;
+}
+
+ObjectLinkingScope* Structural::getPersistentModifierScope() {
+	return nullptr;
+}
+
+ObjectLinkingScope::ObjectLinkingScope() : _parent(nullptr) {
+}
+
+ObjectLinkingScope::~ObjectLinkingScope() {
+}
+
+void ObjectLinkingScope::setParent(ObjectLinkingScope *parent) {
+	_parent = parent;
+}
+
+void ObjectLinkingScope::addObject(uint32 guid, const Common::WeakPtr<RuntimeObject> &object) {
+	_guidToObject[guid] = object;
+}
+
+void ObjectLinkingScope::reset() {
+	_parent = nullptr;
+	_guidToObject.clear(true);
+}
+
+SceneStateTransition::SceneStateTransition(VisualElement *scene, SceneState targetState) : scene(scene), targetState(targetState) {
+}
+
+Runtime::Runtime() : _nextRuntimeGUID(1) {
 	_vthread.reset(new VThread());
 }
 
@@ -916,13 +1066,48 @@ void Runtime::runFrame() {
 		Common::SharedPtr<ProjectDescription> desc = _queuedProjectDesc;
 		_queuedProjectDesc.reset();
 
-		_project.reset();
+		unloadProject();
+
 		_project.reset(new Project());
 
 		_project->loadFromDescription(*desc);
+
+		_rootLinkingScope.addObject(_project->getStaticGUID(), _project);
+
+		// We have to materialize global variables because they are not cloned from aliases.
+		debug(1, "Materializing global variables...");
+		_project->materializeGlobalVariables(this, &_rootLinkingScope);
+
+		debug(1, "Materializing project...");
+		_project->materializeSelfAndDescendents(this, &_rootLinkingScope);
+
+		debug(1, "Project is fully loaded!  Starting up...");
+
+		if (_project->getChildren().size() == 0) {
+			error("Project has no sections");
+		}
+
+		Structural *firstSection = _project->getChildren()[0].get();
+		if (firstSection->getChildren().size() == 0) {
+			error("Project has no subsections");
+		}
+
+		Structural *firstSubsection = firstSection->getChildren()[0].get();
+		if (firstSubsection->getChildren().size() < 2) {
+			error("Project has no subsections");
+		}
+
+		_pendingSceneStateTransitions.push_back(SceneStateTransition(static_cast<VisualElement *>(firstSubsection->getChildren()[0].get()), kSceneStateShared));
+		_pendingSceneStateTransitions.push_back(SceneStateTransition(static_cast<VisualElement *>(firstSubsection->getChildren()[1].get()), kSceneStateActive));
 	}
 }
 
+void Runtime::unloadProject() {
+	_project.reset();
+	_rootLinkingScope.reset();
+	_pendingSceneStateTransitions.clear();
+}
+
 void Runtime::queueProject(const Common::SharedPtr<ProjectDescription> &desc) {
 	_queuedProjectDesc = desc;
 }
@@ -936,6 +1121,14 @@ void Runtime::addVolume(int volumeID, const char *name, bool isMounted) {
 	_volumes.push_back(volume);
 }
 
+Project *Runtime::getProject() const {
+	return _project.get();
+}
+
+uint32 Runtime::allocateRuntimeGUID() {
+	return _nextRuntimeGUID++;
+}
+
 const Common::Array<Common::SharedPtr<Modifier> > &IModifierContainer::getModifiers() const {
 	return const_cast<IModifierContainer &>(*this).getModifiers();
 };
@@ -1075,11 +1268,25 @@ void Project::loadFromDescription(const ProjectDescription& desc) {
 
 	loadBootStream(bootStreamIndex);
 
-	debug(1, "Boot stream loaded successfully, materializing project...");
+	debug(1, "Boot stream loaded successfully");
+}
+
+Common::SharedPtr<Modifier> Project::resolveAlias(uint32 aliasID) const {
+	if (aliasID == 0 || aliasID > _globalModifiers.getModifiers().size())
+		return Common::SharedPtr<Modifier>();
+
+	return _globalModifiers.getModifiers()[aliasID - 1];
+}
 
-	error("TODO");
+void Project::materializeGlobalVariables(Runtime *runtime, ObjectLinkingScope *outerScope) {
+	for (Common::Array<Common::SharedPtr<Modifier> >::const_iterator it = _globalModifiers.getModifiers().begin(), itEnd = _globalModifiers.getModifiers().end(); it != itEnd; ++it) {
+		Modifier *modifier = it->get();
+		if (!modifier)
+			continue;
 
-	debug(1, "Materialized project OK, project is loaded!");
+		if (modifier->isVariable())
+			modifier->materialize(runtime, outerScope);
+	}
 }
 
 void Project::openSegmentStream(int segmentIndex) {
@@ -1178,6 +1385,10 @@ void Project::loadBootStream(size_t streamIndex) {
 
 		numObjectsLoaded++;
 	}
+
+	if (loaderStack.contexts.size() != 1 || loaderStack.contexts[0].type != ChildLoaderContext::kTypeProject) {
+		error("Boot stream loader finished in an expected state, something didn't finish loading");
+	}
 }
 
 void Project::loadPresentationSettings(const Data::PresentationSettings &presentationSettings) {
@@ -1328,6 +1539,14 @@ size_t Project::recursiveCountLabels(const Data::ProjectLabelMap::LabelTree& tre
 	return numLabels;
 }
 
+ObjectLinkingScope *Project::getPersistentStructuralScope() {
+	return &_structuralScope;
+}
+
+ObjectLinkingScope *Project::getPersistentModifierScope() {
+	return &_modifierScope;
+}
+
 void Project::loadContextualObject(ChildLoaderStack &stack, const Data::DataObject &dataObject) {
 	ChildLoaderContext &topContext = stack.contexts.back();
 	const Data::DataObjectTypes::DataObjectType dataObjectType = dataObject.getType();
@@ -1473,6 +1692,14 @@ bool Section::load(const Data::SectionStructuralDef &data) {
 	return true;
 }
 
+ObjectLinkingScope *Section::getPersistentStructuralScope() {
+	return &_structuralScope;
+}
+
+ObjectLinkingScope *Section::getPersistentModifierScope() {
+	return &_modifierScope;
+}
+
 bool Subsection::load(const Data::SubsectionStructuralDef &data) {
 	_name = data.name;
 	_guid = data.guid;
@@ -1480,6 +1707,18 @@ bool Subsection::load(const Data::SubsectionStructuralDef &data) {
 	return true;
 }
 
+ObjectLinkingScope *Subsection::getPersistentStructuralScope() {
+	return &_structuralScope;
+}
+
+ObjectLinkingScope *Subsection::getPersistentModifierScope() {
+	return &_modifierScope;
+}
+
+uint32 Element::getStreamLocator() const {
+	return _streamLocator;
+}
+
 bool VisualElement::isVisual() const {
 	return true;
 }
@@ -1510,13 +1749,44 @@ bool ModifierFlags::load(const uint32 dataModifierFlags) {
 	return true;
 }
 
-Modifier::Modifier() : _guid(0) {
+Modifier::Modifier() {
 }
 
 Modifier::~Modifier() {
 }
 
-bool Modifier::loadTypicalHeader(const Data::TypicalModifierHeader& typicalHeader) {
+void Modifier::materialize(Runtime *runtime, ObjectLinkingScope *outerScope) {
+	ModifierChildMaterializer childMaterializer(runtime, outerScope);
+	this->visitInternalReferences(&childMaterializer);
+
+	linkInternalReferences();
+	setRuntimeGUID(runtime->allocateRuntimeGUID());
+}
+
+bool Modifier::isAlias() const {
+	return false;
+}
+
+bool Modifier::isVariable() const {
+	return false;
+}
+
+void Modifier::setName(const Common::String& name) {
+	_name = name;
+}
+
+const Common::String& Modifier::getName() const {
+	return _name;
+}
+
+void Modifier::visitInternalReferences(IStructuralReferenceVisitor *visitor) {
+}
+
+bool VariableModifier::isVariable() const {
+	return true;
+}
+
+bool Modifier::loadTypicalHeader(const Data::TypicalModifierHeader &typicalHeader) {
 	if (!_modifierFlags.load(typicalHeader.modifierFlags))
 		return false;
 	_guid = typicalHeader.guid;
@@ -1525,4 +1795,7 @@ bool Modifier::loadTypicalHeader(const Data::TypicalModifierHeader& typicalHeade
 	return true;
 }
 
+void Modifier::linkInternalReferences() {
+}
+
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index b7e7f3c442f..61a3db2a339 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -35,9 +35,13 @@
 namespace MTropolis {
 
 class Asset;
-class Project;
-class PlugIn;
+class Element;
 class Modifier;
+class RuntimeObject;
+class PlugIn;
+class Project;
+class Structural;
+class VisualElement;
 struct IModifierFactory;
 struct IPlugInModifierFactory;
 struct IPlugInModifierFactoryAndDataFactory;
@@ -484,6 +488,35 @@ struct VolumeState {
 	bool isMounted;
 };
 
+class ObjectLinkingScope {
+public:
+	ObjectLinkingScope();
+	~ObjectLinkingScope();
+
+	void setParent(ObjectLinkingScope *parent);
+	void addObject(uint32 guid, const Common::WeakPtr<RuntimeObject> &object);
+
+	void reset();
+
+private:
+	Common::HashMap<uint32, Common::WeakPtr<RuntimeObject> > _guidToObject;
+	ObjectLinkingScope *_parent;
+};
+
+enum SceneState {
+	kSceneStateShared,
+	kSceneStateInactive,
+	kSceneStateActive,
+	kSceneStateUnloaded,
+};
+
+struct SceneStateTransition {
+	SceneStateTransition(VisualElement *scene, SceneState targetState);
+
+	VisualElement *scene;
+	SceneState targetState;
+};
+
 class Runtime {
 public:
 	Runtime();
@@ -492,12 +525,25 @@ public:
 	void queueProject(const Common::SharedPtr<ProjectDescription> &desc);
 
 	void addVolume(int volumeID, const char *name, bool isMounted);
+	void addSceneStateTransition(const SceneStateTransition *transition);
+
+	Project *getProject() const;
+
+	uint32 allocateRuntimeGUID();
 
 private:
+	VThreadState *unloadActiveScene();
+	VThreadState *unloadActiveShared();
+	void unloadProject();
+
 	Common::Array<VolumeState> _volumes;
 	Common::SharedPtr<ProjectDescription> _queuedProjectDesc;
 	Common::SharedPtr<Project> _project;
 	Common::ScopedPtr<VThread> _vthread;
+	ObjectLinkingScope _rootLinkingScope;
+	Common::Array<SceneStateTransition> _pendingSceneStateTransitions;
+
+	uint32 _nextRuntimeGUID;
 };
 
 struct IModifierContainer {
@@ -506,7 +552,7 @@ struct IModifierContainer {
 };
 
 class SimpleModifierContainer : public IModifierContainer {
-
+public:
 	const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const;
 	void appendModifier(const Common::SharedPtr<Modifier> &modifier) override;
 
@@ -514,8 +560,32 @@ private:
 	Common::Array<Common::SharedPtr<Modifier> > _modifiers;
 };
 
-class Structural : public IModifierContainer {
+class RuntimeObject {
+public:
+	RuntimeObject();
+	virtual ~RuntimeObject();
+
+	uint32 getStaticGUID() const;
+	uint32 getRuntimeGUID() const;
+
+	void setRuntimeGUID(uint32 runtimeGUID);
 
+protected:
+	// This is the static GUID stored in the data, it is not guaranteed
+	// to be globally unique at runtime.  In particular, cloning an object
+	// and using aliased modifiers will cause multiple objects with the same
+	uint32 _guid;
+	uint32 _runtimeGUID;
+};
+
+struct IStructuralReferenceVisitor {
+	virtual void visitChildStructuralRef(Common::SharedPtr<Structural> &structural) = 0;
+	virtual void visitChildModifierRef(Common::SharedPtr<Modifier> &modifier) = 0;
+	virtual void visitWeakStructuralRef(Common::SharedPtr<Structural> &structural) = 0;
+	virtual void visitWeakModifierRef(Common::SharedPtr<Modifier> &modifier) = 0;
+};
+
+class Structural : public RuntimeObject, public IModifierContainer {
 public:
 	Structural();
 	virtual ~Structural();
@@ -528,11 +598,18 @@ public:
 	const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const override;
 	void appendModifier(const Common::SharedPtr<Modifier> &modifier) override;
 
+	void materializeSelfAndDescendents(Runtime *runtime, ObjectLinkingScope *outerScope);
+	void materializeDescendents(Runtime *runtime, ObjectLinkingScope *outerScope);
+
 protected:
+	virtual ObjectLinkingScope *getPersistentStructuralScope();
+	virtual ObjectLinkingScope *getPersistentModifierScope();
+
+	virtual void linkInternalReferences(ObjectLinkingScope *outerScope);
+
 	Common::Array<Common::SharedPtr<Structural> > _children;
 	Common::Array<Common::SharedPtr<Modifier> > _modifiers;
 	Common::String _name;
-	uint32 _guid;
 };
 
 struct ProjectPresentationSettings {
@@ -612,6 +689,8 @@ public:
 	~Project();
 
 	void loadFromDescription(const ProjectDescription &desc);
+	Common::SharedPtr<Modifier> resolveAlias(uint32 aliasID) const;
+	void materializeGlobalVariables(Runtime *runtime, ObjectLinkingScope *scope);
 
 private:
 	struct LabelSuperGroup {
@@ -673,6 +752,9 @@ private:
 	void loadLabelMap(const Data::ProjectLabelMap &projectLabelMap);
 	static size_t recursiveCountLabels(const Data::ProjectLabelMap::LabelTree &tree);
 
+	ObjectLinkingScope *getPersistentStructuralScope() override;
+	ObjectLinkingScope *getPersistentModifierScope() override;
+
 	Common::Array<Segment> _segments;
 	Common::Array<StreamDesc> _streams;
 	Common::Array<LabelTree> _labelTree;
@@ -695,22 +777,40 @@ private:
 
 	Common::Array<Common::SharedPtr<PlugIn> > _plugIns;
 	Common::SharedPtr<ProjectResources> _resources;
+	ObjectLinkingScope _structuralScope;
+	ObjectLinkingScope _modifierScope;
 };
 
 class Section : public Structural {
 public:
 	bool load(const Data::SectionStructuralDef &data);
+
+private:
+	ObjectLinkingScope *getPersistentStructuralScope() override;
+	ObjectLinkingScope *getPersistentModifierScope() override;
+
+	ObjectLinkingScope _structuralScope;
+	ObjectLinkingScope _modifierScope;
 };
 
 class Subsection : public Structural {
 public:
 	bool load(const Data::SubsectionStructuralDef &data);
+
+private:
+	ObjectLinkingScope *getPersistentStructuralScope() override;
+	ObjectLinkingScope *getPersistentModifierScope() override;
+
+	ObjectLinkingScope _structuralScope;
+	ObjectLinkingScope _modifierScope;
 };
 
 class Element : public Structural {
 public:
 	virtual bool isVisual() const = 0;
 
+	uint32 getStreamLocator() const;
+
 protected:
 	uint32 _streamLocator;
 	uint16 _sectionID;
@@ -742,17 +842,35 @@ struct ModifierFlags {
 	bool isLastModifier : 1;
 };
 
-class Modifier {
+class Modifier : public RuntimeObject {
 public:
 	Modifier();
 	virtual ~Modifier();
 
+	void materialize(Runtime *runtime, ObjectLinkingScope *outerScope);
+
+	virtual bool isAlias() const;
+	virtual bool isVariable() const;
+
+	void setName(const Common::String &name);
+	const Common::String &getName() const;
+
+	// Shallow clones only need to copy the object.  Descendent copies are done using visitInternalReferences.
+	virtual Common::SharedPtr<Modifier> shallowClone() const = 0;
+
+	virtual void visitInternalReferences(IStructuralReferenceVisitor *visitor);
+
 protected:
-	uint32 _guid;
+	bool loadTypicalHeader(const Data::TypicalModifierHeader &typicalHeader);
+	virtual void linkInternalReferences();
+
 	Common::String _name;
 	ModifierFlags _modifierFlags;
+};
 
-	bool loadTypicalHeader(const Data::TypicalModifierHeader &typicalHeader);
+class VariableModifier : public Modifier {
+public:
+	virtual bool isVariable() const;
 };
 
 class Asset {


Commit: a3b0cb1436fd9f6bf9745da853ba0f71e4423812
    https://github.com/scummvm/scummvm/commit/a3b0cb1436fd9f6bf9745da853ba0f71e4423812
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Stub out scene transitioning and loading

Changed paths:
    engines/mtropolis/notes.txt
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h
    engines/mtropolis/vthread.cpp
    engines/mtropolis/vthread.h


diff --git a/engines/mtropolis/notes.txt b/engines/mtropolis/notes.txt
index 879122baccf..7bda07dfdb1 100644
--- a/engines/mtropolis/notes.txt
+++ b/engines/mtropolis/notes.txt
@@ -57,4 +57,7 @@ This has a bunch of confirmed broken cases:
   be removed on returning.  This can even cause no scenes to be loaded.
 - Doing a regular transition out of ATDS stacked scenes and then returning will
   cause only the top scene to be loaded.
+- Transitioning to the shared scene causes very nonsensical behavior, including
+  doubled-up events, modifiers not working, and other chaos.  Currently the
+  mTropolis engine errors out if you attempt this.
 - Probably a bunch of other cases.
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 7de22447f2c..0471e9efec8 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -75,7 +75,7 @@ bool Point16::load(const Data::Point &point) {
 	return true;
 }
 
-bool Rect16::load(const Data::Rect& rect) {
+bool Rect16::load(const Data::Rect &rect) {
 	top = rect.top;
 	left = rect.left;
 	bottom = rect.bottom;
@@ -84,21 +84,21 @@ bool Rect16::load(const Data::Rect& rect) {
 	return true;
 }
 
-bool IntRange::load(const Data::IntRange& range) {
+bool IntRange::load(const Data::IntRange &range) {
 	max = range.max;
 	min = range.min;
 
 	return true;
 }
 
-bool Label::load(const Data::Label& label) {
+bool Label::load(const Data::Label &label) {
 	id = label.labelID;
 	superGroupID = label.superGroupID;
 
 	return true;
 }
 
-bool ColorRGB8::load(const Data::ColorRGB16& color) {
+bool ColorRGB8::load(const Data::ColorRGB16 &color) {
 	this->r = (color.red * 510 + 1) / 131070;
 	this->g = (color.green * 510 + 1) / 131070;
 	this->b = (color.blue * 510 + 1) / 131070;
@@ -109,7 +109,6 @@ bool ColorRGB8::load(const Data::ColorRGB16& color) {
 MessageFlags::MessageFlags() : relay(true), cascade(true), immediate(true) {
 }
 
-
 DynamicListContainerBase::~DynamicListContainerBase() {
 }
 
@@ -146,7 +145,7 @@ void DynamicListDefaultSetter::defaultSet(Label &value) {
 }
 
 void DynamicListDefaultSetter::defaultSet(Event &value) {
-	value.eventType = 0;
+	value.eventType = EventIDs::EventID::kNothing;
 	value.eventInfo = 0;
 }
 
@@ -234,7 +233,7 @@ bool DynamicListContainer<void>::setAtIndex(size_t index, const DynamicValue &dy
 }
 
 void DynamicListContainer<void>::setFrom(const DynamicListContainerBase &other) {
-	_size = other.getSize();	// ... the only thing we have anyway...
+	_size = other.getSize(); // ... the only thing we have anyway...
 }
 
 const void *DynamicListContainer<void>::getConstArrayPtr() const {
@@ -308,7 +307,6 @@ void DynamicListContainer<VarReference>::rebuildStringPointers() {
 	}
 }
 
-
 DynamicList::DynamicList() : _type(DynamicValueTypes::kEmpty), _container(nullptr) {
 }
 
@@ -414,7 +412,7 @@ bool DynamicList::operator==(const DynamicList &other) const {
 		return other._container == nullptr;
 
 	if (other._container == nullptr)
-		return false;	// (_container == nullptr)
+		return false; // (_container == nullptr)
 
 	return _container->compareEqual(*other._container);
 }
@@ -433,8 +431,7 @@ void DynamicList::swap(DynamicList &other) {
 }
 
 bool DynamicList::changeToType(DynamicValueTypes::DynamicValueType type) {
-	switch (type)
-	{
+	switch (type) {
 	case DynamicValueTypes::kNull:
 		_container = new DynamicListContainer<void>();
 		break;
@@ -562,7 +559,7 @@ bool DynamicValue::load(const Data::InternalTypeTaggedValue &data, const Common:
 	return true;
 }
 
-bool DynamicValue::load(const Data::PlugInTypeTaggedValue& data) {
+bool DynamicValue::load(const Data::PlugInTypeTaggedValue &data) {
 	switch (data.type) {
 	case Data::PlugInTypeTaggedValue::kNull:
 		_type = DynamicValueTypes::kNull;
@@ -605,7 +602,7 @@ bool DynamicValue::load(const Data::PlugInTypeTaggedValue& data) {
 		_type = DynamicValueTypes::kVariableReference;
 		_value.asVarReference.guid = data.value.asVarRefGUID;
 		_value.asVarReference.source = &_str;
-		_str.clear();	// Extra data doesn't seem to correlate to this
+		_str.clear(); // Extra data doesn't seem to correlate to this
 		break;
 	case Data::PlugInTypeTaggedValue::kPoint:
 		_type = DynamicValueTypes::kPoint;
@@ -620,7 +617,6 @@ bool DynamicValue::load(const Data::PlugInTypeTaggedValue& data) {
 	return true;
 }
 
-
 DynamicValueTypes::DynamicValueType DynamicValue::getType() const {
 	return _type;
 }
@@ -754,7 +750,7 @@ void DynamicValue::clear() {
 	_type = DynamicValueTypes::kNull;
 }
 
-void DynamicValue::initFromOther(const DynamicValue& other) {
+void DynamicValue::initFromOther(const DynamicValue &other) {
 	assert(_type == DynamicValueTypes::kNull);
 
 	switch (_type) {
@@ -838,8 +834,24 @@ bool MessengerSendSpec::load(const Data::PlugInTypeTaggedValue &dataEvent, const
 	return true;
 }
 
+Event Event::create() {
+	Event evt;
+	evt.eventInfo = 0;
+	evt.eventType = EventIDs::kNothing;
+
+	return evt;
+}
+
+Event Event::create(EventIDs::EventID eventType, uint32 eventInfo) {
+	Event evt;
+	evt.eventType = eventType;
+	evt.eventInfo = eventInfo;
+
+	return evt;
+}
+
 bool Event::load(const Data::Event &data) {
-	eventType = data.eventID;
+	eventType = static_cast<EventIDs::EventID>(data.eventID);
 	eventInfo = data.eventInfo;
 
 	return true;
@@ -926,7 +938,24 @@ void RuntimeObject::setRuntimeGUID(uint32 runtimeGUID) {
 	_runtimeGUID = runtimeGUID;
 }
 
-Structural::Structural() {
+
+MessageProperties::MessageProperties(const Event &evt, const DynamicValue &value, const Common::WeakPtr<RuntimeObject> &source)
+	: _evt(evt), _value(value), _source(source) {
+}
+
+const Event &MessageProperties::getEvent() const {
+	return _evt;
+}
+
+const DynamicValue& MessageProperties::getValue() const {
+	return _value;
+}
+
+const Common::WeakPtr<RuntimeObject>& MessageProperties::getSource() const {
+	return _source;
+}
+
+Structural::Structural() : _parent(nullptr) {
 }
 
 Structural::~Structural() {
@@ -941,6 +970,32 @@ const Common::Array<Common::SharedPtr<Structural> > &Structural::getChildren() c
 
 void Structural::addChild(const Common::SharedPtr<Structural>& child) {
 	_children.push_back(child);
+	child->setParent(this);
+}
+
+void Structural::removeAllChildren() {
+	_children.clear();
+}
+
+void Structural::removeAllModifiers() {
+	_modifiers.clear();
+}
+
+void Structural::removeChild(Structural* child) {
+	for (size_t i = 0; i < _children.size(); i++) {
+		if (_children[i].get() == child) {
+			_children.remove_at(i);
+			return;
+		}
+	}
+}
+
+Structural* Structural::getParent() const {
+	return _parent;
+}
+
+void Structural::setParent(Structural *parent) {
+	_parent = parent;
 }
 
 const Common::String &Structural::getName() const {
@@ -955,6 +1010,15 @@ void Structural::appendModifier(const Common::SharedPtr<Modifier> &modifier) {
 	_modifiers.push_back(modifier);
 }
 
+bool Structural::respondsToEvent(const Event &evt) const {
+	return false;
+}
+
+VThreadState Structural::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) const {
+	assert(false);
+	return kVThreadError;
+}
+
 void Structural::materializeSelfAndDescendents(Runtime *runtime, ObjectLinkingScope *outerScope) {
 	linkInternalReferences(outerScope);
 	setRuntimeGUID(runtime->allocateRuntimeGUID());
@@ -1048,7 +1112,177 @@ void ObjectLinkingScope::reset() {
 	_guidToObject.clear(true);
 }
 
-SceneStateTransition::SceneStateTransition(VisualElement *scene, SceneState targetState) : scene(scene), targetState(targetState) {
+LowLevelSceneStateTransitionAction::LowLevelSceneStateTransitionAction(const Common::SharedPtr<MessageDispatch> &msg)
+	: _actionType(LowLevelSceneStateTransitionAction::kSendMessage), _msg(msg) {
+}
+
+LowLevelSceneStateTransitionAction::LowLevelSceneStateTransitionAction(const LowLevelSceneStateTransitionAction &other)
+	: _actionType(other._actionType), _msg(other._msg), _scene(other._scene) {
+}
+
+LowLevelSceneStateTransitionAction::LowLevelSceneStateTransitionAction(const Common::SharedPtr<Structural> &scene, ActionType actionType)
+	: _scene(scene), _actionType(actionType) {
+}
+
+LowLevelSceneStateTransitionAction::ActionType LowLevelSceneStateTransitionAction::getActionType() const {
+	return _actionType;
+}
+
+const Common::SharedPtr<Structural>& LowLevelSceneStateTransitionAction::getScene() const {
+	return _scene;
+}
+
+const Common::SharedPtr<MessageDispatch>& LowLevelSceneStateTransitionAction::getMessage() const {
+	return _msg;
+}
+
+LowLevelSceneStateTransitionAction &LowLevelSceneStateTransitionAction::operator=(const LowLevelSceneStateTransitionAction &other) {
+	_scene = other._scene;
+	_msg = other._msg;
+	_actionType = other._actionType;
+
+	return *this;
+}
+
+
+HighLevelSceneTransition::HighLevelSceneTransition(const Common::SharedPtr<Structural> &scene, Type type, bool addToDestinationScene, bool addToReturnList)
+	: scene(scene), type(type), addToDestinationScene(addToDestinationScene), addToReturnList(addToReturnList) {
+}
+
+MessageDispatch::MessageDispatch(const Event &evt, Structural *root, bool cascade, bool relay)
+	: _cascade(cascade), _relay(relay), _terminated(false) {
+	PropagationStack topEntry;
+	topEntry.index = 0;
+	topEntry.propagationStage = PropagationStack::kStageSendToStructuralSelf;
+	topEntry.ptr.structural = root;
+
+	_propagationStack.push_back(topEntry);
+}
+
+MessageDispatch::MessageDispatch(const Event &evt, Modifier *root, bool cascade, bool relay)
+	: _cascade(cascade), _relay(relay), _terminated(false) {
+	_msg.reset(new MessageProperties(evt, DynamicValue(), Common::WeakPtr<RuntimeObject>()));
+
+	PropagationStack topEntry;
+	topEntry.index = 0;
+	topEntry.propagationStage = PropagationStack::kStageSendToModifier;
+	topEntry.ptr.modifier = root;
+
+	_propagationStack.push_back(topEntry);
+}
+
+bool MessageDispatch::isTerminated() const {
+	return _terminated;
+}
+
+VThreadState MessageDispatch::continuePropagating(Runtime *runtime) {
+	// By the point this function is called, continuePropagating has been re-posted to the VThread,
+	// so any propagation state changed in this function will be handled after any VThread tasks
+	// posted here.
+	while (_propagationStack.size() > 0) {
+		PropagationStack &stackTop = _propagationStack.back();
+
+		switch (stackTop.propagationStage)
+		{
+		case PropagationStack::kStageSendToModifier: {
+				Modifier *modifier = stackTop.ptr.modifier;
+				_propagationStack.pop_back();
+
+				// Handle the action in the VThread
+				bool responds = modifier->respondsToEvent(_msg->getEvent());
+
+				// Queue propagation to children, if any, when the VThread task is done
+				if (responds && !_relay) {
+					_terminated = true;
+				} else {
+					IModifierContainer *childContainer = modifier->getMessagePropagationContainer();
+					if (childContainer && childContainer->getModifiers().size() > 0) {
+						PropagationStack childPropagation;
+						childPropagation.propagationStage = PropagationStack::kStageSendToModifierContainer;
+						childPropagation.index = 0;
+						childPropagation.ptr.modifierContainer = childContainer;
+						_propagationStack.push_back(childPropagation);
+					}
+				}
+
+				// Post to the message action itself to VThread
+				runtime->postConsumeMessageTask(modifier, _msg);
+				return kVThreadReturn;
+			} break;
+		case PropagationStack::kStageSendToModifierContainer: {
+				IModifierContainer *container = stackTop.ptr.modifierContainer;
+				const Common::Array<Common::SharedPtr<Modifier> > &children = container->getModifiers();
+				if (stackTop.index >= children.size()) {
+					_propagationStack.pop_back();
+				} else {
+					Common::SharedPtr<Modifier> target = children[stackTop.index++];
+
+					PropagationStack modifierPropagation;
+					modifierPropagation.propagationStage = PropagationStack::kStageSendToModifier;
+					modifierPropagation.index = 0;
+					modifierPropagation.ptr.modifier = target.get();
+					_propagationStack.push_back(modifierPropagation);
+				}
+			} break;
+		case PropagationStack::kStageSendToStructuralChildren: {
+				Structural *structural = stackTop.ptr.structural;
+				const Common::Array<Common::SharedPtr<Structural> > &children = structural->getChildren();
+				if (stackTop.index >= children.size()) {
+					_propagationStack.pop_back();
+				} else {
+					PropagationStack childPropagation;
+					childPropagation.propagationStage = PropagationStack::kStageSendToStructuralSelf;
+					childPropagation.index = 0;
+					childPropagation.ptr.structural = structural;
+					_propagationStack.push_back(childPropagation);
+				}
+			} break;
+		case PropagationStack::kStageSendToStructuralSelf: {
+				Structural *structural = stackTop.ptr.structural;
+				stackTop.propagationStage = PropagationStack::kStageSendToStructuralModifiers;
+				stackTop.index = 0;
+				stackTop.ptr.structural = structural;
+
+				bool responds = structural->respondsToEvent(_msg->getEvent());
+
+				if (responds && !_relay) {
+					_terminated = true;
+				}
+
+				runtime->postConsumeMessageTask(structural, _msg);
+				return kVThreadReturn;
+			} break;
+		case PropagationStack::kStageSendToStructuralModifiers: {
+				Structural *structural = stackTop.ptr.structural;
+
+				// Once done with modifiers, propagate to children if set to cascade
+				if (_cascade) {
+					stackTop.propagationStage = PropagationStack::kStageSendToStructuralChildren;
+					stackTop.index = 0;
+					stackTop.ptr.structural = structural;
+				} else {
+					_propagationStack.pop_back();
+				}
+
+				if (structural->getModifiers().size() > 0) {
+					PropagationStack modifierContainerPropagation;
+					modifierContainerPropagation.propagationStage = PropagationStack::kStageSendToModifierContainer;
+					modifierContainerPropagation.index = 0;
+					modifierContainerPropagation.ptr.modifierContainer = structural;
+					_propagationStack.push_back(modifierContainerPropagation);
+				}
+			} break;
+		default:
+			return kVThreadError;
+		}
+	}
+
+	_terminated = true;
+
+	return kVThreadReturn;
+}
+
+Runtime::SceneStackEntry::SceneStackEntry() {
 }
 
 Runtime::Runtime() : _nextRuntimeGUID(1) {
@@ -1056,56 +1290,386 @@ Runtime::Runtime() : _nextRuntimeGUID(1) {
 }
 
 void Runtime::runFrame() {
-	VThreadState state = _vthread->step();
-	if (state != kVThreadReturn) {
-		// Still doing blocking tasks
-		return;
-	}
+	for (;;) {
+		VThreadState state = _vthread->step();
+		if (state != kVThreadReturn) {
+			// Still doing blocking tasks
+			return;
+		}
+
+		if (_queuedProjectDesc) {
+			Common::SharedPtr<ProjectDescription> desc = _queuedProjectDesc;
+			_queuedProjectDesc.reset();
+
+			unloadProject();
+
+			_project.reset(new Project());
 
-	if (_queuedProjectDesc) {
-		Common::SharedPtr<ProjectDescription> desc = _queuedProjectDesc;
-		_queuedProjectDesc.reset();
+			_project->loadFromDescription(*desc);
 
-		unloadProject();
+			_rootLinkingScope.addObject(_project->getStaticGUID(), _project);
 
-		_project.reset(new Project());
+			// We have to materialize global variables because they are not cloned from aliases.
+			debug(1, "Materializing global variables...");
+			_project->materializeGlobalVariables(this, &_rootLinkingScope);
 
-		_project->loadFromDescription(*desc);
+			debug(1, "Materializing project...");
+			_project->materializeSelfAndDescendents(this, &_rootLinkingScope);
 
-		_rootLinkingScope.addObject(_project->getStaticGUID(), _project);
+			debug(1, "Project is fully loaded!  Starting up...");
 
-		// We have to materialize global variables because they are not cloned from aliases.
-		debug(1, "Materializing global variables...");
-		_project->materializeGlobalVariables(this, &_rootLinkingScope);
+			if (_project->getChildren().size() == 0) {
+				error("Project has no sections");
+			}
+
+			Structural *firstSection = _project->getChildren()[0].get();
+			if (firstSection->getChildren().size() == 0) {
+				error("Project has no subsections");
+			}
 
-		debug(1, "Materializing project...");
-		_project->materializeSelfAndDescendents(this, &_rootLinkingScope);
+			Structural *firstSubsection = firstSection->getChildren()[0].get();
+			if (firstSubsection->getChildren().size() < 2) {
+				error("Project has no subsections");
+			}
+
+			_pendingSceneTransitions.push_back(HighLevelSceneTransition(firstSubsection->getChildren()[1], HighLevelSceneTransition::kTypeChangeToScene, false, false));
+			continue;
+		}
 
-		debug(1, "Project is fully loaded!  Starting up...");
+		if (_messageQueue.size() > 0) {
+			Common::SharedPtr<MessageDispatch> msg = _messageQueue[0];
+			_messageQueue.remove_at(0);
 
-		if (_project->getChildren().size() == 0) {
-			error("Project has no sections");
+			sendMessageOnVThread(msg);
+			continue;
 		}
 
-		Structural *firstSection = _project->getChildren()[0].get();
-		if (firstSection->getChildren().size() == 0) {
-			error("Project has no subsections");
+		// Teardowns must only occur during idle conditions where there are no queued message and no VThread tasks
+		if (_pendingTeardowns.size() > 0) {
+			for (Common::Array<Teardown>::const_iterator it = _pendingTeardowns.begin(), itEnd = _pendingTeardowns.end(); it != itEnd; ++it) {
+				executeTeardown(*it);
+			}
+			_pendingTeardowns.clear();
+			continue;
 		}
 
-		Structural *firstSubsection = firstSection->getChildren()[0].get();
-		if (firstSubsection->getChildren().size() < 2) {
-			error("Project has no subsections");
+		if (_pendingLowLevelTransitions.size() > 0) {
+			LowLevelSceneStateTransitionAction transition = _pendingLowLevelTransitions[0];
+			_pendingLowLevelTransitions.remove_at(0);
+
+			executeLowLevelSceneStateTransition(transition);
+			continue;
 		}
 
-		_pendingSceneStateTransitions.push_back(SceneStateTransition(static_cast<VisualElement *>(firstSubsection->getChildren()[0].get()), kSceneStateShared));
-		_pendingSceneStateTransitions.push_back(SceneStateTransition(static_cast<VisualElement *>(firstSubsection->getChildren()[1].get()), kSceneStateActive));
+		if (_pendingSceneTransitions.size() > 0) {
+			HighLevelSceneTransition transition = _pendingSceneTransitions[0];
+			_pendingSceneTransitions.remove_at(0);
+
+			executeHighLevelSceneTransition(transition);
+			continue;
+		}
 	}
 }
 
+Common::SharedPtr<Structural> Runtime::findDefaultSharedSceneForScene(Structural *scene) {
+	Structural *subsection = scene->getParent();
+	const Common::Array<Common::SharedPtr<Structural> > &children = subsection->getChildren();
+	if (children.size() == 0 || children[0].get() == scene)
+		return Common::SharedPtr<Structural>();
+
+	return children[0];
+}
+
+void Runtime::executeTeardown(const Teardown &teardown) {
+	Common::SharedPtr<Structural> structural = teardown.structural.lock();
+	if (!structural)
+		return;	// Already gone
+
+	if (teardown.onlyRemoveChildren) {
+		structural->removeAllChildren();
+		structural->removeAllModifiers();
+	} else {
+		Structural *parent = structural->getParent();
+
+		// Nothing should be holding strong references to structural objects after they're removed from the project
+		assert(parent != nullptr);
+
+		if (!parent) {
+			return; // Already unlinked but still alive somehow
+		}
+
+		parent->removeChild(structural.get());
+
+		structural->setParent(nullptr);
+	}
+}
+
+void Runtime::executeLowLevelSceneStateTransition(const LowLevelSceneStateTransitionAction &action) {
+	switch (action.getActionType())
+	{
+	case LowLevelSceneStateTransitionAction::kSendMessage:
+		sendMessageOnVThread(action.getMessage());
+		break;
+	case LowLevelSceneStateTransitionAction::kLoad:
+		loadScene(action.getScene());
+		break;
+	case LowLevelSceneStateTransitionAction::kUnload: {
+			Teardown teardown;
+			teardown.onlyRemoveChildren = true;
+			teardown.structural = action.getScene();
+
+			_pendingTeardowns.push_back(teardown);
+		} break;
+	default:
+		assert(false);
+		break;
+	}
+}
+
+void Runtime::executeCompleteTransitionToScene(const Common::SharedPtr<Structural> &targetScene) {
+	if (targetScene == _activeMainScene)
+		return;
+
+	if (_sceneStack.size() == 0)
+		_sceneStack.resize(1);	// Reserve shared scene slot
+
+	Common::SharedPtr<Structural> targetSharedScene = findDefaultSharedSceneForScene(targetScene.get());
+
+	if (targetScene == targetSharedScene)
+		error("Transitioned into a default shared scene, this is not supported");
+
+	if (_activeMainScene == targetSharedScene)
+		error("Transitioned into scene currently being used as a target scene, this is not supported");
+
+	bool sceneAlreadyInStack = false;
+	for (size_t i = _sceneStack.size() - 1; i > 0; i--) {
+		Common::SharedPtr<Structural> stackedScene = _sceneStack[i].scene;
+		if (stackedScene == targetScene) {
+			sceneAlreadyInStack = true;
+		} else {
+			_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSceneEnded, 0), _activeMainScene.get(), false, true))));
+			_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kParentDisabled, 0), _activeMainScene.get(), true, true))));
+			_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(_activeMainScene, LowLevelSceneStateTransitionAction::kUnload));
+
+			if (stackedScene == targetSharedScene)
+				error("Transitioned to a shared scene which was already on the stack as a normal scene.  This is not supported.");
+
+			_sceneStack.remove_at(i);
+		}
+	}
+
+	if (targetSharedScene != _activeSharedScene) {
+		if (_activeSharedScene) {
+			_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSceneEnded, 0), _activeSharedScene.get(), false, true))));
+			_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kParentDisabled, 0), _activeSharedScene.get(), true, true))));
+			_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(_activeSharedScene, LowLevelSceneStateTransitionAction::kUnload));
+		}
+
+		_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(targetSharedScene, LowLevelSceneStateTransitionAction::kLoad));
+		_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kParentEnabled, 0), targetSharedScene.get(), true, true))));
+		_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSceneStarted, 0), targetSharedScene.get(), false, true))));
+
+		SceneStackEntry sharedSceneEntry;
+		sharedSceneEntry.scene = targetScene;
+
+		_sceneStack[0] = sharedSceneEntry;
+	}
+
+	if (!sceneAlreadyInStack) {
+		_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(targetScene, LowLevelSceneStateTransitionAction::kLoad));
+		_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kParentEnabled, 0), targetScene.get(), true, true))));
+		_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSceneStarted, 0), targetScene.get(), false, true))));
+
+		SceneStackEntry sceneEntry;
+		sceneEntry.scene = targetScene;
+
+		_sceneStack.push_back(sceneEntry);
+	}
+
+	_activeMainScene = targetScene;
+	_activeSharedScene = targetSharedScene;
+
+	executeSharedScenePostSceneChangeActions();
+}
+
+void Runtime::executeHighLevelSceneTransition(const HighLevelSceneTransition &transition) {
+	if (_sceneStack.size() == 0)
+		_sceneStack.resize(1); // Reserve shared scene slot
+
+	// This replicates a bunch of quirks/bugs of mTropolis's scene transition logic,
+	// see the accompanying notes file.  There are probably some missing cases related to
+	// shared scene, calling return/scene transitions during scene deactivation, or other
+	// edge cases that hopefully no games actually do!
+	switch (transition.type) {
+	case HighLevelSceneTransition::kTypeReturn: {
+			if (_sceneReturnList.size() == 0) {
+				warning("A scene return was requested, but no scenes are in the scene return list");
+				return;
+			}
+
+			const SceneReturnListEntry &sceneReturn = _sceneReturnList.back();
+
+			if (sceneReturn.scene == _activeSharedScene)
+				error("Transitioned into the active shared scene as the main scene, this is not supported");
+
+			if (sceneReturn.scene != _activeMainScene) {
+				assert(_activeMainScene.get() != nullptr); // There should always be an active main scene after the first transition
+
+				if (sceneReturn.isAddToDestinationSceneTransition) {
+					// In this case we unload the active main scene and reactivate the old main
+					_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSceneEnded, 0), _activeMainScene.get(), false, true))));
+					_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kParentDisabled, 0), _activeMainScene.get(), true, true))));
+					_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(_activeMainScene, LowLevelSceneStateTransitionAction::kUnload));
+
+					_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSceneReactivated, 0), _activeMainScene.get(), false, true))));
+
+					_activeMainScene = sceneReturn.scene;
+
+					executeSharedScenePostSceneChangeActions();
+				} else {
+					executeCompleteTransitionToScene(sceneReturn.scene);
+				}
+			}
+		} break;
+	case HighLevelSceneTransition::kTypeChangeToScene: {
+			const Common::SharedPtr<Structural> targetScene = transition.scene;
+
+			if (transition.addToDestinationScene || !transition.addToReturnList) {
+				SceneReturnListEntry returnListEntry;
+				returnListEntry.isAddToDestinationSceneTransition = transition.addToDestinationScene;
+				returnListEntry.scene = _activeMainScene;
+				_sceneReturnList.push_back(returnListEntry);
+			}
+
+			if (transition.addToDestinationScene) {
+				if (targetScene != _activeMainScene) {
+					Common::SharedPtr<Structural> targetSharedScene = findDefaultSharedSceneForScene(targetScene.get());
+
+					if (targetScene == targetSharedScene)
+						error("Transitioned into a default shared scene, this is not supported");
+
+					if (_activeMainScene == targetSharedScene)
+						error("Transitioned into scene currently being used as a target scene, this is not supported");
+
+					_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSceneDeactivated, 0), _activeMainScene.get(), false, true))));
+
+					if (targetSharedScene != _activeSharedScene) {
+						if (_activeSharedScene) {
+							_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSceneEnded, 0), _activeSharedScene.get(), false, true))));
+							_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kParentDisabled, 0), _activeSharedScene.get(), true, true))));
+							_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(_activeSharedScene, LowLevelSceneStateTransitionAction::kUnload));
+						}
+
+						_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(targetSharedScene, LowLevelSceneStateTransitionAction::kLoad));
+						_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kParentEnabled, 0), targetSharedScene.get(), true, true))));
+						_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSceneStarted, 0), targetSharedScene.get(), false, true))));
+
+						SceneStackEntry sharedSceneEntry;
+						sharedSceneEntry.scene = targetScene;
+
+						_sceneStack[0] = sharedSceneEntry;
+					}
+
+						
+					bool sceneAlreadyInStack = false;
+					for (size_t i = _sceneStack.size() - 1; i > 0; i--) {
+						Common::SharedPtr<Structural> stackedScene = _sceneStack[i].scene;
+						if (stackedScene == targetScene) {
+							sceneAlreadyInStack = true;
+							break;
+						}
+					}
+
+					// This is probably wrong if it's already in the stack, but transitioning to already-in-stack scenes is extremely buggy in mTropolis Player anyway
+					if (!sceneAlreadyInStack) {
+						_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(targetScene, LowLevelSceneStateTransitionAction::kLoad));
+						_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kParentEnabled, 0), targetScene.get(), true, true))));
+						_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSceneStarted, 0), targetScene.get(), false, true))));
+
+						SceneStackEntry sceneEntry;
+						sceneEntry.scene = targetScene;
+
+						_sceneStack.push_back(sceneEntry);
+					}
+
+					_activeMainScene = targetScene;
+					_activeSharedScene = targetSharedScene;
+
+					executeSharedScenePostSceneChangeActions();
+				}
+			} else {
+				executeCompleteTransitionToScene(targetScene);
+			}
+		} break;
+	default:
+		break;
+	}
+}
+
+void Runtime::executeSharedScenePostSceneChangeActions() {
+	Structural *subsection = _activeMainScene->getParent();
+	const Common::Array<Common::SharedPtr<Structural> > &subsectionScenes = subsection->getChildren();
+
+	_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSharedSceneSceneChanged, 0), _activeSharedScene.get(), false, true))));
+	if (subsectionScenes.size() > 1) {
+		if (_activeMainScene == subsectionScenes[subsectionScenes.size() - 1])
+			_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSharedSceneNoNextScene, 0), _activeSharedScene.get(), false, true))));
+		if (_activeMainScene == subsectionScenes[1])
+			_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSharedSceneNoPrevScene, 0), _activeSharedScene.get(), false, true))));
+	}
+}
+
+void Runtime::loadScene(const Common::SharedPtr<Structural>& scene) {
+	Element *element = static_cast<Element *>(scene.get());
+	uint32 streamIndex = element->getStreamLocator() & 0xffff;	// Not actually sure how many bits are legal here
+
+	error("TODO: Load scene");
+}
+
+void Runtime::sendMessageOnVThread(const Common::SharedPtr<MessageDispatch> &dispatch) {
+	DispatchMethodTaskData *taskData = _vthread->pushTask(this, &Runtime::dispatchMessageTask);
+	taskData->dispatch = dispatch;
+}
+
+VThreadState Runtime::dispatchMessageTask(const DispatchMethodTaskData &data) {
+	Common::SharedPtr<MessageDispatch> dispatchPtr = data.dispatch;
+	MessageDispatch &dispatch = *dispatchPtr.get();
+
+	if (dispatch.isTerminated())
+		return kVThreadReturn;
+	else {
+		// Requeue propagation after whatever happens with this propagation step
+		DispatchMethodTaskData *requeueData = _vthread->pushTask(this, &Runtime::dispatchMessageTask);
+		requeueData->dispatch = dispatchPtr;
+
+		return dispatch.continuePropagating(this);
+	}
+}
+
+VThreadState Runtime::consumeMessageTask(const ConsumeMessageTaskData &data) {
+	IMessageConsumer *consumer = data.consumer;
+	assert(consumer->respondsToEvent(data.message->getEvent()));
+	return consumer->consumeMessage(this, data.message);
+}
+
+void Runtime::queueMessage(const Common::SharedPtr<MessageDispatch>& dispatch) {
+	_messageQueue.push_back(dispatch);
+}
+
 void Runtime::unloadProject() {
+	_activeMainScene.reset();
+	_activeSharedScene.reset();
+	_sceneStack.clear();
+	_sceneReturnList.clear();
+	_pendingLowLevelTransitions.clear();
+	_pendingSceneTransitions.clear();
+	_pendingTeardowns.clear();
+	_messageQueue.clear();
+	_vthread.reset(new VThread());
+
+	// These should be last
 	_project.reset();
 	_rootLinkingScope.reset();
-	_pendingSceneStateTransitions.clear();
 }
 
 void Runtime::queueProject(const Common::SharedPtr<ProjectDescription> &desc) {
@@ -1121,10 +1685,20 @@ void Runtime::addVolume(int volumeID, const char *name, bool isMounted) {
 	_volumes.push_back(volume);
 }
 
+void Runtime::addSceneStateTransition(const HighLevelSceneTransition &transition) {
+	_pendingSceneTransitions.push_back(transition);
+}
+
 Project *Runtime::getProject() const {
 	return _project.get();
 }
 
+void Runtime::postConsumeMessageTask(IMessageConsumer *consumer, const Common::SharedPtr<MessageProperties> &msg) {
+	ConsumeMessageTaskData *params = _vthread->pushTask(this, &Runtime::consumeMessageTask);
+	params->consumer = consumer;
+	params->message = msg;
+}
+
 uint32 Runtime::allocateRuntimeGUID() {
 	return _nextRuntimeGUID++;
 }
@@ -1576,6 +2150,7 @@ void Project::loadContextualObject(ChildLoaderStack &stack, const Data::DataObje
 					error("Failed to load section");
 
 				project->addChild(section);
+				section->setParent(project);
 
 				// For some reason all section objects have the "no more siblings" structural flag.
 				// There doesn't appear to be any indication of how many section objects there will
@@ -1771,6 +2346,20 @@ bool Modifier::isVariable() const {
 	return false;
 }
 
+IModifierContainer *Modifier::getMessagePropagationContainer() const {
+	return nullptr;
+}
+
+bool Modifier::respondsToEvent(const Event &evt) const {
+	return false;
+}
+
+VThreadState Modifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) const {
+	// If you're here, a message type was reported as responsive by respondsToEvent but consumeMessage wasn't overrided
+	assert(false);
+	return kVThreadError;
+}
+
 void Modifier::setName(const Common::String& name) {
 	_name = name;
 }
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 61a3db2a339..e37a8071711 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -36,17 +36,133 @@ namespace MTropolis {
 
 class Asset;
 class Element;
+class MessageDispatch;
+struct MessageProperties;
 class Modifier;
 class RuntimeObject;
 class PlugIn;
 class Project;
+class Runtime;
 class Structural;
 class VisualElement;
+struct IMessageConsumer;
+struct IModifierContainer;
 struct IModifierFactory;
 struct IPlugInModifierFactory;
 struct IPlugInModifierFactoryAndDataFactory;
 struct ModifierLoaderContext;
 
+
+
+namespace DynamicValueTypes {
+
+enum DynamicValueType {
+	kInvalid,
+
+	kNull,
+	kInteger,
+	kFloat,
+	kPoint,
+	kIntegerRange,
+	kBoolean,
+	kVector,
+	kLabel,
+	kEvent,
+	kVariableReference,
+	kIncomingData,
+	kString,
+	kList,
+
+	kEmpty,
+};
+
+} // End of namespace DynamicValuesTypes
+
+namespace AttributeIDs {
+
+	enum AttributeID {
+	kAttribCache = 55,
+	kAttribDirect = 56,
+	kAttribVisible = 58,
+	kAttribLayer = 24,
+	kAttribPosition = 1,
+	kAttribWidth = 2,
+	kAttribHeight = 3,
+};
+
+} // End of namespace AttributeIDs
+
+namespace EventIDs {
+
+enum EventID {
+	kNothing = 0,
+
+	kElementEnableEdit = 207,
+	kElementDisableEdit = 220,
+	kElementSelect = 209,
+	kElementDeselect = 210,
+	kElementToggleSelect = 213,
+	kElementUpdatedCalculated = 219,
+	kElementShow = 222,
+	kElementHide = 223,
+
+	kElementScrollUp = 1001,
+	kElementScrollDown = 1002,
+	kElementScrollRight = 1005,
+	kElementScrollLeft = 1006,
+
+	kMotionStarted = 501,
+	kMotionEnded = 502,
+
+	kTransitionStarted = 503,
+	kTransitionEnded = 504,
+
+	kMouseUp = 301,
+	kMouseDown = 302,
+	kMouseOver = 303,
+	kMouseOutside = 304,
+	kMouseTrackedInside = 305,
+	kMouseTracking = 306,
+	kMouseTrackedOutside = 307,
+	kMouseUpInside = 309,
+	kMouseUpOutside = 310,
+
+	kSceneStarted = 101,
+	kSceneEnded = 102,
+	kSceneDeactivated = 103,
+	kSceneReactivated = 104,
+	kSceneTransitionEnded = 506,
+
+	kSharedSceneReturnedToScene = 401,
+	kSharedSceneSceneChanged = 402,
+	kSharedSceneNoNextScene = 403,
+	kSharedSceneNoPrevScene = 404,
+
+	kParentEnabled = 2001,
+	kParentDisabled = 2002,
+	kParentChanged = 227,
+
+	kPreloadMedia = 1701,
+	kFlushMedia = 1703,
+	kPrerollMedia = 1704,
+
+	kCloseProject = 1601,
+
+	kUserTimeout = 1801,
+	kProjectStarted = 1802,
+	kProjectEnded = 1803,
+
+	kAttribGet = 1300,
+	kAttribSet = 1200,
+
+	kClone = 226,
+	kKill = 228,
+
+	kAuthorMessage = 900,
+};
+
+} // End of namespace EventIDs
+
 struct Point16 {
 	int16 x;
 	int16 y;
@@ -110,9 +226,12 @@ struct Label {
 };
 
 struct Event {
-	uint32 eventType;
+	EventIDs::EventID eventType;
 	uint32 eventInfo;
 
+	static Event create();
+	static Event create(EventIDs::EventID eventType, uint32 eventInfo);
+
 	bool load(const Data::Event &data);
 
 	inline bool operator==(const Event &other) const {
@@ -166,28 +285,6 @@ struct MessageFlags {
 	bool immediate : 1;
 };
 
-namespace DynamicValueTypes {
-	enum DynamicValueType {
-		kInvalid,
-
-		kNull,
-		kInteger,
-		kFloat,
-		kPoint,
-		kIntegerRange,
-		kBoolean,
-		kVector,
-		kLabel,
-		kEvent,
-		kVariableReference,
-		kIncomingData,
-		kString,
-		kList,
-
-		kEmpty,
-	};
-}
-
 struct DynamicValue;
 struct DynamicList;
 
@@ -418,6 +515,13 @@ struct MessengerSendSpec {
 	uint32 destination; // May be a MessageDestination or GUID
 };
 
+struct Message {
+	Message();
+
+	Event evt;
+	DynamicValue data;
+};
+
 enum MessageDestination {
 	kMessageDestSharedScene = 0x65,
 	kMessageDestScene = 0x66,
@@ -503,18 +607,78 @@ private:
 	ObjectLinkingScope *_parent;
 };
 
-enum SceneState {
-	kSceneStateShared,
-	kSceneStateInactive,
-	kSceneStateActive,
-	kSceneStateUnloaded,
+struct LowLevelSceneStateTransitionAction {
+	enum ActionType {
+		kLoad,
+		kUnload,
+		kSendMessage,
+	};
+
+	explicit LowLevelSceneStateTransitionAction(const Common::SharedPtr<MessageDispatch> &msg);
+	LowLevelSceneStateTransitionAction(const LowLevelSceneStateTransitionAction &other);
+	LowLevelSceneStateTransitionAction(const Common::SharedPtr<Structural> &scene, ActionType actionType);
+
+	ActionType getActionType() const;
+	const Common::SharedPtr<Structural> &getScene() const;
+	const Common::SharedPtr<MessageDispatch> &getMessage() const;
+
+	LowLevelSceneStateTransitionAction &operator=(const LowLevelSceneStateTransitionAction &other);
+
+private:
+	ActionType _actionType;
+	Common::SharedPtr<Structural> _scene;
+	Common::SharedPtr<MessageDispatch> _msg;
+};
+
+struct HighLevelSceneTransition {
+	enum Type {
+		kTypeReturn,
+		kTypeChangeToScene,
+	};
+
+	HighLevelSceneTransition(const Common::SharedPtr<Structural> &scene, Type type, bool addToDestinationScene, bool addToReturnList);
+
+	Common::SharedPtr<Structural> scene;
+	Type type;
+	bool addToDestinationScene;
+	bool addToReturnList;
 };
 
-struct SceneStateTransition {
-	SceneStateTransition(VisualElement *scene, SceneState targetState);
+class MessageDispatch {
+public:
+	MessageDispatch(const Event &evt, Structural *root, bool cascade, bool relay);
+	MessageDispatch(const Event &evt, Modifier *root, bool cascade, bool relay);
+
+	bool isTerminated() const;
+	VThreadState continuePropagating(Runtime *runtime);
+
+private:
+	struct PropagationStack {
+		union Ptr {
+			Structural *structural;
+			Modifier *modifier;
+			IModifierContainer *modifierContainer;
+		};
+
+		enum PropagationStage {
+			kStageSendToModifier,
+			kStageSendToModifierContainer,
+
+			kStageSendToStructuralSelf,
+			kStageSendToStructuralModifiers,
+			kStageSendToStructuralChildren,
+		};
+
+		PropagationStage propagationStage;
+		size_t index;
+		Ptr ptr;
+	};
 
-	VisualElement *scene;
-	SceneState targetState;
+	Common::Array<PropagationStack> _propagationStack;
+	Common::SharedPtr<MessageProperties> _msg;
+	bool _cascade;	// Traverses structure tree
+	bool _relay;	// Fire on multiple modifiers
+	bool _terminated;
 };
 
 class Runtime {
@@ -525,23 +689,73 @@ public:
 	void queueProject(const Common::SharedPtr<ProjectDescription> &desc);
 
 	void addVolume(int volumeID, const char *name, bool isMounted);
-	void addSceneStateTransition(const SceneStateTransition *transition);
+	void addSceneStateTransition(const HighLevelSceneTransition &transition);
 
 	Project *getProject() const;
 
+	void postConsumeMessageTask(IMessageConsumer *msgConsumer, const Common::SharedPtr<MessageProperties> &msg);
+
 	uint32 allocateRuntimeGUID();
 
 private:
-	VThreadState *unloadActiveScene();
-	VThreadState *unloadActiveShared();
+	struct SceneStackEntry {
+		SceneStackEntry();
+
+		Common::SharedPtr<Structural> scene;
+	};
+
+	struct Teardown {
+		Common::WeakPtr<Structural> structural;
+		bool onlyRemoveChildren;
+	};
+
+	struct SceneReturnListEntry {
+		Common::SharedPtr<Structural> scene;
+		bool isAddToDestinationSceneTransition;
+	};
+
+	struct DispatchMethodTaskData {
+		Common::SharedPtr<MessageDispatch> dispatch;
+	};
+
+	struct ConsumeMessageTaskData {
+		IMessageConsumer *consumer;
+		Common::SharedPtr<MessageProperties> message;
+	};
+
+	static Common::SharedPtr<Structural> findDefaultSharedSceneForScene(Structural *scene);
+	void executeTeardown(const Teardown &teardown);
+	void executeLowLevelSceneStateTransition(const LowLevelSceneStateTransitionAction &transitionAction);
+	void executeHighLevelSceneTransition(const HighLevelSceneTransition &transition);
+	void executeCompleteTransitionToScene(const Common::SharedPtr<Structural> &scene);
+	void executeSharedScenePostSceneChangeActions();
+
+	void loadScene(const Common::SharedPtr<Structural> &scene);
+
+	// Sending a message on the VThread means "immediately"
+	void sendMessageOnVThread(const Common::SharedPtr<MessageDispatch> &dispatch);
+	void queueMessage(const Common::SharedPtr<MessageDispatch> &dispatch);
+
 	void unloadProject();
 
+	VThreadState dispatchMessageTask(const DispatchMethodTaskData &data);
+	VThreadState consumeMessageTask(const ConsumeMessageTaskData &data);
+
 	Common::Array<VolumeState> _volumes;
 	Common::SharedPtr<ProjectDescription> _queuedProjectDesc;
 	Common::SharedPtr<Project> _project;
 	Common::ScopedPtr<VThread> _vthread;
+	Common::Array<Common::SharedPtr<MessageDispatch> > _messageQueue;
 	ObjectLinkingScope _rootLinkingScope;
-	Common::Array<SceneStateTransition> _pendingSceneStateTransitions;
+
+	Common::Array<Teardown> _pendingTeardowns;
+	Common::Array<LowLevelSceneStateTransitionAction> _pendingLowLevelTransitions;
+	Common::Array<HighLevelSceneTransition> _pendingSceneTransitions;
+	Common::Array<SceneStackEntry> _sceneStack;
+	Common::SharedPtr<Structural> _activeMainScene;
+	Common::SharedPtr<Structural> _activeSharedScene;
+	Common::Array<SceneReturnListEntry> _sceneReturnList;
+
 
 	uint32 _nextRuntimeGUID;
 };
@@ -578,6 +792,19 @@ protected:
 	uint32 _runtimeGUID;
 };
 
+struct MessageProperties {
+	MessageProperties(const Event &evt, const DynamicValue &value, const Common::WeakPtr<RuntimeObject> &source);
+
+	const Event &getEvent() const;
+	const DynamicValue &getValue() const;
+	const Common::WeakPtr<RuntimeObject> &getSource() const;
+
+private:
+	Event _evt;
+	DynamicValue _value;
+	Common::WeakPtr<RuntimeObject> _source;
+};
+
 struct IStructuralReferenceVisitor {
 	virtual void visitChildStructuralRef(Common::SharedPtr<Structural> &structural) = 0;
 	virtual void visitChildModifierRef(Common::SharedPtr<Modifier> &modifier) = 0;
@@ -585,19 +812,34 @@ struct IStructuralReferenceVisitor {
 	virtual void visitWeakModifierRef(Common::SharedPtr<Modifier> &modifier) = 0;
 };
 
-class Structural : public RuntimeObject, public IModifierContainer {
+struct IMessageConsumer {
+	// These should only be implemented as direct responses - child traversal is handled by the message propagation process
+	virtual bool respondsToEvent(const Event &evt) const = 0;
+	virtual VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) const = 0;
+};
+
+class Structural : public RuntimeObject, public IModifierContainer, public IMessageConsumer {
 public:
 	Structural();
 	virtual ~Structural();
 
 	const Common::Array<Common::SharedPtr<Structural> > &getChildren() const;
 	void addChild(const Common::SharedPtr<Structural> &child);
+	void removeAllChildren();
+	void removeAllModifiers();
+	void removeChild(Structural *child);
+
+	Structural *getParent() const;
+	void setParent(Structural *parent);
 
 	const Common::String &getName() const;
 
 	const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const override;
 	void appendModifier(const Common::SharedPtr<Modifier> &modifier) override;
 
+	bool respondsToEvent(const Event &evt) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) const override;
+
 	void materializeSelfAndDescendents(Runtime *runtime, ObjectLinkingScope *outerScope);
 	void materializeDescendents(Runtime *runtime, ObjectLinkingScope *outerScope);
 
@@ -607,6 +849,7 @@ protected:
 
 	virtual void linkInternalReferences(ObjectLinkingScope *outerScope);
 
+	Structural *_parent;
 	Common::Array<Common::SharedPtr<Structural> > _children;
 	Common::Array<Common::SharedPtr<Modifier> > _modifiers;
 	Common::String _name;
@@ -621,7 +864,6 @@ struct ProjectPresentationSettings {
 };
 
 class AssetInfo {
-
 public:
 	AssetInfo();
 	virtual ~AssetInfo();
@@ -842,7 +1084,7 @@ struct ModifierFlags {
 	bool isLastModifier : 1;
 };
 
-class Modifier : public RuntimeObject {
+class Modifier : public RuntimeObject, public IMessageConsumer {
 public:
 	Modifier();
 	virtual ~Modifier();
@@ -852,6 +1094,12 @@ public:
 	virtual bool isAlias() const;
 	virtual bool isVariable() const;
 
+	// This should only return a propagation container if messages should actually be propagated (i.e. NOT switched-off behaviors!)
+	virtual IModifierContainer *getMessagePropagationContainer() const;
+
+	bool respondsToEvent(const Event &evt) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) const override;
+
 	void setName(const Common::String &name);
 	const Common::String &getName() const;
 
diff --git a/engines/mtropolis/vthread.cpp b/engines/mtropolis/vthread.cpp
index 44513d61f7a..3fd2f13b5b8 100644
--- a/engines/mtropolis/vthread.cpp
+++ b/engines/mtropolis/vthread.cpp
@@ -46,11 +46,13 @@ VThreadState VThread::step() {
 	void *dataPtr;
 	void *framePtr;
 	while (popFrame(dataPtr, framePtr)) {
-		VThreadStackFrame *frame = static_cast<VThreadStackFrame *>(framePtr);
-		bool isHandling = (frame->faultID == _faultID);
+		VThreadTaskData *data = static_cast<VThreadTaskData *>(dataPtr);
+
+		const bool isHandling = (data->handlesFault(_faultID));
 		static_cast<VThreadStackFrame *>(framePtr)->~VThreadStackFrame();
 		if (isHandling) {
-			VThreadState state = static_cast<VThreadTaskData *>(dataPtr)->destructAndRunTask();
+			_faultID = nullptr;
+			VThreadState state = data->destructAndRunTask();
 			if (state != kVThreadReturn)
 				return state;
 		} else {
diff --git a/engines/mtropolis/vthread.h b/engines/mtropolis/vthread.h
index 4982096111e..08598c30b09 100644
--- a/engines/mtropolis/vthread.h
+++ b/engines/mtropolis/vthread.h
@@ -54,15 +54,12 @@ public:
 
 	virtual VThreadState destructAndRunTask() = 0;
 	virtual void relocateTo(void *newPosition) = 0;
-
-	virtual bool isFaultHandler() = 0;
-	virtual bool handlesFault() = 0;
+	virtual bool handlesFault(const VThreadFaultIdentifier *faultID) const = 0;
 };
 
 struct VThreadStackFrame {
 	size_t taskDataOffset;	// Offset to VThreadTaskData
 	size_t prevFrameOffset;
-	VThreadFaultIdentifier *faultID;
 
 #ifdef MTROPOLIS_DEBUG_VTHREAD_STACKS
 	VThreadTaskData *data;
@@ -74,7 +71,7 @@ template<typename TClass, typename TData>
 class VThreadMethodData : public VThreadTaskData {
 
 public:
-	VThreadMethodData(VThreadFaultIdentifier *faultID, TClass *target, VThreadState (TClass::*method)(const TData &data));
+	VThreadMethodData(const VThreadFaultIdentifier *faultID, TClass *target, VThreadState (TClass::*method)(const TData &data));
 	VThreadMethodData(const VThreadMethodData &other);
 
 #if __cplusplus >= 201103L
@@ -83,11 +80,12 @@ public:
 
 	VThreadState destructAndRunTask() override;
 	void relocateTo(void *newPosition) override;
+	bool handlesFault(const VThreadFaultIdentifier *faultID) const override;
 
 	TData &getData();
 
 private:
-	VThreadFaultIdentifier *_faultID;
+	const VThreadFaultIdentifier *_faultID;
 	TClass *_target;
 	VThreadState (TClass::*_method)(const TData &data);
 	TData _data;
@@ -97,7 +95,7 @@ template<typename TData>
 class VThreadFunctionData : public VThreadTaskData {
 
 public:
-	explicit VThreadFunctionData(VThreadFaultIdentifier *faultID, VThreadState (*func)(const TData &data));
+	explicit VThreadFunctionData(const VThreadFaultIdentifier *faultID, VThreadState (*func)(const TData &data));
 	VThreadFunctionData(const VThreadFunctionData &other);
 
 #if __cplusplus >= 201103L
@@ -106,11 +104,12 @@ public:
 
 	VThreadState destructAndRunTask() override;
 	void relocateTo(void *newPosition) override;
+	bool handlesFault(const VThreadFaultIdentifier *faultID) const override;
 
 	TData &getData();
 
 private:
-	VThreadFaultIdentifier *_faultID;
+	const VThreadFaultIdentifier *_faultID;
 	VThreadState (*_func)(const TData &data);
 	TData _data;
 };
@@ -137,10 +136,10 @@ public:
 
 private:
 	template<typename TClass, typename TData>
-	TData *pushTaskWithFaultHandler(VThreadFaultIdentifier *faultID, TClass *obj, VThreadState (TClass::*method)(const TData &data));
+	TData *pushTaskWithFaultHandler(const VThreadFaultIdentifier *faultID, TClass *obj, VThreadState (TClass::*method)(const TData &data));
 
 	template<typename TData>
-	TData *pushTaskWithFaultHandler(VThreadFaultIdentifier *faultID, VThreadState (*func)(const TData &data));
+	TData *pushTaskWithFaultHandler(const VThreadFaultIdentifier *faultID, VThreadState (*func)(const TData &data));
 
 	void reserveFrame(size_t size, size_t alignment, void *&outFramePtr, void *&outUnadjustedDataPtr, size_t &outPrevFrameOffset);
 	bool popFrame(void *&dataPtr, void *&outFramePtr);
@@ -154,7 +153,7 @@ private:
 };
 
 template<typename TClass, typename TData>
-VThreadMethodData<TClass, TData>::VThreadMethodData(VThreadFaultIdentifier *faultID, TClass *target, VThreadState (TClass::*method)(const TData &data))
+VThreadMethodData<TClass, TData>::VThreadMethodData(const VThreadFaultIdentifier *faultID, TClass *target, VThreadState (TClass::*method)(const TData &data))
 	: _faultID(faultID), _target(target), _method(method) {
 }
 
@@ -185,7 +184,7 @@ VThreadState VThreadMethodData<TClass, TData>::destructAndRunTask() {
 
 	this->~VThreadMethodData<TClass, TData>();
 
-	return target->*method(data);
+	return (target->*method)(data);
 }
 
 template<typename TClass, typename TData>
@@ -199,13 +198,18 @@ void VThreadMethodData<TClass, TData>::relocateTo(void *newPosition) {
 #endif
 }
 
+template<typename TClass, typename TData>
+bool VThreadMethodData<TClass, TData>::handlesFault(const VThreadFaultIdentifier* faultID) const {
+	return _faultID == faultID;
+}
+
 template<typename TClass, typename TData>
 TData &VThreadMethodData<TClass, TData>::getData() {
 	return _data;
 }
 
 template<typename TData>
-VThreadFunctionData<TData>::VThreadFunctionData(VThreadFaultIdentifier *faultID, VThreadState (*func)(const TData &data))
+VThreadFunctionData<TData>::VThreadFunctionData(const VThreadFaultIdentifier *faultID, VThreadState (*func)(const TData &data))
 	: _faultID(faultID), _func(func) {
 }
 
@@ -249,6 +253,11 @@ void VThreadFunctionData<TData>::relocateTo(void *newPosition) {
 #endif
 }
 
+template<typename TData>
+bool VThreadFunctionData<TData>::handlesFault(const VThreadFaultIdentifier *faultID) const {
+	return _faultID == faultID;
+}
+
 template<typename TData>
 TData &VThreadFunctionData<TData>::getData() {
 	return _data;
@@ -276,7 +285,7 @@ TData *VThread::pushFaultHandler(VThreadState (*func)(const TData &data)) {
 
 
 template<typename TClass, typename TData>
-TData *VThread::pushTaskWithFaultHandler(VThreadFaultIdentifier *faultID, TClass *obj, VThreadState (TClass::*method)(const TData &data)) {
+TData *VThread::pushTaskWithFaultHandler(const VThreadFaultIdentifier *faultID, TClass *obj, VThreadState (TClass::*method)(const TData &data)) {
 	typedef VThreadMethodData<TClass, TData> FrameData_t; 
 
 	const size_t frameAlignment = AlignmentHelper<VThreadStackFrame>::getAlignment();
@@ -285,15 +294,13 @@ TData *VThread::pushTaskWithFaultHandler(VThreadFaultIdentifier *faultID, TClass
 
 	void *framePtr;
 	void *dataPtr;
-	size_t dataOffset;
 	size_t prevFrameOffset;
 	reserveFrame(sizeof(FrameData_t), maxAlignment, framePtr, dataPtr, prevFrameOffset);
 
 	VThreadStackFrame *frame = new (framePtr) VThreadStackFrame();
 	frame->prevFrameOffset = prevFrameOffset;
-	frame->faultID = faultID;
 
-	FrameData_t *frameData = new (dataPtr) FrameData_t(obj, method);
+	FrameData_t *frameData = new (dataPtr) FrameData_t(faultID, obj, method);
 	frame->taskDataOffset = reinterpret_cast<char *>(static_cast<VThreadTaskData *>(frameData)) - static_cast<char *>(_stackAlignedBase);
 
 #ifdef MTROPOLIS_DEBUG_VTHREAD_STACKS
@@ -301,11 +308,11 @@ TData *VThread::pushTaskWithFaultHandler(VThreadFaultIdentifier *faultID, TClass
 	frame->data = frameData;
 #endif
 
-	return &frameData->data;
+	return &frameData->getData();
 }
 
 template<typename TData>
-TData *VThread::pushTaskWithFaultHandler(VThreadFaultIdentifier *faultID, VThreadState (*func)(const TData &data)) {
+TData *VThread::pushTaskWithFaultHandler(const VThreadFaultIdentifier *faultID, VThreadState (*func)(const TData &data)) {
 	typedef VThreadFunctionData<TData> FrameData_t;
 
 	const size_t frameAlignment = AlignmentHelper<VThreadStackFrame>::getAlignment();
@@ -319,9 +326,8 @@ TData *VThread::pushTaskWithFaultHandler(VThreadFaultIdentifier *faultID, VThrea
 
 	VThreadStackFrame *frame = new (framePtr) VThreadStackFrame();
 	frame->prevFrameOffset = prevFrameOffset;
-	frame->faultID = faultID;
 
-	FrameData_t *frameData = new (dataPtr) FrameData_t(func);
+	FrameData_t *frameData = new (dataPtr) FrameData_t(faultID, func);
 	frame->taskDataOffset = reinterpret_cast<char *>(static_cast<VThreadTaskData *>(frameData)) - static_cast<char *>(_stackAlignedBase);
 
 #ifdef MTROPOLIS_DEBUG_VTHREAD_STACKS


Commit: 3f6c5d37bc98f45ab674f13b511d3b8b5b532554
    https://github.com/scummvm/scummvm/commit/3f6c5d37bc98f45ab674f13b511d3b8b5b532554
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Recognize some missing Miniscript instruction flags

Changed paths:
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/miniscript.h


diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index b2b917aeec0..8a064e9e073 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -90,7 +90,7 @@ bool MiniscriptInstructionLoader<MiniscriptInstructions::GetChild>::loadInstruct
 	if (!instrDataReader.readU32(childAttribute))
 		return false;
 
-	new (dest) MiniscriptInstructions::GetChild(childAttribute);
+	new (dest) MiniscriptInstructions::GetChild(childAttribute, (instrFlags & 1) != 0, (instrFlags & 32) != 0);
 	return true;
 }
 
@@ -100,7 +100,7 @@ bool MiniscriptInstructionLoader<MiniscriptInstructions::PushGlobal>::loadInstru
 	if (!instrDataReader.readU32(globalID))
 		return false;
 
-	new (dest) MiniscriptInstructions::PushGlobal(globalID);
+	new (dest) MiniscriptInstructions::PushGlobal(globalID, (instrFlags & 1) != 0);
 	return true;
 }
 
@@ -128,39 +128,39 @@ bool MiniscriptInstructionLoader<MiniscriptInstructions::PushValue>::loadInstruc
 		return false;
 
 	if (dataType == 0)
-		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeNull, nullptr);
+		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeNull, nullptr, false);
 	else if (dataType == 0x15) {
 		Data::XPFloat f;
 		if (!f.load(instrDataReader))
 			return false;
 
 		double d = f.toDouble();
-		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeDouble, &d);
+		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeDouble, &d, false);
 	} else if (dataType == 0x1a) {
 		uint8 boolValue;
 		if (!instrDataReader.readU8(boolValue))
 			return false;
 
 		bool b = (boolValue != 0);
-		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeBool, &b);
+		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeBool, &b, false);
 	} else if (dataType == 0x1f9) {
 		uint32 refValue;
 		if (!instrDataReader.readU32(refValue))
 			return false;
 
-		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeLocalRef, &refValue);
+		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeLocalRef, &refValue, (instrFlags & 1) != 0);
 	} else if (dataType == 0x1fa) {
 		uint32 refValue;
 		if (!instrDataReader.readU32(refValue))
 			return false;
 
-		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeGlobalRef, &refValue);
+		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeGlobalRef, &refValue, (instrFlags & 1) != 0);
 	} else if (dataType == 0x1d) {
 		MiniscriptInstructions::PushValue::Label label;
 		if (!instrDataReader.readU32(label.superGroup) || !instrDataReader.readU32(label.id))
 			return false;
 
-		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeLabel, &label);
+		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeLabel, &label, false);
 	} else
 		return false;
 
@@ -443,10 +443,12 @@ Send::Send(const Event &evt) : _evt(evt) {
 BuiltinFunc::BuiltinFunc(BuiltinFunctionID bfid) : _funcID(bfid) {
 }
 
-GetChild::GetChild(uint32 attribute) : _attribute(attribute) {
+GetChild::GetChild(uint32 attribute, bool isLValue, bool isIndexed)
+	: _attribute(attribute), _isLValue(isLValue), _isIndexed(isIndexed) {
 }
 
-PushValue::PushValue(DataType dataType, const void *value) {
+PushValue::PushValue(DataType dataType, const void *value, bool isLValue)
+	: _dataType(dataType), _isLValue(isLValue) {
 	switch (dataType) {
 	case DataType::kDataTypeBool:
 		_value.b = *static_cast<const bool *>(value);
@@ -466,7 +468,7 @@ PushValue::PushValue(DataType dataType, const void *value) {
 	}
 }
 
-PushGlobal::PushGlobal(uint32 guid) : _guid(guid), _refSetIndex(0) {
+PushGlobal::PushGlobal(uint32 guid, bool isLValue) : _guid(guid), _refSetIndex(0), _isLValue(isLValue) {
 }
 
 uint32 PushGlobal::getStaticGUID() const {
diff --git a/engines/mtropolis/miniscript.h b/engines/mtropolis/miniscript.h
index 830791c3ae7..934a79b085c 100644
--- a/engines/mtropolis/miniscript.h
+++ b/engines/mtropolis/miniscript.h
@@ -187,10 +187,12 @@ namespace MiniscriptInstructions {
 
 	class GetChild : public UnimplementedInstruction {
 	public:
-		explicit GetChild(uint32 attribute);
+		GetChild(uint32 attribute, bool isLValue, bool isIndexed);
 
 	private:
 		uint32 _attribute;
+		bool _isLValue;
+		bool _isIndexed;
 	};
 
 	class ListAppend : public UnimplementedInstruction {
@@ -215,7 +217,7 @@ namespace MiniscriptInstructions {
 			uint32 id;
 		};
 
-		PushValue(DataType dataType, const void *value);
+		PushValue(DataType dataType, const void *value, bool isLValue);
 
 	private:
 		union ValueUnion {
@@ -227,11 +229,12 @@ namespace MiniscriptInstructions {
 
 		DataType _dataType;
 		ValueUnion _value;
+		bool _isLValue;
 	};
 
 	class PushGlobal : public UnimplementedInstruction {
 	public:
-		explicit PushGlobal(uint32 guid);
+		explicit PushGlobal(uint32 guid, bool isLValue);
 
 		uint32 getStaticGUID() const;
 		void setReferenceSetIndex(size_t refSetIndex);
@@ -240,6 +243,7 @@ namespace MiniscriptInstructions {
 	private:
 		uint32 _guid;
 		size_t _refSetIndex;
+		bool _isLValue;
 	};
 
 	class PushString : public UnimplementedInstruction {


Commit: 9a7acd460e6d8732ddd821d4b7aa62ea17d2dcac
    https://github.com/scummvm/scummvm/commit/9a7acd460e6d8732ddd821d4b7aa62ea17d2dcac
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix build

Changed paths:
    engines/mtropolis/module.mk


diff --git a/engines/mtropolis/module.mk b/engines/mtropolis/module.mk
index cd6dd681604..2734e0d08a7 100644
--- a/engines/mtropolis/module.mk
+++ b/engines/mtropolis/module.mk
@@ -2,6 +2,7 @@ MODULE := engines/mtropolis
 
 MODULE_OBJS = \
 	asset_factory.o \
+	assets.o \
 	console.o \
 	data.o \
 	detection.o \


Commit: 3432d0180bb179d405e106f60f41236bca90a1f9
    https://github.com/scummvm/scummvm/commit/3432d0180bb179d405e106f60f41236bca90a1f9
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix text comment

Changed paths:
    engines/mtropolis/detection_tables.h


diff --git a/engines/mtropolis/detection_tables.h b/engines/mtropolis/detection_tables.h
index f7b0a735133..d6e9cad616d 100644
--- a/engines/mtropolis/detection_tables.h
+++ b/engines/mtropolis/detection_tables.h
@@ -82,7 +82,7 @@ static const MTropolisGameDescription gameDescriptions[] = {
 };
 
 /**
- * The fallback game descriptor used by the Made engine's fallbackDetector.
+ * The fallback game descriptor used by the mTropolis engine's fallbackDetector.
  * Contents of this struct are to be overwritten by the fallbackDetector.
  */
 static MTropolisGameDescription g_fallbackDesc = {


Commit: b55268fa7eb4ca5906bd35ca02ba400b0668325e
    https://github.com/scummvm/scummvm/commit/b55268fa7eb4ca5906bd35ca02ba400b0668325e
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Stub out some debugger things, add Win cursor loading

Changed paths:
  A engines/mtropolis/debug.cpp
  A engines/mtropolis/debug.h
    engines/mtropolis/console.cpp
    engines/mtropolis/console.h
    engines/mtropolis/detection.cpp
    engines/mtropolis/detection_tables.h
    engines/mtropolis/elements.h
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/module.mk
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/mtropolis.h
    engines/mtropolis/plugin/obsidian.h
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h
    engines/mtropolis/vthread.h


diff --git a/engines/mtropolis/console.cpp b/engines/mtropolis/console.cpp
index 35b3045c433..b76e257701f 100644
--- a/engines/mtropolis/console.cpp
+++ b/engines/mtropolis/console.cpp
@@ -23,8 +23,10 @@
 
 namespace MTropolis {
 
-Console::Console() {
+Console::Console(MTropolisEngine *engine) : _engine(engine) {
 }
 
+Console::~Console() {
+}
 
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/console.h b/engines/mtropolis/console.h
index 46a730dc29f..161a8aa110e 100644
--- a/engines/mtropolis/console.h
+++ b/engines/mtropolis/console.h
@@ -26,10 +26,15 @@
 
 namespace MTropolis {
 
+class MTropolisEngine;
+
 class Console : public GUI::Debugger {
 public:
-	explicit Console();
-	~Console(void) override {}
+	explicit Console(MTropolisEngine *engine);
+	~Console() override;
+
+private:
+	MTropolisEngine *_engine;
 };
 }
 
diff --git a/engines/mtropolis/debug.cpp b/engines/mtropolis/debug.cpp
new file mode 100644
index 00000000000..cb2afc3f35b
--- /dev/null
+++ b/engines/mtropolis/debug.cpp
@@ -0,0 +1,52 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mtropolis/debug.h"
+
+#include "gui/dialog.h"
+
+namespace MTropolis {
+
+DebugInspector::DebugInspector(IDebuggable *debuggable) {
+}
+
+DebugInspector::~DebugInspector() {
+}
+
+void DebugInspector::onDestroyed() {
+	_debuggable = nullptr;
+}
+
+Debugger::Debugger() : _paused(false) {
+}
+
+Debugger::~Debugger() {
+}
+
+void Debugger::setPaused(bool paused) {
+	_paused = paused;
+}
+
+bool Debugger::isPaused() const {
+	return _paused;
+}
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/debug.h b/engines/mtropolis/debug.h
new file mode 100644
index 00000000000..8852236e589
--- /dev/null
+++ b/engines/mtropolis/debug.h
@@ -0,0 +1,81 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_DEBUG_H
+#define MTROPOLIS_DEBUG_H
+
+#include "common/str.h"
+#include "common/ptr.h"
+
+#define MTROPOLIS_DEBUG_VTHREAD_STACKS
+#define MTROPOLIS_DEBUG_ENABLE
+
+namespace MTropolis {
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
+struct IDebuggable;
+
+enum SupportStatus {
+	kSupportStatusNone,
+	kSupportStatusPartial,
+	kSupportStatusDone,
+};
+
+class DebugInspector {
+public:
+	explicit DebugInspector(IDebuggable *debuggable);
+	virtual ~DebugInspector();
+
+	virtual void onDestroyed();
+
+private:
+	IDebuggable *_debuggable;
+};
+
+class Debugger {
+public:
+	Debugger();
+	~Debugger();
+
+	void setPaused(bool paused);
+	bool isPaused() const;
+
+private:
+	bool _paused;
+};
+
+#endif
+
+struct IDebuggable {
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	virtual SupportStatus debugGetSupportStatus() const = 0;
+	virtual const char *debugGetTypeName() const = 0;
+	virtual const Common::String &debugGetName() const = 0;
+	virtual Common::SharedPtr<DebugInspector> debugGetInspector() const = 0;
+#endif
+};
+
+} // End of namespace MTropolis
+
+#endif /* MTROPOLIS_DEBUG_H */
+
+
+
diff --git a/engines/mtropolis/detection.cpp b/engines/mtropolis/detection.cpp
index 52b665e6991..46d91eee4e9 100644
--- a/engines/mtropolis/detection.cpp
+++ b/engines/mtropolis/detection.cpp
@@ -25,6 +25,7 @@
 #include "mtropolis/detection.h"
 
 #include "common/config-manager.h"
+#include "common/translation.h"
 
 static const PlainGameDescriptor mTropolisGames[] = {
 	{"obsidian", "Obsidian"},
@@ -65,8 +66,24 @@ public:
 };
 
 const ExtraGuiOptions MTropolisMetaEngineDetection::getExtraGuiOptions(const Common::String &target) const {
-
 	ExtraGuiOptions options;
+
+	static const ExtraGuiOption launchDebugOption = {
+		_s("Start with debugger"),
+		_s("Starts with the debugger dashboard active"),
+		"mtropolis_debug_at_start",
+		false
+	};
+	static const ExtraGuiOption launchBreakOption = {
+		_s("Start debugging immediately"),
+		_s("Halts progress and stops at the debugger immediately"),
+		"mtropolis_pause_at_start",
+		false
+	};
+
+	options.push_back(launchDebugOption);
+	options.push_back(launchBreakOption);
+
 	return options;
 }
 
diff --git a/engines/mtropolis/detection_tables.h b/engines/mtropolis/detection_tables.h
index d6e9cad616d..61b01e78421 100644
--- a/engines/mtropolis/detection_tables.h
+++ b/engines/mtropolis/detection_tables.h
@@ -56,6 +56,7 @@ static const MTropolisGameDescription gameDescriptions[] = {
 			"obsidian",
 			"V1.0, 1/13/97, installed, CD",
 			{
+				{ "Obsidian.exe", 0, "0b50a779136ae6c9cc8bcfa3148c1127", -1 },
 				{ "Obsidian.c95", 0, "fea68ff30ff319cdab30b79d2850a480", -1 },
 				{ "RSGKit.r95", 0, "071dc9098f9610fcec45c96342b1b69a", -1 },
 				{ "MCURSORS.C95", 0, "dcbe480913eebf233d0cdc33809bf048", -1 },
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index 5a9c20be913..fe5b1144ba3 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -36,6 +36,10 @@ public:
 
 	bool load(ElementLoaderContext &context, const Data::GraphicElement &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Graphic Element"; }
+#endif
+
 private:
 	bool _directToScreen;
 	bool _cacheBitmap;
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 8a064e9e073..8c3bb896700 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -21,6 +21,7 @@
 
 #include "mtropolis/miniscript.h"
 #include "mtropolis/alignment_helper.h"
+#include "common/config-manager.h"
 
 #include "common/memstream.h"
 
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 9b18c48b688..692c87a082e 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -38,6 +38,10 @@ public:
 	const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const override;
 	void appendModifier(const Common::SharedPtr<Modifier> &modifier) override;
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Behavior Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 	void visitInternalReferences(IStructuralReferenceVisitor *visitor) override;
@@ -52,6 +56,10 @@ class MiniscriptModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::MiniscriptModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Miniscript Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -65,6 +73,10 @@ class MessengerModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::MessengerModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Messenger Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -76,6 +88,10 @@ class SetModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::SetModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Set Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -89,6 +105,10 @@ public:
 	bool load(ModifierLoaderContext &context, const Data::AliasModifier &data);
 	uint32 getAliasID() const;
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Alias Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -99,6 +119,10 @@ class ChangeSceneModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::ChangeSceneModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Change Scene Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -122,6 +146,10 @@ class SoundEffectModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::SoundEffectModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Sound Effect Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -141,6 +169,10 @@ class DragMotionModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::DragMotionModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Drag Motion Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -162,6 +194,10 @@ class VectorMotionModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::VectorMotionModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Vector Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -192,6 +228,10 @@ public:
 		kTransitionDirectionRight = 0x387,
 	};
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Scene Transition Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -220,6 +260,10 @@ public:
 		kRevealTypeConceal = 1,
 	};
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Element Transition Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -236,6 +280,10 @@ class IfMessengerModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::IfMessengerModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "If Messenger Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -250,6 +298,10 @@ class TimerMessengerModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::TimerMessengerModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Timer Messenger Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -264,6 +316,10 @@ class BoundaryDetectionMessengerModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::BoundaryDetectionMessengerModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Boundary Detection Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -292,6 +348,10 @@ class CollisionDetectionMessengerModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::CollisionDetectionMessengerModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Collision Detection Messenger Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -317,6 +377,10 @@ class KeyboardMessengerModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::KeyboardMessengerModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Keyboard Messenger Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -377,6 +441,10 @@ public:
 		bool load(uint8 dataStyleFlags);
 	};
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Text Style Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -417,6 +485,10 @@ public:
 		kShapeStar = 0xb,	// 5-point star, horizontal arms are at (top+bottom*2)/3
 	};
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Graphic Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -439,6 +511,10 @@ class CompoundVariableModifier : public VariableModifier, public IModifierContai
 public:
 	bool load(ModifierLoaderContext &context, const Data::CompoundVariableModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Compound Variable Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -453,6 +529,10 @@ class BooleanVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::BooleanVariableModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Boolean Variable Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -463,6 +543,10 @@ class IntegerVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::IntegerVariableModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Integer Variable Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -473,6 +557,10 @@ class IntegerRangeVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::IntegerRangeVariableModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Integer Range Variable Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -483,6 +571,10 @@ class VectorVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::VectorVariableModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Vector Variable Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -493,6 +585,10 @@ class PointVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::PointVariableModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Point Variable Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -503,6 +599,10 @@ class FloatingPointVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::FloatingPointVariableModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Floating Point Variable Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -513,6 +613,10 @@ class StringVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::StringVariableModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "String Variable Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
diff --git a/engines/mtropolis/module.mk b/engines/mtropolis/module.mk
index 2734e0d08a7..3f2162ae601 100644
--- a/engines/mtropolis/module.mk
+++ b/engines/mtropolis/module.mk
@@ -5,6 +5,7 @@ MODULE_OBJS = \
 	assets.o \
 	console.o \
 	data.o \
+	debug.o \
 	detection.o \
 	element_factory.o \
 	elements.o \
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index 6d83115bfe0..0f8a352a538 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -21,6 +21,7 @@
 
 #include "mtropolis/mtropolis.h"
 #include "mtropolis/console.h"
+#include "mtropolis/debug.h"
 #include "mtropolis/runtime.h"
 
 #include "mtropolis/plugins.h"
@@ -28,12 +29,47 @@
 
 #include "common/config-manager.h"
 #include "common/debug.h"
+#include "common/events.h"
+#include "common/file.h"
 #include "common/macresman.h"
 #include "common/ptr.h"
 #include "common/stuffit.h"
+#include "common/system.h"
+#include "common/winexe.h"
+
+#include "engines/util.h"
+
+#include "graphics/pixelformat.h"
+#include "graphics/cursorman.h"
+#include "graphics/wincursor.h"
 
 namespace MTropolis {
 
+static bool loadCursorsFromPE(CursorGraphicCollection &cursorGraphics, Common::SeekableReadStream *stream) {
+	Common::SharedPtr<Common::WinResources> winRes(Common::WinResources::createFromEXE(stream));
+	if (!winRes) {
+		warning("Couldn't load resources from PE file");
+		return false;
+	}
+
+	Common::Array<Common::WinResourceID> cursorGroupIDs = winRes->getIDList(Common::kWinGroupCursor);
+	for (Common::Array<Common::WinResourceID>::const_iterator it = cursorGroupIDs.begin(), itEnd = cursorGroupIDs.end(); it != itEnd; ++it) {
+		const Common::WinResourceID &id = *it;
+
+		Common::SharedPtr<Graphics::WinCursorGroup> cursorGroup(Graphics::WinCursorGroup::createCursorGroup(winRes.get(), *it));
+		if (!winRes) {
+			warning("Couldn't load cursor group");
+			return false;
+		}
+
+		if (cursorGroup->cursors.size() == 0) {
+			// Empty?
+			continue;
+		}
+
+		cursorGraphics.addWinCursorGroup(id.getID(), cursorGroup);
+	}
+}
 
 struct MacObsidianResources : public ProjectResources {
 	MacObsidianResources();
@@ -112,18 +148,32 @@ MTropolisEngine::MTropolisEngine(OSystem *syst, const MTropolisGameDescription *
 }
 
 MTropolisEngine::~MTropolisEngine() {
-
 }
 
 void MTropolisEngine::handleEvents() {
-
+	Common::Event evt;
+	Common::EventManager *eventMan = _system->getEventManager();
+
+	while (eventMan->pollEvent(evt)) {
+		switch (evt.type) {
+		default:
+			break;
+		}
+	}
 }
 
 Common::Error MTropolisEngine::run() {
+	int preferredWidth = 1024;
+	int preferredHeight = 768;
+	Graphics::PixelFormat preferredPixelFormat = Graphics::createPixelFormat<888>();
 
 	_runtime.reset(new Runtime());
 
 	if (_gameDescription->gameID == GID_OBSIDIAN && _gameDescription->desc.platform == Common::kPlatformWindows) {
+		preferredWidth = 640;
+		preferredHeight = 480;
+		preferredPixelFormat = Graphics::createPixelFormat<555>();
+
 		_runtime->addVolume(0, "Installed", true);
 		_runtime->addVolume(1, "OBSIDIAN1", true);
 		_runtime->addVolume(2, "OBSIDIAN2", true);
@@ -139,6 +189,21 @@ Common::Error MTropolisEngine::run() {
 		desc->addSegment(4, "Obsidian Data 5.MPX");
 		desc->addSegment(5, "Obsidian Data 6.MPX");
 
+		Common::SharedPtr<CursorGraphicCollection> cursors(new CursorGraphicCollection());
+		{
+			// Has to be in this order, some resources from MCURSORS will clobber resources from the player executable
+			const char *cursorFiles[3] = {"Obsidian.exe", "MCURSORS.C95", "Obsidian.c95"};
+
+			for (int i = 0; i < 3; i++) {
+				Common::File f;
+				if (!f.open(cursorFiles[i]) || !loadCursorsFromPE(*cursors, &f)) {
+					error("Failed to load resources");
+				}
+			}
+		}
+
+		desc->setCursorGraphics(cursors);
+
 		Common::SharedPtr<MTropolis::PlugIn> standardPlugIn = PlugIns::createStandard();
 		static_cast<Standard::StandardPlugIn *>(standardPlugIn.get())->getHacks().allowGarbledListModData = true;
 		desc->addPlugIn(standardPlugIn);
@@ -147,6 +212,10 @@ Common::Error MTropolisEngine::run() {
 
 		_runtime->queueProject(desc);
 	} else if (_gameDescription->gameID == GID_OBSIDIAN && _gameDescription->desc.platform == Common::kPlatformMacintosh) {
+		preferredWidth = 640;
+		preferredHeight = 480;
+		preferredPixelFormat = Graphics::createPixelFormat<555>();
+
 		MacObsidianResources *resources = new MacObsidianResources();
 		Common::SharedPtr<ProjectResources> resPtr(resources);
 
@@ -175,8 +244,22 @@ Common::Error MTropolisEngine::run() {
 		_runtime->queueProject(desc);
 	}
 
+	initGraphics(preferredWidth, preferredHeight, &preferredPixelFormat);
+	setDebugger(new Console(this));
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	if (ConfMan.getBool("mtropolis_debug_at_start")) {
+		_runtime->debugSetEnabled(true);
+	}
+	if (ConfMan.getBool("mtropolis_pause_at_start")) {
+		_runtime->debugBreak();
+	}
+#endif
+
 	while (!shouldQuit()) {
+		handleEvents();
 		_runtime->runFrame();
+		_system->updateScreen();
 	}
 
 	_runtime.release();
diff --git a/engines/mtropolis/mtropolis.h b/engines/mtropolis/mtropolis.h
index f38ee178ca3..1085ccd23d7 100644
--- a/engines/mtropolis/mtropolis.h
+++ b/engines/mtropolis/mtropolis.h
@@ -59,7 +59,6 @@ public:
 	Common::Platform getPlatform() const;
 
 public:
-
 	void handleEvents();
 
 protected:
diff --git a/engines/mtropolis/plugin/obsidian.h b/engines/mtropolis/plugin/obsidian.h
index c63dd37f172..dc3e755d5c1 100644
--- a/engines/mtropolis/plugin/obsidian.h
+++ b/engines/mtropolis/plugin/obsidian.h
@@ -35,6 +35,10 @@ class MovementModifier : public Modifier {
 public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Obsidian::MovementModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Movement Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 };
@@ -43,6 +47,10 @@ class RectShiftModifier : public Modifier {
 public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Obsidian::RectShiftModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Rect Shift Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 };
@@ -51,6 +59,10 @@ class TextWorkModifier : public Modifier {
 public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Obsidian::TextWorkModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "TextWork Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 };
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index 4b0c3d8ada0..3183e81e53f 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -37,6 +37,10 @@ class CursorModifier : public Modifier {
 public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::CursorModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Cursor Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -50,6 +54,10 @@ class STransCtModifier : public Modifier {
 public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::STransCtModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Unknown STransCt Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 };
@@ -58,6 +66,10 @@ class MediaCueMessengerModifier : public Modifier {
 public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::MediaCueMessengerModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Media Cue Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -77,6 +89,10 @@ class ObjectReferenceVariableModifier : public VariableModifier {
 public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::ObjectReferenceVariableModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Object Reference Variable Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -88,6 +104,10 @@ class MidiModifier : public Modifier {
 public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::MidiModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "MIDI Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -131,6 +151,10 @@ class ListVariableModifier : public VariableModifier {
 public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::ListVariableModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "List Variable Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
@@ -141,6 +165,10 @@ class SysInfoModifier : public Modifier {
 public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::SysInfoModifier &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "System Info Modifier"; }
+#endif
+
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 };
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 0471e9efec8..0c81139a96b 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -31,6 +31,8 @@
 #include "common/file.h"
 #include "common/substream.h"
 
+#include "graphics/wincursor.h"
+#include "graphics/maccursor.h"
 
 namespace MTropolis {
 
@@ -867,6 +869,29 @@ PlugIn::~PlugIn() {
 ProjectResources::~ProjectResources() {
 }
 
+CursorGraphicCollection::CursorGraphicCollection() {
+}
+
+CursorGraphicCollection::~CursorGraphicCollection() {
+}
+
+void CursorGraphicCollection::addWinCursorGroup(uint32 cursorGroupID, const Common::SharedPtr<Graphics::WinCursorGroup> &cursorGroup) {
+	if (cursorGroup->cursors.size() > 0) {
+		// Not sure what the proper logic should be here, but the second one seems to be the one we usually want
+		if (cursorGroup->cursors.size() > 1)
+			_cursorGraphics[cursorGroupID] = cursorGroup->cursors[1].cursor;
+		else
+			_cursorGraphics[cursorGroupID] = cursorGroup->cursors.back().cursor;
+		_winCursorGroups.push_back(cursorGroup);
+	}
+}
+
+void CursorGraphicCollection::addMacCursor(uint32 cursorID, const Common::SharedPtr<Graphics::MacCursor> &cursor) {
+	_cursorGraphics[cursorID] = cursor.get();
+	_macCursors.push_back(cursor);
+}
+
+
 ProjectDescription::ProjectDescription() {
 }
 
@@ -910,6 +935,9 @@ const Common::SharedPtr<ProjectResources> &ProjectDescription::getResources() co
 	return _resources;
 }
 
+void ProjectDescription::setCursorGraphics(const Common::SharedPtr<CursorGraphicCollection>& cursorGraphics) {
+	_cursorGraphics = cursorGraphics;
+}
 
 const Common::Array<Common::SharedPtr<Modifier> >& SimpleModifierContainer::getModifiers() const {
 	return _modifiers;
@@ -959,6 +987,10 @@ Structural::Structural() : _parent(nullptr) {
 }
 
 Structural::~Structural() {
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	if (_debugInspector)
+		_debugInspector->onDestroyed();
+#endif
 }
 
 ProjectPresentationSettings::ProjectPresentationSettings() : width(640), height(480), bitsPerPixel(8) {
@@ -1024,6 +1056,10 @@ void Structural::materializeSelfAndDescendents(Runtime *runtime, ObjectLinkingSc
 	setRuntimeGUID(runtime->allocateRuntimeGUID());
 
 	materializeDescendents(runtime, outerScope);
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	_debugInspector.reset(this->debugCreateInspector());
+#endif
 }
 
 void Structural::materializeDescendents(Runtime *runtime, ObjectLinkingScope *outerScope) {
@@ -1033,6 +1069,9 @@ void Structural::materializeDescendents(Runtime *runtime, ObjectLinkingScope *ou
 	// - Clones any non-variable aliased modifiers
 	// - Links any static GUIDs to in-scope visible objects
 	// Objects are only ever materialized once
+	//
+	// Modifiers can see other modifiers but can't see sibling elements...
+	// so the structural scope is effectively nested inside of the modifier scope.
 	ObjectLinkingScope tempModifierScope;
 	ObjectLinkingScope tempStructuralScope;
 	ObjectLinkingScope *modifierScope = this->getPersistentModifierScope();
@@ -1082,6 +1121,24 @@ void Structural::materializeDescendents(Runtime *runtime, ObjectLinkingScope *ou
 	}
 }
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+SupportStatus Structural::debugGetSupportStatus() const {
+	return kSupportStatusNone;
+}
+
+const Common::String& Structural::debugGetName() const {
+	return _name;
+}
+
+Common::SharedPtr<DebugInspector> Structural::debugGetInspector() const {
+	return _debugInspector;
+}
+
+DebugInspector *Structural::debugCreateInspector() {
+	return new DebugInspector(this);
+}
+#endif
+
 void Structural::linkInternalReferences(ObjectLinkingScope *scope) {
 }
 
@@ -1151,6 +1208,8 @@ HighLevelSceneTransition::HighLevelSceneTransition(const Common::SharedPtr<Struc
 
 MessageDispatch::MessageDispatch(const Event &evt, Structural *root, bool cascade, bool relay)
 	: _cascade(cascade), _relay(relay), _terminated(false) {
+	_msg.reset(new MessageProperties(evt, DynamicValue(), Common::WeakPtr<RuntimeObject>()));
+
 	PropagationStack topEntry;
 	topEntry.index = 0;
 	topEntry.propagationStage = PropagationStack::kStageSendToStructuralSelf;
@@ -1206,7 +1265,9 @@ VThreadState MessageDispatch::continuePropagating(Runtime *runtime) {
 				}
 
 				// Post to the message action itself to VThread
-				runtime->postConsumeMessageTask(modifier, _msg);
+				if (responds)
+					runtime->postConsumeMessageTask(modifier, _msg);
+
 				return kVThreadReturn;
 			} break;
 		case PropagationStack::kStageSendToModifierContainer: {
@@ -1249,7 +1310,9 @@ VThreadState MessageDispatch::continuePropagating(Runtime *runtime) {
 					_terminated = true;
 				}
 
-				runtime->postConsumeMessageTask(structural, _msg);
+				if (responds)
+					runtime->postConsumeMessageTask(structural, _msg);
+
 				return kVThreadReturn;
 			} break;
 		case PropagationStack::kStageSendToStructuralModifiers: {
@@ -1290,6 +1353,11 @@ Runtime::Runtime() : _nextRuntimeGUID(1) {
 }
 
 void Runtime::runFrame() {
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	if (_debugger && _debugger->isPaused())
+		return;
+#endif
+
 	for (;;) {
 		VThreadState state = _vthread->step();
 		if (state != kVThreadReturn) {
@@ -1368,6 +1436,9 @@ void Runtime::runFrame() {
 			executeHighLevelSceneTransition(transition);
 			continue;
 		}
+
+		// Ran out of actions
+		break;
 	}
 }
 
@@ -1620,10 +1691,20 @@ void Runtime::executeSharedScenePostSceneChangeActions() {
 }
 
 void Runtime::loadScene(const Common::SharedPtr<Structural>& scene) {
+	debug(1, "Loading scene '%s'", scene->getName().c_str());
 	Element *element = static_cast<Element *>(scene.get());
-	uint32 streamIndex = element->getStreamLocator() & 0xffff;	// Not actually sure how many bits are legal here
+	uint32 streamID = element->getStreamLocator() & 0xffff; // Not actually sure how many bits are legal here
+
+	Subsection *subsection = static_cast<Subsection *>(scene->getParent());
+
+	_project->loadSceneFromStream(scene, streamID);
+	debug(1, "Scene loaded OK, materializing objects...");
+	scene->materializeDescendents(this, subsection->getSceneLoadMaterializeScope());
+	debug(1, "Scene materialized OK");
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
 
-	error("TODO: Load scene");
+#endif
 }
 
 void Runtime::sendMessageOnVThread(const Common::SharedPtr<MessageDispatch> &dispatch) {
@@ -1703,6 +1784,21 @@ uint32 Runtime::allocateRuntimeGUID() {
 	return _nextRuntimeGUID++;
 }
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+void Runtime::debugSetEnabled(bool enabled) {
+	if (enabled) {
+		if (!_debugger) 
+			_debugger.reset(new Debugger());
+	} else {
+		_debugger.reset();
+	}
+}
+void Runtime::debugBreak() {
+	debugSetEnabled(true);
+	_debugger->setPaused(true);
+}
+#endif
+
 const Common::Array<Common::SharedPtr<Modifier> > &IModifierContainer::getModifiers() const {
 	return const_cast<IModifierContainer &>(*this).getModifiers();
 };
@@ -1845,6 +1941,74 @@ void Project::loadFromDescription(const ProjectDescription& desc) {
 	debug(1, "Boot stream loaded successfully");
 }
 
+void Project::loadSceneFromStream(const Common::SharedPtr<Structural>& scene, uint32 streamID) {
+	if (streamID == 0 || streamID > _streams.size()) {
+		error("Invalid stream ID");
+	}
+
+	const StreamDesc &streamDesc = _streams[streamID - 1];
+	uint segmentIndex = streamDesc.segmentIndex;
+
+	openSegmentStream(segmentIndex);
+
+	Common::SeekableSubReadStreamEndian stream(_segments[segmentIndex].weakStream, streamDesc.pos, streamDesc.pos + streamDesc.size, _isBigEndian);
+	Data::DataReader reader(stream, _projectFormat);
+
+	const Data::PlugInModifierRegistry &plugInDataLoaderRegistry = _plugInRegistry.getDataLoaderRegistry();
+
+	{
+		Common::SharedPtr<Data::DataObject> dataObject;
+		Data::loadDataObject(plugInDataLoaderRegistry, reader, dataObject);
+
+		if (dataObject == nullptr || dataObject->getType() != Data::DataObjectTypes::kStreamHeader) {
+			error("Scene stream header was missing");
+		}
+	}
+
+	ChildLoaderStack loaderStack;
+	AssetDefLoaderContext assetDefLoader;
+
+	{
+		ChildLoaderContext loaderContext;
+		loaderContext.containerUnion.filteredElements.filterFunc = Data::DataObjectTypes::isElement;
+		loaderContext.containerUnion.filteredElements.structural = scene.get();
+		loaderContext.remainingCount = 0;
+		loaderContext.type = ChildLoaderContext::kTypeFilteredElements;
+
+		loaderStack.contexts.push_back(loaderContext);
+	}
+
+	size_t numObjectsLoaded = 0;
+	while (stream.pos() != streamDesc.size) {
+		Common::SharedPtr<Data::DataObject> dataObject;
+		Data::loadDataObject(plugInDataLoaderRegistry, reader, dataObject);
+
+		if (!dataObject) {
+			error("Failed to load stream");
+		}
+
+		Data::DataObjectTypes::DataObjectType dataObjectType = dataObject->getType();
+
+		if (Data::DataObjectTypes::isAsset(dataObjectType)) {
+			// Asset defs can appear anywhere
+			loadAssetDef(assetDefLoader, *dataObject.get());
+		} else if (dataObjectType == Data::DataObjectTypes::kAssetDataChunk) {
+			// Ignore
+			continue;
+		} else if (loaderStack.contexts.size() > 0) {
+			loadContextualObject(loaderStack, *dataObject.get());
+		} else {
+			error("Unexpectedly exited scene context in loader");
+		}
+
+		numObjectsLoaded++;
+	}
+
+	if (loaderStack.contexts.size() != 1 || loaderStack.contexts[0].type != ChildLoaderContext::kTypeFilteredElements) {
+		error("Scene stream loader finished in an expected state, something didn't finish loading");
+	}
+}
+
 Common::SharedPtr<Modifier> Project::resolveAlias(uint32 aliasID) const {
 	if (aliasID == 0 || aliasID > _globalModifiers.getModifiers().size())
 		return Common::SharedPtr<Modifier>();
@@ -2282,6 +2446,10 @@ bool Subsection::load(const Data::SubsectionStructuralDef &data) {
 	return true;
 }
 
+ObjectLinkingScope *Subsection::getSceneLoadMaterializeScope() {
+	return getPersistentStructuralScope();
+}
+
 ObjectLinkingScope *Subsection::getPersistentStructuralScope() {
 	return &_structuralScope;
 }
@@ -2328,6 +2496,10 @@ Modifier::Modifier() {
 }
 
 Modifier::~Modifier() {
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	if (_debugInspector)
+		_debugInspector->onDestroyed();
+#endif
 }
 
 void Modifier::materialize(Runtime *runtime, ObjectLinkingScope *outerScope) {
@@ -2336,6 +2508,10 @@ void Modifier::materialize(Runtime *runtime, ObjectLinkingScope *outerScope) {
 
 	linkInternalReferences();
 	setRuntimeGUID(runtime->allocateRuntimeGUID());
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	_debugInspector.reset(this->debugCreateInspector());
+#endif
 }
 
 bool Modifier::isAlias() const {
@@ -2371,6 +2547,25 @@ const Common::String& Modifier::getName() const {
 void Modifier::visitInternalReferences(IStructuralReferenceVisitor *visitor) {
 }
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+SupportStatus Modifier::debugGetSupportStatus() const {
+	return kSupportStatusNone;
+}
+
+const Common::String &Modifier::debugGetName() const {
+	return _name;
+}
+
+Common::SharedPtr<DebugInspector> Modifier::debugGetInspector() const {
+	return _debugInspector;
+}
+
+DebugInspector *Modifier::debugCreateInspector() {
+	return new DebugInspector(this);
+
+}
+#endif
+
 bool VariableModifier::isVariable() const {
 	return true;
 }
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index e37a8071711..24be3f44cf9 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -29,12 +29,25 @@
 #include "common/hashmap.h"
 #include "common/hash-str.h"
 
+#include "graphics/pixelformat.h"
+
 #include "mtropolis/data.h"
+#include "mtropolis/debug.h"
 #include "mtropolis/vthread.h"
 
+namespace Graphics {
+
+struct WinCursorGroup;
+class MacCursor;
+class Cursor;
+
+} // End of namespace Graphics
+
 namespace MTropolis {
 
 class Asset;
+class CursorGraphic;
+class CursorGraphicCollection;
 class Element;
 class MessageDispatch;
 struct MessageProperties;
@@ -53,7 +66,6 @@ struct IPlugInModifierFactoryAndDataFactory;
 struct ModifierLoaderContext;
 
 
-
 namespace DynamicValueTypes {
 
 enum DynamicValueType {
@@ -564,8 +576,23 @@ struct ProjectResources {
 	virtual ~ProjectResources();
 };
 
-class ProjectDescription {
 
+class CursorGraphicCollection {
+public:
+	CursorGraphicCollection();
+	~CursorGraphicCollection();
+
+	void addWinCursorGroup(uint32 cursorGroupID, const Common::SharedPtr<Graphics::WinCursorGroup> &cursorGroup);
+	void addMacCursor(uint32 cursorID, const Common::SharedPtr<Graphics::MacCursor> &cursor);
+
+private:
+	Common::HashMap<uint32, Graphics::Cursor *> _cursorGraphics;
+
+	Common::Array<Common::SharedPtr<Graphics::WinCursorGroup> > _winCursorGroups;
+	Common::Array<Common::SharedPtr<Graphics::MacCursor> > _macCursors;
+};
+
+class ProjectDescription {
 public:
 	ProjectDescription(); 
 	~ProjectDescription();
@@ -580,10 +607,13 @@ public:
 	void setResources(const Common::SharedPtr<ProjectResources> &resources);
 	const Common::SharedPtr<ProjectResources> &getResources() const;
 
+	void setCursorGraphics(const Common::SharedPtr<CursorGraphicCollection> &cursorGraphics);
+
 private:
 	Common::Array<SegmentDescription> _segments;
 	Common::Array<Common::SharedPtr<PlugIn> > _plugIns;
 	Common::SharedPtr<ProjectResources> _resources;
+	Common::SharedPtr<CursorGraphicCollection> _cursorGraphics;
 };
 
 struct VolumeState {
@@ -697,6 +727,11 @@ public:
 
 	uint32 allocateRuntimeGUID();
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	void debugSetEnabled(bool enabled);
+	void debugBreak();
+#endif
+
 private:
 	struct SceneStackEntry {
 		SceneStackEntry();
@@ -756,6 +791,9 @@ private:
 	Common::SharedPtr<Structural> _activeSharedScene;
 	Common::Array<SceneReturnListEntry> _sceneReturnList;
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	Common::SharedPtr<Debugger> _debugger;
+#endif
 
 	uint32 _nextRuntimeGUID;
 };
@@ -818,7 +856,7 @@ struct IMessageConsumer {
 	virtual VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) const = 0;
 };
 
-class Structural : public RuntimeObject, public IModifierContainer, public IMessageConsumer {
+class Structural : public RuntimeObject, public IModifierContainer, public IMessageConsumer, public IDebuggable {
 public:
 	Structural();
 	virtual ~Structural();
@@ -843,6 +881,14 @@ public:
 	void materializeSelfAndDescendents(Runtime *runtime, ObjectLinkingScope *outerScope);
 	void materializeDescendents(Runtime *runtime, ObjectLinkingScope *outerScope);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	virtual SupportStatus debugGetSupportStatus() const override;
+	virtual const Common::String &debugGetName() const override;
+	virtual Common::SharedPtr<DebugInspector> debugGetInspector() const override;
+
+	virtual DebugInspector *debugCreateInspector();
+#endif
+
 protected:
 	virtual ObjectLinkingScope *getPersistentStructuralScope();
 	virtual ObjectLinkingScope *getPersistentModifierScope();
@@ -853,6 +899,10 @@ protected:
 	Common::Array<Common::SharedPtr<Structural> > _children;
 	Common::Array<Common::SharedPtr<Modifier> > _modifiers;
 	Common::String _name;
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	Common::SharedPtr<DebugInspector> _debugInspector;
+#endif
 };
 
 struct ProjectPresentationSettings {
@@ -931,9 +981,15 @@ public:
 	~Project();
 
 	void loadFromDescription(const ProjectDescription &desc);
+	void loadSceneFromStream(const Common::SharedPtr<Structural> &structural, uint32 streamID);
+
 	Common::SharedPtr<Modifier> resolveAlias(uint32 aliasID) const;
 	void materializeGlobalVariables(Runtime *runtime, ObjectLinkingScope *scope);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Project"; }
+#endif
+
 private:
 	struct LabelSuperGroup {
 		size_t firstRootNodeIndex;
@@ -1027,6 +1083,10 @@ class Section : public Structural {
 public:
 	bool load(const Data::SectionStructuralDef &data);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Section"; }
+#endif
+
 private:
 	ObjectLinkingScope *getPersistentStructuralScope() override;
 	ObjectLinkingScope *getPersistentModifierScope() override;
@@ -1039,6 +1099,12 @@ class Subsection : public Structural {
 public:
 	bool load(const Data::SubsectionStructuralDef &data);
 
+	ObjectLinkingScope *getSceneLoadMaterializeScope();
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Subsection"; }
+#endif
+
 private:
 	ObjectLinkingScope *getPersistentStructuralScope() override;
 	ObjectLinkingScope *getPersistentModifierScope() override;
@@ -1084,7 +1150,7 @@ struct ModifierFlags {
 	bool isLastModifier : 1;
 };
 
-class Modifier : public RuntimeObject, public IMessageConsumer {
+class Modifier : public RuntimeObject, public IMessageConsumer, public IDebuggable {
 public:
 	Modifier();
 	virtual ~Modifier();
@@ -1108,12 +1174,24 @@ public:
 
 	virtual void visitInternalReferences(IStructuralReferenceVisitor *visitor);
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	virtual SupportStatus debugGetSupportStatus() const override;
+	virtual const Common::String &debugGetName() const override;
+	virtual Common::SharedPtr<DebugInspector> debugGetInspector() const override;
+
+	virtual DebugInspector *debugCreateInspector();
+#endif
+
 protected:
 	bool loadTypicalHeader(const Data::TypicalModifierHeader &typicalHeader);
 	virtual void linkInternalReferences();
 
 	Common::String _name;
 	ModifierFlags _modifierFlags;
+	
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	Common::SharedPtr<DebugInspector> _debugInspector;
+#endif
 };
 
 class VariableModifier : public Modifier {
@@ -1130,6 +1208,7 @@ protected:
 	uint32 _assetID;
 };
 
+
 } // End of namespace MTropolis
 
 #endif
diff --git a/engines/mtropolis/vthread.h b/engines/mtropolis/vthread.h
index 08598c30b09..f571ed30b3d 100644
--- a/engines/mtropolis/vthread.h
+++ b/engines/mtropolis/vthread.h
@@ -24,15 +24,14 @@
 
 #include <new>
 #include "mtropolis/alignment_helper.h"
+#include "mtropolis/debug.h"
 
 namespace MTropolis {
 
-#define MTROPOLIS_DEBUG_VTHREAD_STACKS
-
 // Virtual thread, really a task stack
 enum VThreadState {
 	kVThreadReturn,
-	kVThreadSuspend,
+	kVThreadSuspended,
 	kVThreadError,
 };
 


Commit: 7d055f332b95eb252678acab35bb2c4c09c7c468
    https://github.com/scummvm/scummvm/commit/7d055f332b95eb252678acab35bb2c4c09c7c468
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix up Mac resource loading, add debug stub things

Changed paths:
  A engines/mtropolis/render.cpp
  A engines/mtropolis/render.h
    engines/mtropolis/debug.cpp
    engines/mtropolis/debug.h
    engines/mtropolis/module.mk
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/mtropolis.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/debug.cpp b/engines/mtropolis/debug.cpp
index cb2afc3f35b..7f297623d38 100644
--- a/engines/mtropolis/debug.cpp
+++ b/engines/mtropolis/debug.cpp
@@ -35,7 +35,7 @@ void DebugInspector::onDestroyed() {
 	_debuggable = nullptr;
 }
 
-Debugger::Debugger() : _paused(false) {
+Debugger::Debugger(Runtime *runtime) : _paused(false), _runtime(runtime) {
 }
 
 Debugger::~Debugger() {
@@ -49,4 +49,20 @@ bool Debugger::isPaused() const {
 	return _paused;
 }
 
+void Debugger::notify(DebugSeverity severity, const Common::String& str) {
+	// TODO
+}
+
+void Debugger::notifyFmt(DebugSeverity severity, const char *fmt, ...) {
+	va_list args;
+	va_start(args, fmt);
+	this->vnotifyFmt(severity, fmt, args);
+	va_end(args);
+}
+
+void Debugger::vnotifyFmt(DebugSeverity severity, const char* fmt, va_list args) {
+	Common::String str(Common::String::vformat(fmt, args));
+	this->notify(severity, fmt);
+}
+
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/debug.h b/engines/mtropolis/debug.h
index 8852236e589..41ef650d911 100644
--- a/engines/mtropolis/debug.h
+++ b/engines/mtropolis/debug.h
@@ -25,12 +25,17 @@
 #include "common/str.h"
 #include "common/ptr.h"
 
+#include <cstdarg>
+
 #define MTROPOLIS_DEBUG_VTHREAD_STACKS
 #define MTROPOLIS_DEBUG_ENABLE
 
 namespace MTropolis {
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
+
+class Runtime;
+
 struct IDebuggable;
 
 enum SupportStatus {
@@ -50,19 +55,46 @@ private:
 	IDebuggable *_debuggable;
 };
 
+enum DebugSeverity {
+	kDebugSeverityInfo,
+	kDebugSeverityWarning,
+	kDebugSeverityError,
+};
+
+class Notification {
+public:
+
+private:
+	Runtime *_runtime;
+};
+
 class Debugger {
 public:
-	Debugger();
+	explicit Debugger(Runtime *runtime);
 	~Debugger();
 
 	void setPaused(bool paused);
 	bool isPaused() const;
+	void notify(DebugSeverity severity, const Common::String &str);
+	void notifyFmt(DebugSeverity severity, const char *fmt, ...);
+	void vnotifyFmt(DebugSeverity severity, const char *fmt, va_list args);
 
 private:
+	Debugger();
+
 	bool _paused;
+	Runtime *_runtime;
 };
 
-#endif
+#define MTROPOLIS_DEBUG_NOTIFY(...) \
+		(static_cast<const IDebuggable *>(this)->debugGetDebugger()->notifyFmt(__VA_ARGS__));
+
+
+#else /* MTROPOLIS_DEBUG_ENABLE */
+
+#define MTROPOLIS_DEBUG_NOTIFY(...) ((void)0)
+
+#endif /* !MTROPOLIS_DEBUG_ENABLE */
 
 struct IDebuggable {
 #ifdef MTROPOLIS_DEBUG_ENABLE
@@ -70,12 +102,10 @@ struct IDebuggable {
 	virtual const char *debugGetTypeName() const = 0;
 	virtual const Common::String &debugGetName() const = 0;
 	virtual Common::SharedPtr<DebugInspector> debugGetInspector() const = 0;
+	virtual Debugger *debugGetDebugger() const = 0;
 #endif
 };
 
 } // End of namespace MTropolis
 
 #endif /* MTROPOLIS_DEBUG_H */
-
-
-
diff --git a/engines/mtropolis/module.mk b/engines/mtropolis/module.mk
index 3f2162ae601..e140df188eb 100644
--- a/engines/mtropolis/module.mk
+++ b/engines/mtropolis/module.mk
@@ -18,6 +18,7 @@ MODULE_OBJS = \
 	plugin/obsidian_data.o \
 	plugin/standard.o \
 	plugin/standard_data.o \
+	render.o \
 	runtime.o \
 	vthread.o
 
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index 0f8a352a538..346455e1035 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -39,8 +39,10 @@
 
 #include "engines/util.h"
 
-#include "graphics/pixelformat.h"
 #include "graphics/cursorman.h"
+#include "graphics/maccursor.h"
+#include "graphics/surface.h"
+#include "graphics/pixelformat.h"
 #include "graphics/wincursor.h"
 
 namespace MTropolis {
@@ -69,6 +71,55 @@ static bool loadCursorsFromPE(CursorGraphicCollection &cursorGraphics, Common::S
 
 		cursorGraphics.addWinCursorGroup(id.getID(), cursorGroup);
 	}
+
+	return true;
+}
+
+static bool loadCursorsFromMacResources(CursorGraphicCollection &cursorGraphics, Common::MacResManager &resMan) {
+	const uint32 bwType = MKTAG('C', 'U', 'R', 'S');
+	const uint32 colorType = MKTAG('c', 'r', 's', 'r');
+
+	Common::MacResIDArray bwIDs = resMan.getResIDArray(bwType);
+	Common::MacResIDArray colorIDs = resMan.getResIDArray(colorType);
+
+	Common::MacResIDArray bwOnlyIDs;
+	for (Common::MacResIDArray::const_iterator bwIt = bwIDs.begin(), bwItEnd = bwIDs.end(); bwIt != bwItEnd; ++bwIt) {
+		bool hasColor = false;
+		for (Common::MacResIDArray::const_iterator colorIt = colorIDs.begin(), colorItEnd = colorIDs.end(); colorIt != colorItEnd; ++colorIt) {
+			if ((*colorIt) == (*bwIt)) {
+				hasColor = true;
+				break;
+			}
+		}
+
+		if (!hasColor)
+			bwOnlyIDs.push_back(*bwIt);
+	}
+
+	for (int cti = 0; cti < 2; cti++) {
+		const uint32 resType = (cti == 0) ? bwType : colorType;
+		const bool isBW = (cti == 0);
+		const Common::MacResIDArray &resArray = (cti == 0) ? bwOnlyIDs : colorIDs;
+
+		for (size_t i = 0; i < resArray.size(); i++) {
+			Common::SeekableReadStream *resData = resMan.getResource(resType, resArray[i]);
+			if (!resData) {
+				warning("Failed to open cursor resource");
+				return false;
+			}
+
+			Common::SharedPtr<Graphics::MacCursor> cursor(new Graphics::MacCursor());
+			// Some CURS resources are 72 bytes instead of the expected 68, make sure they load as the correct format
+			if (!cursor->readFromStream(*resData, isBW, 0xff, isBW)) {
+				warning("Failed to load cursor resource");
+				return false;
+			}
+
+			cursorGraphics.addMacCursor(resArray[i], cursor);
+		}
+	}
+
+	return true;
 }
 
 struct MacObsidianResources : public ProjectResources {
@@ -77,6 +128,7 @@ struct MacObsidianResources : public ProjectResources {
 
 	void setup();
 	Common::SeekableReadStream *getSegmentStream(int index) const;
+	const Common::SharedPtr<CursorGraphicCollection> &getCursorGraphics() const;
 
 private:
 	Common::MacResManager _installerResMan;
@@ -85,11 +137,15 @@ private:
 	Common::SeekableReadStream *_installerDataForkStream;
 	Common::Archive *_installerArchive;
 	Common::SeekableReadStream *_segmentStreams[6];
+
+	Common::SharedPtr<CursorGraphicCollection> _cursorGraphics;
 };
 
 MacObsidianResources::MacObsidianResources() : _installerArchive(nullptr), _installerDataForkStream(nullptr) {
 	for (int i = 0; i < 6; i++)
 		_segmentStreams[i] = nullptr;
+
+	_cursorGraphics.reset(new CursorGraphicCollection());
 }
 
 void MacObsidianResources::setup() {
@@ -110,7 +166,7 @@ void MacObsidianResources::setup() {
 	debug(1, "Reading data from installer...");
 	_segmentStreams[0] = _installerArchive->createReadStreamForMember("Obsidian Data 1");
 
-	debug("Opening data segments...");
+	debug(1, "Opening data segments...");
 	for (int i = 0; i < 5; i++) {
 		char fileName[32];
 		sprintf(fileName, "Obsidian Data %i", (i + 2));
@@ -124,12 +180,32 @@ void MacObsidianResources::setup() {
 
 		_segmentStreams[1 + i] = resMan.getDataFork();
 	}
+
+	debug(1, "Opening resources...");
+
+	const char *cursorSources[4] = {"Obsidian", "Basic.rPP", "mCursors.cPP", "Obsidian.cPP"};
+	for (int i = 0; i < 4; i++) {
+		const char *fileName = cursorSources[i];
+
+		Common::MacResManager resMan;
+		if (!resMan.open(Common::Path(fileName), *_installerArchive))
+			error("Failed to open resources in file '%s'", fileName);
+
+		if (!loadCursorsFromMacResources(*_cursorGraphics, resMan))
+			error("Failed to read cursor resources from file '%s'", fileName);
+	}
+
+	debug(1, "Finished unpacking installer resources");
 }
 
 Common::SeekableReadStream *MacObsidianResources::getSegmentStream(int index) const {
 	return _segmentStreams[index];
 }
 
+const Common::SharedPtr<CursorGraphicCollection>& MacObsidianResources::getCursorGraphics() const {
+	return _cursorGraphics;
+}
+
 MacObsidianResources::~MacObsidianResources() {
 	for (int i = 0; i < 6; i++)
 		delete _segmentStreams[i];
@@ -165,14 +241,14 @@ void MTropolisEngine::handleEvents() {
 Common::Error MTropolisEngine::run() {
 	int preferredWidth = 1024;
 	int preferredHeight = 768;
-	Graphics::PixelFormat preferredPixelFormat = Graphics::createPixelFormat<888>();
+	ColorDepthMode preferredColorDepthMode = kColorDepthMode8Bit;
 
 	_runtime.reset(new Runtime());
 
 	if (_gameDescription->gameID == GID_OBSIDIAN && _gameDescription->desc.platform == Common::kPlatformWindows) {
 		preferredWidth = 640;
 		preferredHeight = 480;
-		preferredPixelFormat = Graphics::createPixelFormat<555>();
+		preferredColorDepthMode = kColorDepthMode16Bit;
 
 		_runtime->addVolume(0, "Installed", true);
 		_runtime->addVolume(1, "OBSIDIAN1", true);
@@ -214,7 +290,7 @@ Common::Error MTropolisEngine::run() {
 	} else if (_gameDescription->gameID == GID_OBSIDIAN && _gameDescription->desc.platform == Common::kPlatformMacintosh) {
 		preferredWidth = 640;
 		preferredHeight = 480;
-		preferredPixelFormat = Graphics::createPixelFormat<555>();
+		preferredColorDepthMode = kColorDepthMode16Bit;
 
 		MacObsidianResources *resources = new MacObsidianResources();
 		Common::SharedPtr<ProjectResources> resPtr(resources);
@@ -240,11 +316,82 @@ Common::Error MTropolisEngine::run() {
 		desc->addPlugIn(PlugIns::createObsidian());
 
 		desc->setResources(resPtr);
+		desc->setCursorGraphics(resources->getCursorGraphics());
 
 		_runtime->queueProject(desc);
 	}
 
-	initGraphics(preferredWidth, preferredHeight, &preferredPixelFormat);
+	// Figure out pixel formats
+	Graphics::PixelFormat modePixelFormats[kColorDepthModeCount];
+	bool haveExactMode[kColorDepthModeCount];
+	bool haveCloseMode[kColorDepthModeCount];
+
+	for (int i = 0; i < kColorDepthModeCount; i++) {
+		haveExactMode[i] = false;
+		haveCloseMode[i] = false;
+	}
+
+	{
+		Common::List<Graphics::PixelFormat> pixelFormats = _system->getSupportedFormats();
+
+		Graphics::PixelFormat clut8Format = Graphics::PixelFormat::createFormatCLUT8();
+
+		for (Common::List<Graphics::PixelFormat>::const_iterator it = pixelFormats.begin(), itEnd = pixelFormats.end(); it != itEnd; ++it) {
+			const Graphics::PixelFormat &candidateFormat = *it;
+			ColorDepthMode thisFormatMode = kColorDepthModeInvalid;
+			bool isExactMatch = false;
+			if (candidateFormat.rBits() == 8 && candidateFormat.gBits() == 8 && candidateFormat.bBits() == 8) {
+				isExactMatch = (candidateFormat.aBits() == 8);
+				thisFormatMode = kColorDepthMode32Bit;
+			} else if (candidateFormat.rBits() == 5 && candidateFormat.bBits() == 5 && candidateFormat.bytesPerPixel == 2) {
+				if (candidateFormat.gBits() == 5) {
+					isExactMatch = true;
+					thisFormatMode = kColorDepthMode16Bit;
+				} else if (candidateFormat.gBits() == 6) {
+					isExactMatch = false;
+					thisFormatMode = kColorDepthMode16Bit;
+				}
+			} else if (candidateFormat == clut8Format) {
+				isExactMatch = true;
+				thisFormatMode = kColorDepthMode8Bit;
+			}
+
+			if (thisFormatMode != kColorDepthModeInvalid && !haveExactMode[thisFormatMode]) {
+				if (isExactMatch) {
+					haveExactMode[thisFormatMode] = true;
+					haveCloseMode[thisFormatMode] = true;
+					modePixelFormats[thisFormatMode] = candidateFormat;
+				} else if (!haveCloseMode[thisFormatMode]) {
+					haveCloseMode[thisFormatMode] = true;
+					modePixelFormats[thisFormatMode] = candidateFormat;
+				}
+			}
+		}
+	}
+
+	// Figure out a pixel format.  First try to find one that's at least as good or better.
+	ColorDepthMode selectedMode = kColorDepthModeInvalid;
+	for (int i = preferredColorDepthMode; i < kColorDepthModeCount; i++) {
+		if (haveExactMode[i] || haveCloseMode[i]) {
+			selectedMode = static_cast<ColorDepthMode>(i);
+			break;
+		}
+	}
+
+	// If that fails, then try to find the best one available
+	if (selectedMode == kColorDepthModeInvalid) {
+		for (int i = preferredColorDepthMode - 1; i >= 0; i++) {
+			if (haveExactMode[i] || haveCloseMode[i]) {
+				selectedMode = static_cast<ColorDepthMode>(i);
+				break;
+			}
+		}
+	}
+
+	if (selectedMode == kColorDepthModeInvalid)
+		error("Couldn't resolve a color depth mode");
+
+	initGraphics(preferredWidth, preferredHeight, &modePixelFormats[selectedMode]);
 	setDebugger(new Console(this));
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
@@ -259,7 +406,7 @@ Common::Error MTropolisEngine::run() {
 	while (!shouldQuit()) {
 		handleEvents();
 		_runtime->runFrame();
-		_system->updateScreen();
+		_runtime->drawFrame(_system);
 	}
 
 	_runtime.release();
diff --git a/engines/mtropolis/mtropolis.h b/engines/mtropolis/mtropolis.h
index 1085ccd23d7..c0d921723cc 100644
--- a/engines/mtropolis/mtropolis.h
+++ b/engines/mtropolis/mtropolis.h
@@ -38,11 +38,23 @@
  */
 namespace MTropolis {
 
+enum ColorDepthMode {
+	kColorDepthMode1Bit,
+	kColorDepthMode2Bit,
+	kColorDepthMode4Bit,
+	kColorDepthMode8Bit,
+	kColorDepthMode16Bit,
+	kColorDepthMode32Bit,
+
+	kColorDepthModeCount,
+
+	kColorDepthModeInvalid,
+};
+
 class Runtime;
 
 class MTropolisEngine : public ::Engine {
 protected:
-
 	// Engine APIs
 	Common::Error run() override;
 
diff --git a/engines/mtropolis/render.cpp b/engines/mtropolis/render.cpp
new file mode 100644
index 00000000000..a6d6df8eca8
--- /dev/null
+++ b/engines/mtropolis/render.cpp
@@ -0,0 +1,89 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mtropolis/render.h"
+#include "mtropolis/runtime.h"
+#include "graphics/surface.h"
+
+namespace MTropolis {
+
+template<class TNumber, int TResolution>
+struct OrderedDitherGenerator {
+	static void generateOrderedDither(TNumber (&pattern)[TResolution][TResolution]);
+};
+
+template<class TNumber>
+struct OrderedDitherGenerator<TNumber, 1> {
+	static void generateOrderedDither(TNumber (&pattern)[1][1]);
+};
+
+template<class TNumber, int TResolution>
+void OrderedDitherGenerator<TNumber, TResolution>::generateOrderedDither(TNumber (&pattern)[TResolution][TResolution]) {
+	const int kHalfResolution = TResolution / 2;
+	byte halfRes[kHalfResolution][kHalfResolution];
+
+	OrderedDitherGenerator<kHalfResolution>::generateOrderedDither(halfRes);
+
+	const int kHalfResNumSteps = kHalfResolution * kHalfResolution;
+	for (int y = 0; y < kHalfResolution; y++) {
+		for (int x = 0; x < kHalfResolution; x++) {
+			pattern[y * 2][x * 2] = halfRes[y][x];
+			pattern[y * 2 + 1][x * 2 + 1] = halfRes[y][x] + kHalfResNumSteps * 1;
+			pattern[y * 2][x * 2 + 1] = halfRes[y][x] + kHalfResNumSteps * 2;
+			pattern[y * 2 + 1][x * 2] = halfRes[y][x] + kHalfResNumSteps * 3;
+		}
+	}
+}
+
+template<class TNumber>
+void OrderedDitherGenerator<TNumber, 1>::generateOrderedDither(TNumber (&pattern)[1][1]) {
+	pattern[0][0] = 0;
+}
+
+inline int quantize8To5(int value, byte orderedDither16x16) {
+	return (value * 249 + (orderedDither16x16 << 3)) >> 11;
+}
+
+Window::Window(int32 x, int32 y, int16 width, int16 height, const Graphics::PixelFormat &format) : _x(x), _y(y), _surface(nullptr) {
+	_surface = new Graphics::Surface();
+	_surface->create(width, height, format);
+}
+
+Window::~Window() {
+	if (_surface) {
+		_surface->free();
+		delete _surface;
+	}
+}
+
+int32 Window::getX() const {
+	return _x;
+}
+
+int32 Window::getY() const {
+	return _y;
+}
+
+Graphics::Surface &Window::getSurface() const {
+	return *_surface;
+}
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/render.h b/engines/mtropolis/render.h
new file mode 100644
index 00000000000..89a6bdcd39b
--- /dev/null
+++ b/engines/mtropolis/render.h
@@ -0,0 +1,53 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_RENDER_H
+#define MTROPOLIS_RENDER_H
+
+#include "common/scummsys.h"
+#include "graphics/pixelformat.h"
+
+namespace Graphics {
+
+struct Surface;
+
+} // End of namespace Graphics
+
+namespace MTropolis {
+
+class Window {
+public:
+	Window(int32 x, int32 y, int16 width, int16 height, const Graphics::PixelFormat &format);
+	~Window();
+
+	int32 getX() const;
+	int32 getY() const;
+	Graphics::Surface &getSurface() const;
+
+private:
+	int32 _x;
+	int32 _y;
+	Graphics::Surface *_surface;
+};
+
+} // End of namespace MTropolis
+
+#endif
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 0c81139a96b..ab50b0c9008 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -26,11 +26,14 @@
 #include "mtropolis/element_factory.h"
 #include "mtropolis/modifier_factory.h"
 #include "mtropolis/modifiers.h"
+#include "mtropolis/render.h"
 
 #include "common/debug.h"
 #include "common/file.h"
 #include "common/substream.h"
+#include "common/system.h"
 
+#include "graphics/surface.h"
 #include "graphics/wincursor.h"
 #include "graphics/maccursor.h"
 
@@ -984,6 +987,9 @@ const Common::WeakPtr<RuntimeObject>& MessageProperties::getSource() const {
 }
 
 Structural::Structural() : _parent(nullptr) {
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	_debugger = nullptr;
+#endif
 }
 
 Structural::~Structural() {
@@ -1134,10 +1140,14 @@ Common::SharedPtr<DebugInspector> Structural::debugGetInspector() const {
 	return _debugInspector;
 }
 
+Debugger* Structural::debugGetDebugger() const {
+	return _debugger;
+}
+
 DebugInspector *Structural::debugCreateInspector() {
 	return new DebugInspector(this);
 }
-#endif
+#endif /* MTROPOLIS_DEBUG_ENABLE */
 
 void Structural::linkInternalReferences(ObjectLinkingScope *scope) {
 }
@@ -1348,6 +1358,7 @@ VThreadState MessageDispatch::continuePropagating(Runtime *runtime) {
 Runtime::SceneStackEntry::SceneStackEntry() {
 }
 
+
 Runtime::Runtime() : _nextRuntimeGUID(1) {
 	_vthread.reset(new VThread());
 }
@@ -1442,6 +1453,58 @@ void Runtime::runFrame() {
 	}
 }
 
+void Runtime::drawFrame(OSystem* system) {
+	int width = system->getWidth();
+	int height = system->getHeight();
+
+	for (Common::Array<Common::SharedPtr<Window> >::const_iterator it = _windows.begin(), itEnd = _windows.end(); it != itEnd; ++it) {
+		const Window &window = *it->get();
+		const Graphics::Surface &surface = window.getSurface();
+
+		int32 destLeft = window.getX();
+		int32 destRight = destLeft + surface.w;
+		int32 destTop = window.getY();
+		int32 destBottom = destTop + surface.h;
+
+		int32 srcLeft = 0;
+		int32 srcRight = surface.w;
+		int32 srcTop = 0;
+		int32 srcBottom = surface.h;
+
+		// Clip to drawable area
+		if (destLeft < 0) {
+			int leftAdjust = -destLeft;
+			destLeft += leftAdjust;
+			srcLeft += leftAdjust;
+		}
+
+		if (destTop < 0) {
+			int topAdjust = -destTop;
+			destTop += topAdjust;
+			srcTop += topAdjust;
+		}
+
+		if (destRight > width) {
+			int rightAdjust = width - destRight;
+			destRight += rightAdjust;
+			srcRight += rightAdjust;
+		}
+
+		if (destBottom > height) {
+			int bottomAdjust = height - destBottom;
+			destBottom += bottomAdjust;
+			srcBottom += bottomAdjust;
+		}
+
+		if (destLeft >= destRight || destTop >= destBottom || destLeft >= width || destTop >= height || destRight <= 0 || destBottom <= 0)
+			continue;
+
+		system->copyRectToScreen(surface.getBasePtr(destLeft, destTop), surface.pitch, destLeft, destTop, destRight - destLeft, destBottom - destTop);
+	}
+
+	system->updateScreen();
+}
+
 Common::SharedPtr<Structural> Runtime::findDefaultSharedSceneForScene(Structural *scene) {
 	Structural *subsection = scene->getParent();
 	const Common::Array<Common::SharedPtr<Structural> > &children = subsection->getChildren();
@@ -1784,11 +1847,16 @@ uint32 Runtime::allocateRuntimeGUID() {
 	return _nextRuntimeGUID++;
 }
 
+void Runtime::addWindow(const Common::SharedPtr<Window> &window) {
+	_windows.push_back(window);
+}
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
+
 void Runtime::debugSetEnabled(bool enabled) {
 	if (enabled) {
 		if (!_debugger) 
-			_debugger.reset(new Debugger());
+			_debugger.reset(new Debugger(this));
 	} else {
 		_debugger.reset();
 	}
@@ -1797,7 +1865,12 @@ void Runtime::debugBreak() {
 	debugSetEnabled(true);
 	_debugger->setPaused(true);
 }
-#endif
+
+Debugger* Runtime::debugGetDebugger() const {
+	return _debugger.get();
+}
+
+#endif /* MTROPOLIS_DEBUG_ENABLE */
 
 const Common::Array<Common::SharedPtr<Modifier> > &IModifierContainer::getModifiers() const {
 	return const_cast<IModifierContainer &>(*this).getModifiers();
@@ -2493,6 +2566,9 @@ bool ModifierFlags::load(const uint32 dataModifierFlags) {
 }
 
 Modifier::Modifier() {
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	_debugger = nullptr;
+#endif
 }
 
 Modifier::~Modifier() {
@@ -2510,6 +2586,7 @@ void Modifier::materialize(Runtime *runtime, ObjectLinkingScope *outerScope) {
 	setRuntimeGUID(runtime->allocateRuntimeGUID());
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
+	_debugger = runtime->debugGetDebugger();
 	_debugInspector.reset(this->debugCreateInspector());
 #endif
 }
@@ -2548,6 +2625,7 @@ void Modifier::visitInternalReferences(IStructuralReferenceVisitor *visitor) {
 }
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
+
 SupportStatus Modifier::debugGetSupportStatus() const {
 	return kSupportStatusNone;
 }
@@ -2560,11 +2638,15 @@ Common::SharedPtr<DebugInspector> Modifier::debugGetInspector() const {
 	return _debugInspector;
 }
 
+Debugger *Modifier::debugGetDebugger() const {
+	return _debugger;
+}
+
 DebugInspector *Modifier::debugCreateInspector() {
 	return new DebugInspector(this);
 
 }
-#endif
+#endif /* MTROPOLIS_DEBUG_ENABLE */
 
 bool VariableModifier::isVariable() const {
 	return true;
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 24be3f44cf9..a9e931e470d 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -29,17 +29,19 @@
 #include "common/hashmap.h"
 #include "common/hash-str.h"
 
-#include "graphics/pixelformat.h"
-
 #include "mtropolis/data.h"
 #include "mtropolis/debug.h"
 #include "mtropolis/vthread.h"
 
+class OSystem;
+
 namespace Graphics {
 
 struct WinCursorGroup;
 class MacCursor;
 class Cursor;
+struct PixelFormat;
+struct Surface;
 
 } // End of namespace Graphics
 
@@ -50,7 +52,6 @@ class CursorGraphic;
 class CursorGraphicCollection;
 class Element;
 class MessageDispatch;
-struct MessageProperties;
 class Modifier;
 class RuntimeObject;
 class PlugIn;
@@ -58,14 +59,15 @@ class Project;
 class Runtime;
 class Structural;
 class VisualElement;
+class Window;
 struct IMessageConsumer;
 struct IModifierContainer;
 struct IModifierFactory;
 struct IPlugInModifierFactory;
 struct IPlugInModifierFactoryAndDataFactory;
+struct MessageProperties;
 struct ModifierLoaderContext;
 
-
 namespace DynamicValueTypes {
 
 enum DynamicValueType {
@@ -92,7 +94,7 @@ enum DynamicValueType {
 
 namespace AttributeIDs {
 
-	enum AttributeID {
+enum AttributeID {
 	kAttribCache = 55,
 	kAttribDirect = 56,
 	kAttribVisible = 58,
@@ -181,7 +183,7 @@ struct Point16 {
 
 	bool load(const Data::Point &point);
 
-	inline bool operator==(const Point16& other) const {
+	inline bool operator==(const Point16 &other) const {
 		return x == other.x && y == other.y;
 	}
 
@@ -304,7 +306,7 @@ class DynamicListContainerBase {
 public:
 	virtual ~DynamicListContainerBase();
 	virtual bool setAtIndex(size_t index, const DynamicValue &dynValue) = 0;
-	virtual void setFrom(const DynamicListContainerBase &other) = 0;	// Only supports setting same type!
+	virtual void setFrom(const DynamicListContainerBase &other) = 0; // Only supports setting same type!
 	virtual const void *getConstArrayPtr() const = 0;
 	virtual size_t getSize() const = 0;
 	virtual bool compareEqual(const DynamicListContainerBase &other) const = 0;
@@ -462,7 +464,6 @@ private:
 	DynamicListContainerBase *_container;
 };
 
-
 struct DynamicValue {
 	DynamicValue();
 	DynamicValue(const DynamicValue &other);
@@ -488,7 +489,7 @@ struct DynamicValue {
 	DynamicValue &operator=(const DynamicValue &other);
 
 	bool operator==(const DynamicValue &other) const;
-	inline bool operator!=(const DynamicValue& other) const {
+	inline bool operator!=(const DynamicValue &other) const {
 		return !((*this) == other);
 	}
 
@@ -576,7 +577,6 @@ struct ProjectResources {
 	virtual ~ProjectResources();
 };
 
-
 class CursorGraphicCollection {
 public:
 	CursorGraphicCollection();
@@ -594,7 +594,7 @@ private:
 
 class ProjectDescription {
 public:
-	ProjectDescription(); 
+	ProjectDescription();
 	~ProjectDescription();
 
 	void addSegment(int volumeID, const char *filePath);
@@ -706,8 +706,8 @@ private:
 
 	Common::Array<PropagationStack> _propagationStack;
 	Common::SharedPtr<MessageProperties> _msg;
-	bool _cascade;	// Traverses structure tree
-	bool _relay;	// Fire on multiple modifiers
+	bool _cascade; // Traverses structure tree
+	bool _relay;   // Fire on multiple modifiers
 	bool _terminated;
 };
 
@@ -716,6 +716,7 @@ public:
 	Runtime();
 
 	void runFrame();
+	void drawFrame(OSystem *osystem);
 	void queueProject(const Common::SharedPtr<ProjectDescription> &desc);
 
 	void addVolume(int volumeID, const char *name, bool isMounted);
@@ -727,9 +728,12 @@ public:
 
 	uint32 allocateRuntimeGUID();
 
+	void addWindow(const Common::SharedPtr<Window> &window);
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	void debugSetEnabled(bool enabled);
 	void debugBreak();
+	Debugger *debugGetDebugger() const;
 #endif
 
 private:
@@ -791,6 +795,8 @@ private:
 	Common::SharedPtr<Structural> _activeSharedScene;
 	Common::Array<SceneReturnListEntry> _sceneReturnList;
 
+	Common::Array<Common::SharedPtr<Window> > _windows;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	Common::SharedPtr<Debugger> _debugger;
 #endif
@@ -882,9 +888,10 @@ public:
 	void materializeDescendents(Runtime *runtime, ObjectLinkingScope *outerScope);
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
-	virtual SupportStatus debugGetSupportStatus() const override;
-	virtual const Common::String &debugGetName() const override;
-	virtual Common::SharedPtr<DebugInspector> debugGetInspector() const override;
+	SupportStatus debugGetSupportStatus() const override;
+	const Common::String &debugGetName() const override;
+	Common::SharedPtr<DebugInspector> debugGetInspector() const override;
+	Debugger *debugGetDebugger() const override;
 
 	virtual DebugInspector *debugCreateInspector();
 #endif
@@ -902,6 +909,7 @@ protected:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	Common::SharedPtr<DebugInspector> _debugInspector;
+	Debugger *_debugger;
 #endif
 };
 
@@ -1175,9 +1183,10 @@ public:
 	virtual void visitInternalReferences(IStructuralReferenceVisitor *visitor);
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
-	virtual SupportStatus debugGetSupportStatus() const override;
-	virtual const Common::String &debugGetName() const override;
-	virtual Common::SharedPtr<DebugInspector> debugGetInspector() const override;
+	SupportStatus debugGetSupportStatus() const override;
+	const Common::String &debugGetName() const override;
+	Common::SharedPtr<DebugInspector> debugGetInspector() const override;
+	Debugger *debugGetDebugger() const override;
 
 	virtual DebugInspector *debugCreateInspector();
 #endif
@@ -1191,6 +1200,7 @@ protected:
 	
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	Common::SharedPtr<DebugInspector> _debugInspector;
+	Debugger *_debugger;
 #endif
 };
 


Commit: ca3ddbf23f8eddc48423a60147ad1b61a4128c59
    https://github.com/scummvm/scummvm/commit/ca3ddbf23f8eddc48423a60147ad1b61a4128c59
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add toast notifications and scene header to debug dashboard.

Changed paths:
    engines/mtropolis/debug.cpp
    engines/mtropolis/debug.h
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/mtropolis.h
    engines/mtropolis/render.cpp
    engines/mtropolis/render.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/debug.cpp b/engines/mtropolis/debug.cpp
index 7f297623d38..3a68b9a8b09 100644
--- a/engines/mtropolis/debug.cpp
+++ b/engines/mtropolis/debug.cpp
@@ -20,9 +20,13 @@
  */
 
 #include "mtropolis/debug.h"
+#include "mtropolis/render.h"
+#include "mtropolis/runtime.h"
 
 #include "gui/dialog.h"
 
+#include "graphics/fontman.h"
+
 namespace MTropolis {
 
 DebugInspector::DebugInspector(IDebuggable *debuggable) {
@@ -36,9 +40,36 @@ void DebugInspector::onDestroyed() {
 }
 
 Debugger::Debugger(Runtime *runtime) : _paused(false), _runtime(runtime) {
+	refreshSceneStatus();
 }
 
 Debugger::~Debugger() {
+	if (_runtime)
+		_runtime->removeWindow(_sceneStatusWindow.get());
+}
+
+void Debugger::runFrame(uint32 msec) {
+	for (size_t ri = _toastNotifications.size(); ri > 0; ri--) {
+		size_t i = ri - 1;
+
+		ToastNotification &toastNotification = _toastNotifications[i];
+
+		uint64 realTime = _runtime->getRealTime();
+		Window &window = *toastNotification.window;
+
+		if (realTime >= toastNotification.dismissTime) {
+			_runtime->removeWindow(&window);
+			_toastNotifications.remove_at(i);
+		}
+		else {
+			uint64 timeRemaining = toastNotification.dismissTime - realTime;
+			uint32 dismissDuration = 250;
+			if (timeRemaining < dismissDuration) {
+				int32 offset = window.getSurface()->w * static_cast<int32>(dismissDuration - timeRemaining) / static_cast<int32>(dismissDuration);
+				window.setPosition(-offset, window.getY());
+			}
+		}
+	}
 }
 
 void Debugger::setPaused(bool paused) {
@@ -50,7 +81,47 @@ bool Debugger::isPaused() const {
 }
 
 void Debugger::notify(DebugSeverity severity, const Common::String& str) {
-	// TODO
+	const int toastNotificationHeight = 15;
+
+	uint16 displayWidth, displayHeight;
+	_runtime->getDisplayResolution(displayWidth, displayHeight);
+
+	int horizPadding = 10;
+	const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
+	int width = font->getStringWidth(str) + horizPadding * 2;
+	if (width > displayWidth)
+		width = displayWidth;
+
+	const Graphics::PixelFormat pixelFmt = _runtime->getRenderPixelFormat();
+
+	ToastNotification toastNotification;
+	toastNotification.window.reset(new Window(0, displayHeight, width, toastNotificationHeight, pixelFmt));
+
+	byte fillColor[3] = {255, 255, 255};
+	if (severity == kDebugSeverityError) {
+		fillColor[0] = 255;
+		fillColor[1] = 100;
+		fillColor[2] = 100;
+	} else if (severity == kDebugSeverityWarning) {
+		fillColor[0] = 255;
+		fillColor[1] = 225;
+		fillColor[2] = 120;
+	}
+
+	Graphics::ManagedSurface &managedSurface = *toastNotification.window->getSurface();
+	managedSurface.fillRect(Common::Rect(0, 0, width, toastNotificationHeight), Render::resolveRGB(fillColor[0], fillColor[1], fillColor[2], pixelFmt));
+
+	font->drawString(&managedSurface, str, 10, (toastNotificationHeight - font->getFontAscent()) / 2, width - horizPadding * 2, Render::resolveRGB(0, 0, 0, pixelFmt));
+
+	toastNotification.dismissTime = _runtime->getRealTime() + 5250;
+
+	_toastNotifications.push_back(toastNotification);
+	_runtime->addWindow(toastNotification.window);
+
+	for (size_t i = 0; i < _toastNotifications.size(); i++) {
+		Window &window = *_toastNotifications[i].window;
+		window.setPosition(window.getX(), window.getY() - toastNotificationHeight);
+	}
 }
 
 void Debugger::notifyFmt(DebugSeverity severity, const char *fmt, ...) {
@@ -62,7 +133,106 @@ void Debugger::notifyFmt(DebugSeverity severity, const char *fmt, ...) {
 
 void Debugger::vnotifyFmt(DebugSeverity severity, const char* fmt, va_list args) {
 	Common::String str(Common::String::vformat(fmt, args));
-	this->notify(severity, fmt);
+	this->notify(severity, str);
 }
 
+void Debugger::refreshSceneStatus() {
+	const int sceneWindowWidth = 256;
+	const int sceneWindowHeight = 32;
+
+	const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
+
+	Common::Array<Common::String> sceneStrs;
+	Structural *sharedScene = _runtime->getActiveSharedScene().get();
+	if (sharedScene)
+		sceneStrs.push_back(Common::String("Shar: ") + sharedScene->debugGetName());
+	Structural *mainScene = _runtime->getActiveMainScene().get();
+	if (mainScene)
+		sceneStrs.push_back(Common::String("Main: ") + mainScene->debugGetName());
+
+	const uint horizPadding = 10;
+	const uint vertSpacing = 15;
+	int width = 0;
+	for (uint i = 0; i < sceneStrs.size(); i++) {
+		int lineWidth = font->getStringWidth(sceneStrs[i]);
+		if (lineWidth > width)
+			width = lineWidth;
+	}
+
+	if (_sceneStatusWindow.get()) {
+		_runtime->removeWindow(_sceneStatusWindow.get());
+		_sceneStatusWindow.reset();
+	}
+
+	const Graphics::PixelFormat pixelFmt = _runtime->getRenderPixelFormat();
+
+	_sceneStatusWindow.reset(new Window(0, 0, horizPadding * 2 + width, vertSpacing * sceneStrs.size(), pixelFmt));
+	_runtime->addWindow(_sceneStatusWindow);
+
+	for (uint i = 0; i < sceneStrs.size(); i++) {
+		font->drawString(_sceneStatusWindow->getSurface().get(), sceneStrs[i], horizPadding, vertSpacing * i + (vertSpacing - font->getFontAscent()) / 2, width, Render::resolveRGB(255, 255, 255, pixelFmt));
+	}
+}
+
+void Debugger::complainAboutUnfinished(Structural *structural) {
+	Common::HashMap<Common::String, SupportStatus> unfinishedModifiers;
+	Common::HashMap<Common::String, SupportStatus> unfinishedElements;
+
+	scanStructuralStatus(structural, unfinishedModifiers, unfinishedElements);
+
+	const SupportStatus supportStatusBins[2] = {kSupportStatusPartial,
+												kSupportStatusNone};
+	const char *supportStatusNames[2] = {"partially-finished", "unimplemented"};
+
+	const Common::HashMap<Common::String, SupportStatus> *typeBins[2] = {&unfinishedModifiers, &unfinishedElements};
+	const char *typeNames[2] = {"modifier", "element"};
+
+	for (int ssi = 0; ssi < 2; ssi++) {
+		for (int ti = 0; ti < 2; ti++) {
+			Common::Array<Common::String> names;
+			for (Common::HashMap<Common::String, SupportStatus>::const_iterator it = typeBins[ti]->begin(), itEnd = typeBins[ti]->end(); it != itEnd; ++it) {
+				if (it->_value == supportStatusBins[ssi])
+					names.push_back(it->_key);
+			}
+
+			Common::sort(names.begin(), names.end());
+
+			for (size_t i = 0; i < names.size(); i++) {
+				Common::String message = "Scene '" + structural->debugGetName() + "' contains " + supportStatusNames[ssi] + " " + typeNames[ti] + ": " + names[i];
+				this->notify(DebugSeverity::kDebugSeverityWarning, message);
+			}
+		}
+	}
+}
+
+void Debugger::scanStructuralStatus(Structural *structural, Common::HashMap<Common::String, SupportStatus> &unfinishedModifiers, Common::HashMap<Common::String, SupportStatus> &unfinishedElements) {
+	for (Common::Array<Common::SharedPtr<Structural>>::const_iterator it = structural->getChildren().begin(), itEnd = structural->getChildren().end(); it != itEnd; ++it) {
+		scanStructuralStatus(it->get(), unfinishedModifiers, unfinishedElements);
+	}
+
+	for (Common::Array<Common::SharedPtr<Modifier> >::const_iterator it = structural->getModifiers().begin(), itEnd = structural->getModifiers().end(); it != itEnd; ++it) {
+		scanModifierStatus(it->get(), unfinishedModifiers, unfinishedElements);
+	}
+
+	scanDebuggableStatus(structural, unfinishedElements);
+}
+
+void Debugger::scanModifierStatus(Modifier *modifier, Common::HashMap<Common::String, SupportStatus> &unfinishedModifiers, Common::HashMap<Common::String, SupportStatus> &unfinishedElements) {
+	IModifierContainer *children = modifier->getChildContainer();
+	if (children) {
+		for (Common::Array<Common::SharedPtr<Modifier> >::const_iterator it = children->getModifiers().begin(), itEnd = children->getModifiers().end(); it != itEnd; ++it) {
+			scanModifierStatus(it->get(), unfinishedModifiers, unfinishedElements);
+		}
+	}
+
+	scanDebuggableStatus(modifier, unfinishedModifiers);
+}
+
+void Debugger::scanDebuggableStatus(IDebuggable* debuggable, Common::HashMap<Common::String, SupportStatus>& unfinished) {
+	SupportStatus supportStatus = debuggable->debugGetSupportStatus();
+	if (supportStatus != kSupportStatusDone)
+		unfinished[Common::String(debuggable->debugGetTypeName())] = supportStatus;
+}
+
+
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/debug.h b/engines/mtropolis/debug.h
index 41ef650d911..9defffb009a 100644
--- a/engines/mtropolis/debug.h
+++ b/engines/mtropolis/debug.h
@@ -24,17 +24,27 @@
 
 #include "common/str.h"
 #include "common/ptr.h"
+#include "common/hashmap.h"
 
 #include <cstdarg>
 
 #define MTROPOLIS_DEBUG_VTHREAD_STACKS
 #define MTROPOLIS_DEBUG_ENABLE
 
+namespace Graphics {
+
+class MacFontManager;
+
+} // End of namespace Graphics
+
 namespace MTropolis {
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 
 class Runtime;
+class Window;
+class Structural;
+class Modifier;
 
 struct IDebuggable;
 
@@ -73,17 +83,35 @@ public:
 	explicit Debugger(Runtime *runtime);
 	~Debugger();
 
+	void runFrame(uint32 msec);
+
 	void setPaused(bool paused);
 	bool isPaused() const;
+
 	void notify(DebugSeverity severity, const Common::String &str);
 	void notifyFmt(DebugSeverity severity, const char *fmt, ...);
 	void vnotifyFmt(DebugSeverity severity, const char *fmt, va_list args);
 
+	void refreshSceneStatus();
+	void complainAboutUnfinished(Structural *structural);
+
 private:
 	Debugger();
 
+	static void scanStructuralStatus(Structural *structural, Common::HashMap<Common::String, SupportStatus> &unfinishedModifiers, Common::HashMap<Common::String, SupportStatus> &unfinishedElements);
+	static void scanModifierStatus(Modifier *modifier, Common::HashMap<Common::String, SupportStatus> &unfinishedModifiers, Common::HashMap<Common::String, SupportStatus> &unfinishedElements);
+	static void scanDebuggableStatus(IDebuggable *debuggable, Common::HashMap<Common::String, SupportStatus> &unfinished);
+
+	struct ToastNotification {
+		Common::SharedPtr<Window> window;
+		uint64 dismissTime;
+	};
+
 	bool _paused;
 	Runtime *_runtime;
+	Common::SharedPtr<Window> _sceneStatusWindow;
+	Common::SharedPtr<Graphics::MacFontManager> _macFontMan;
+	Common::Array<ToastNotification> _toastNotifications;
 };
 
 #define MTROPOLIS_DEBUG_NOTIFY(...) \
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 682c2536875..029dabb0b35 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -55,6 +55,10 @@ void BehaviorModifier::appendModifier(const Common::SharedPtr<Modifier> &modifie
 	_children.push_back(modifier);
 }
 
+IModifierContainer* BehaviorModifier::getChildContainer() {
+	return this;
+}
+
 Common::SharedPtr<Modifier> BehaviorModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new BehaviorModifier(*this));
 }
@@ -504,6 +508,10 @@ bool CompoundVariableModifier::load(ModifierLoaderContext &context, const Data::
 	return true;
 }
 
+IModifierContainer *CompoundVariableModifier::getChildContainer() {
+	return this;
+}
+
 const Common::Array<Common::SharedPtr<Modifier> > &CompoundVariableModifier::getModifiers() const {
 	return _children;
 }
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 692c87a082e..169759f3d18 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -38,6 +38,8 @@ public:
 	const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const override;
 	void appendModifier(const Common::SharedPtr<Modifier> &modifier) override;
 
+	IModifierContainer *getChildContainer() override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Behavior Modifier"; }
 #endif
@@ -511,6 +513,8 @@ class CompoundVariableModifier : public VariableModifier, public IModifierContai
 public:
 	bool load(ModifierLoaderContext &context, const Data::CompoundVariableModifier &data);
 
+	IModifierContainer *getChildContainer() override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Compound Variable Modifier"; }
 #endif
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index 346455e1035..0ef4a3926a6 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -391,6 +391,16 @@ Common::Error MTropolisEngine::run() {
 	if (selectedMode == kColorDepthModeInvalid)
 		error("Couldn't resolve a color depth mode");
 
+	// Set up supported pixel modes
+	for (int i = 0; i < kColorDepthModeCount; i++) {
+		if (haveExactMode[i] || haveCloseMode[i])
+			_runtime->setupDisplayMode(static_cast<ColorDepthMode>(i), modePixelFormats[i]);
+	}
+
+	// Set active mode
+	_runtime->switchDisplayMode(selectedMode, selectedMode);
+	_runtime->setDisplayResolution(preferredWidth, preferredHeight);
+
 	initGraphics(preferredWidth, preferredHeight, &modePixelFormats[selectedMode]);
 	setDebugger(new Console(this));
 
@@ -403,9 +413,15 @@ Common::Error MTropolisEngine::run() {
 	}
 #endif
 
+	uint32 prevTimeStamp = getTotalPlayTime();
+
 	while (!shouldQuit()) {
+		const uint32 frameTime = getTotalPlayTime();
+		const uint32 elapsedThisFrame = frameTime - prevTimeStamp;
+		prevTimeStamp = frameTime;
+
 		handleEvents();
-		_runtime->runFrame();
+		_runtime->runFrame(elapsedThisFrame);
 		_runtime->drawFrame(_system);
 	}
 
diff --git a/engines/mtropolis/mtropolis.h b/engines/mtropolis/mtropolis.h
index c0d921723cc..c7bb4757ae9 100644
--- a/engines/mtropolis/mtropolis.h
+++ b/engines/mtropolis/mtropolis.h
@@ -38,19 +38,6 @@
  */
 namespace MTropolis {
 
-enum ColorDepthMode {
-	kColorDepthMode1Bit,
-	kColorDepthMode2Bit,
-	kColorDepthMode4Bit,
-	kColorDepthMode8Bit,
-	kColorDepthMode16Bit,
-	kColorDepthMode32Bit,
-
-	kColorDepthModeCount,
-
-	kColorDepthModeInvalid,
-};
-
 class Runtime;
 
 class MTropolisEngine : public ::Engine {
diff --git a/engines/mtropolis/render.cpp b/engines/mtropolis/render.cpp
index a6d6df8eca8..c149f7f3309 100644
--- a/engines/mtropolis/render.cpp
+++ b/engines/mtropolis/render.cpp
@@ -21,7 +21,9 @@
 
 #include "mtropolis/render.h"
 #include "mtropolis/runtime.h"
+
 #include "graphics/surface.h"
+#include "graphics/managed_surface.h"
 
 namespace MTropolis {
 
@@ -62,16 +64,11 @@ inline int quantize8To5(int value, byte orderedDither16x16) {
 	return (value * 249 + (orderedDither16x16 << 3)) >> 11;
 }
 
-Window::Window(int32 x, int32 y, int16 width, int16 height, const Graphics::PixelFormat &format) : _x(x), _y(y), _surface(nullptr) {
-	_surface = new Graphics::Surface();
-	_surface->create(width, height, format);
+Window::Window(int32 x, int32 y, int16 width, int16 height, const Graphics::PixelFormat &format) : _x(x), _y(y) {
+	_surface.reset(new Graphics::ManagedSurface(width, height, format));
 }
 
 Window::~Window() {
-	if (_surface) {
-		_surface->free();
-		delete _surface;
-	}
 }
 
 int32 Window::getX() const {
@@ -82,8 +79,31 @@ int32 Window::getY() const {
 	return _y;
 }
 
-Graphics::Surface &Window::getSurface() const {
-	return *_surface;
+void Window::setPosition(int32 x, int32 y) {
+	_x = x;
+	_y = y;
+}
+
+
+const Common::SharedPtr<Graphics::ManagedSurface> &Window::getSurface() const {
+	return _surface;
+}
+
+const Graphics::PixelFormat& Window::getPixelFormat() const {
+	return _surface->format;
+}
+
+namespace Render {
+
+uint32 resolveRGB(uint8 r, uint8 g, uint8 b, const Graphics::PixelFormat &fmt) {
+	uint32 rPlaced = (static_cast<uint32>(r) >> (8 - fmt.rBits())) << fmt.rShift;
+	uint32 gPlaced = (static_cast<uint32>(g) >> (8 - fmt.gBits())) << fmt.gShift;
+	uint32 bPlaced = (static_cast<uint32>(b) >> (8 - fmt.bBits())) << fmt.bShift;
+	uint32 aPlaced = (static_cast<uint32>(255) >> (8 - fmt.aBits())) << fmt.aShift;
+
+	return rPlaced | gPlaced | bPlaced | aPlaced;
 }
 
+} // End of namespace Render
+
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/render.h b/engines/mtropolis/render.h
index 89a6bdcd39b..a63b017c43a 100644
--- a/engines/mtropolis/render.h
+++ b/engines/mtropolis/render.h
@@ -22,12 +22,14 @@
 #ifndef MTROPOLIS_RENDER_H
 #define MTROPOLIS_RENDER_H
 
+#include "common/ptr.h"
 #include "common/scummsys.h"
+
 #include "graphics/pixelformat.h"
 
 namespace Graphics {
 
-struct Surface;
+class ManagedSurface;
 
 } // End of namespace Graphics
 
@@ -40,14 +42,23 @@ public:
 
 	int32 getX() const;
 	int32 getY() const;
-	Graphics::Surface &getSurface() const;
+	void setPosition(int32 x, int32 y);
+
+	const Common::SharedPtr<Graphics::ManagedSurface> &getSurface() const;
+	const Graphics::PixelFormat &getPixelFormat() const;
 
 private:
 	int32 _x;
 	int32 _y;
-	Graphics::Surface *_surface;
+	Common::SharedPtr<Graphics::ManagedSurface> _surface;
 };
 
+namespace Render {
+
+uint32 resolveRGB(uint8 r, uint8 g, uint8 b, const Graphics::PixelFormat &fmt);
+
+} // End of namespace Render
+
 } // End of namespace MTropolis
 
 #endif
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index ab50b0c9008..4532071e49f 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -33,9 +33,11 @@
 #include "common/substream.h"
 #include "common/system.h"
 
+#include "graphics/managed_surface.h"
 #include "graphics/surface.h"
 #include "graphics/wincursor.h"
 #include "graphics/maccursor.h"
+#include "graphics/macgui/macfontmanager.h"
 
 namespace MTropolis {
 
@@ -895,7 +897,7 @@ void CursorGraphicCollection::addMacCursor(uint32 cursorID, const Common::Shared
 }
 
 
-ProjectDescription::ProjectDescription() {
+ProjectDescription::ProjectDescription() : _language(Common::EN_ANY) {
 }
 
 ProjectDescription::~ProjectDescription() {
@@ -942,6 +944,14 @@ void ProjectDescription::setCursorGraphics(const Common::SharedPtr<CursorGraphic
 	_cursorGraphics = cursorGraphics;
 }
 
+void ProjectDescription::setLanguage(const Common::Language &language) {
+	_language = language;
+}
+
+const Common::Language &ProjectDescription::getLanguage() const {
+	return _language;
+}
+
 const Common::Array<Common::SharedPtr<Modifier> >& SimpleModifierContainer::getModifiers() const {
 	return _modifiers;
 }
@@ -1359,14 +1369,27 @@ Runtime::SceneStackEntry::SceneStackEntry() {
 }
 
 
-Runtime::Runtime() : _nextRuntimeGUID(1) {
+Runtime::Runtime() : _nextRuntimeGUID(1), _realDisplayMode(kColorDepthModeInvalid), _fakeDisplayMode(kColorDepthModeInvalid),
+	_displayWidth(1024), _displayHeight(768),
+					 _realTime(0), _playTime(0) {
 	_vthread.reset(new VThread());
+
+	for (int i = 0; i < kColorDepthModeCount; i++) {
+		_displayModeSupported[i] = false;
+		_realDisplayMode = kColorDepthModeInvalid;
+		_fakeDisplayMode = kColorDepthModeInvalid;
+	}
 }
 
-void Runtime::runFrame() {
+void Runtime::runFrame(uint32 msec) {
+	_realTime += msec;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
-	if (_debugger && _debugger->isPaused())
-		return;
+	if (_debugger) {
+		_debugger->runFrame(msec);
+		if (_debugger->isPaused())
+			return;
+	}
 #endif
 
 	for (;;) {
@@ -1382,6 +1405,8 @@ void Runtime::runFrame() {
 
 			unloadProject();
 
+			_macFontMan.reset(new Graphics::MacFontManager(0, desc->getLanguage()));
+
 			_project.reset(new Project());
 
 			_project->loadFromDescription(*desc);
@@ -1451,15 +1476,19 @@ void Runtime::runFrame() {
 		// Ran out of actions
 		break;
 	}
+
+	_playTime += msec;
 }
 
 void Runtime::drawFrame(OSystem* system) {
 	int width = system->getWidth();
 	int height = system->getHeight();
 
+	system->fillScreen(Render::resolveRGB(0, 0, 0, getRenderPixelFormat()));
+
 	for (Common::Array<Common::SharedPtr<Window> >::const_iterator it = _windows.begin(), itEnd = _windows.end(); it != itEnd; ++it) {
 		const Window &window = *it->get();
-		const Graphics::Surface &surface = window.getSurface();
+		const Graphics::ManagedSurface &surface = *window.getSurface();
 
 		int32 destLeft = window.getX();
 		int32 destRight = destLeft + surface.w;
@@ -1499,7 +1528,9 @@ void Runtime::drawFrame(OSystem* system) {
 		if (destLeft >= destRight || destTop >= destBottom || destLeft >= width || destTop >= height || destRight <= 0 || destBottom <= 0)
 			continue;
 
-		system->copyRectToScreen(surface.getBasePtr(destLeft, destTop), surface.pitch, destLeft, destTop, destRight - destLeft, destBottom - destTop);
+		system->copyRectToScreen(surface.getBasePtr(srcLeft, srcTop), surface.pitch, destLeft, destTop, destRight - destLeft, destBottom - destTop);
+
+		int n = 0;
 	}
 
 	system->updateScreen();
@@ -1766,7 +1797,10 @@ void Runtime::loadScene(const Common::SharedPtr<Structural>& scene) {
 	debug(1, "Scene materialized OK");
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
-
+	if (_debugger) {
+		_debugger->complainAboutUnfinished(scene.get());
+		_debugger->refreshSceneStatus();
+	}
 #endif
 }
 
@@ -1800,6 +1834,19 @@ void Runtime::queueMessage(const Common::SharedPtr<MessageDispatch>& dispatch) {
 	_messageQueue.push_back(dispatch);
 }
 
+void Runtime::ensureMainWindowExists() {
+	// Maybe there's a better spot for this
+	if (!_mainWindow && _project) {
+		const ProjectPresentationSettings &presentationSettings = _project->getPresentationSettings();
+
+		int32 centeredX = (static_cast<int32>(_displayWidth) - static_cast<int32>(presentationSettings.width)) / 2;
+		int32 centeredY = (static_cast<int32>(_displayHeight) - static_cast<int32>(presentationSettings.height)) / 2;
+		Common::SharedPtr<Window> mainWindow(new Window(centeredX, centeredY, presentationSettings.width, presentationSettings.height, _displayModePixelFormats[_realDisplayMode]));
+		addWindow(mainWindow);
+		_mainWindow.reset(mainWindow);
+	}
+}
+
 void Runtime::unloadProject() {
 	_activeMainScene.reset();
 	_activeSharedScene.reset();
@@ -1811,6 +1858,10 @@ void Runtime::unloadProject() {
 	_messageQueue.clear();
 	_vthread.reset(new VThread());
 
+	if (_mainWindow) {
+		removeWindow(_mainWindow.lock().get());
+	}
+
 	// These should be last
 	_project.reset();
 	_rootLinkingScope.reset();
@@ -1851,6 +1902,68 @@ void Runtime::addWindow(const Common::SharedPtr<Window> &window) {
 	_windows.push_back(window);
 }
 
+void Runtime::removeWindow(Window *window) {
+	for (size_t i = 0; i < _windows.size(); i++) {
+		if (_windows[i].get() == window) {
+			_windows.remove_at(i);
+			break;
+		}
+	}
+}
+
+void Runtime::setupDisplayMode(ColorDepthMode displayMode, const Graphics::PixelFormat& pixelFormat) {
+	_displayModeSupported[displayMode] = true;
+	_displayModePixelFormats[displayMode] = pixelFormat;
+}
+
+bool Runtime::switchDisplayMode(ColorDepthMode realDisplayMode, ColorDepthMode fakeDisplayMode) {
+	_fakeDisplayMode = fakeDisplayMode;
+
+	if (_realDisplayMode != realDisplayMode) {
+		_realDisplayMode = realDisplayMode;
+		_windows.clear();
+		return true;
+	}
+
+	return false;
+}
+
+void Runtime::setDisplayResolution(uint16 width, uint16 height) {
+	_displayWidth = width;
+	_displayHeight = height;
+}
+
+void Runtime::getDisplayResolution(uint16 &outWidth, uint16 &outHeight) const {
+	outWidth = _displayWidth;
+	outHeight = _displayHeight;
+}
+
+const Graphics::PixelFormat& Runtime::getRenderPixelFormat() const {
+	assert(_realDisplayMode != kColorDepthModeInvalid);
+
+	return _displayModePixelFormats[_realDisplayMode];
+}
+
+const Common::SharedPtr<Graphics::MacFontManager>& Runtime::getMacFontManager() const {
+	return _macFontMan;
+}
+
+const Common::SharedPtr<Structural> &Runtime::getActiveMainScene() const {
+	return _activeMainScene;
+}
+
+const Common::SharedPtr<Structural> &Runtime::getActiveSharedScene() const {
+	return _activeSharedScene;
+}
+
+uint64 Runtime::getRealTime() const {
+	return _realTime;
+}
+
+uint64 Runtime::getPlayTime() const {
+	return _playTime;
+}
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 
 void Runtime::debugSetEnabled(bool enabled) {
@@ -2100,6 +2213,10 @@ void Project::materializeGlobalVariables(Runtime *runtime, ObjectLinkingScope *o
 	}
 }
 
+const ProjectPresentationSettings& Project::getPresentationSettings() const {
+	return _presentationSettings;
+}
+
 void Project::openSegmentStream(int segmentIndex) {
 	if (segmentIndex < 0 || static_cast<size_t>(segmentIndex) > _segments.size()) {
 		error("Invalid segment index %i", segmentIndex);
@@ -2599,7 +2716,11 @@ bool Modifier::isVariable() const {
 	return false;
 }
 
-IModifierContainer *Modifier::getMessagePropagationContainer() const {
+IModifierContainer *Modifier::getMessagePropagationContainer() {
+	return nullptr;
+}
+
+IModifierContainer *Modifier::getChildContainer() {
 	return nullptr;
 }
 
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index a9e931e470d..91fd7f413dc 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -23,12 +23,15 @@
 #define MTROPOLIS_RUNTIME_H
 
 #include "common/array.h"
+#include "common/language.h"
 #include "common/platform.h"
 #include "common/ptr.h"
 #include "common/stream.h"
 #include "common/hashmap.h"
 #include "common/hash-str.h"
 
+#include "graphics/pixelformat.h"
+
 #include "mtropolis/data.h"
 #include "mtropolis/debug.h"
 #include "mtropolis/vthread.h"
@@ -39,6 +42,7 @@ namespace Graphics {
 
 struct WinCursorGroup;
 class MacCursor;
+class MacFontManager;
 class Cursor;
 struct PixelFormat;
 struct Surface;
@@ -68,6 +72,19 @@ struct IPlugInModifierFactoryAndDataFactory;
 struct MessageProperties;
 struct ModifierLoaderContext;
 
+enum ColorDepthMode {
+	kColorDepthMode1Bit,
+	kColorDepthMode2Bit,
+	kColorDepthMode4Bit,
+	kColorDepthMode8Bit,
+	kColorDepthMode16Bit,
+	kColorDepthMode32Bit,
+
+	kColorDepthModeCount,
+
+	kColorDepthModeInvalid,
+};
+
 namespace DynamicValueTypes {
 
 enum DynamicValueType {
@@ -609,11 +626,15 @@ public:
 
 	void setCursorGraphics(const Common::SharedPtr<CursorGraphicCollection> &cursorGraphics);
 
+	void setLanguage(const Common::Language &language);
+	const Common::Language &getLanguage() const;
+
 private:
 	Common::Array<SegmentDescription> _segments;
 	Common::Array<Common::SharedPtr<PlugIn> > _plugIns;
 	Common::SharedPtr<ProjectResources> _resources;
 	Common::SharedPtr<CursorGraphicCollection> _cursorGraphics;
+	Common::Language _language;
 };
 
 struct VolumeState {
@@ -715,7 +736,7 @@ class Runtime {
 public:
 	Runtime();
 
-	void runFrame();
+	void runFrame(uint32 msec);
 	void drawFrame(OSystem *osystem);
 	void queueProject(const Common::SharedPtr<ProjectDescription> &desc);
 
@@ -729,6 +750,25 @@ public:
 	uint32 allocateRuntimeGUID();
 
 	void addWindow(const Common::SharedPtr<Window> &window);
+	void removeWindow(Window *window);
+
+	// Sets up a supported display mode
+	void setupDisplayMode(ColorDepthMode displayMode, const Graphics::PixelFormat &pixelFormat);
+
+	// Switches to a specified display mode.  Returns true if the mode was actually changed.  If so, all windows will need
+	// to be recreated.
+	bool switchDisplayMode(ColorDepthMode realDisplayMode, ColorDepthMode fakeDisplayMode);
+	void setDisplayResolution(uint16 width, uint16 height);
+	void getDisplayResolution(uint16 &outWidth, uint16 &outHeight) const;
+
+	const Graphics::PixelFormat &getRenderPixelFormat() const;
+	const Common::SharedPtr<Graphics::MacFontManager> &getMacFontManager() const;
+
+	const Common::SharedPtr<Structural> &getActiveMainScene() const;
+	const Common::SharedPtr<Structural> &getActiveSharedScene() const;
+
+	uint64 getRealTime() const;
+	uint64 getPlayTime() const;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	void debugSetEnabled(bool enabled);
@@ -775,6 +815,8 @@ private:
 	void sendMessageOnVThread(const Common::SharedPtr<MessageDispatch> &dispatch);
 	void queueMessage(const Common::SharedPtr<MessageDispatch> &dispatch);
 
+	void ensureMainWindowExists();
+
 	void unloadProject();
 
 	VThreadState dispatchMessageTask(const DispatchMethodTaskData &data);
@@ -795,13 +837,26 @@ private:
 	Common::SharedPtr<Structural> _activeSharedScene;
 	Common::Array<SceneReturnListEntry> _sceneReturnList;
 
+	Common::WeakPtr<Window> _mainWindow;
 	Common::Array<Common::SharedPtr<Window> > _windows;
 
+	Common::SharedPtr<Graphics::MacFontManager> _macFontMan;
+
+	uint32 _nextRuntimeGUID;
+
+	bool _displayModeSupported[kColorDepthModeCount];
+	Graphics::PixelFormat _displayModePixelFormats[kColorDepthModeCount];
+	ColorDepthMode _realDisplayMode;
+	ColorDepthMode _fakeDisplayMode;
+	uint16 _displayWidth;
+	uint16 _displayHeight;
+
+	uint64 _realTime;
+	uint64 _playTime;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	Common::SharedPtr<Debugger> _debugger;
 #endif
-
-	uint32 _nextRuntimeGUID;
 };
 
 struct IModifierContainer {
@@ -994,6 +1049,8 @@ public:
 	Common::SharedPtr<Modifier> resolveAlias(uint32 aliasID) const;
 	void materializeGlobalVariables(Runtime *runtime, ObjectLinkingScope *scope);
 
+	const ProjectPresentationSettings &getPresentationSettings() const;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Project"; }
 #endif
@@ -1169,7 +1226,8 @@ public:
 	virtual bool isVariable() const;
 
 	// This should only return a propagation container if messages should actually be propagated (i.e. NOT switched-off behaviors!)
-	virtual IModifierContainer *getMessagePropagationContainer() const;
+	virtual IModifierContainer *getMessagePropagationContainer();
+	virtual IModifierContainer *getChildContainer();
 
 	bool respondsToEvent(const Event &evt) const override;
 	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) const override;


Commit: d7f3a26c7487462cbaf72cfccb58a848aac8eaf7
    https://github.com/scummvm/scummvm/commit/d7f3a26c7487462cbaf72cfccb58a848aac8eaf7
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix layer field not being set

Changed paths:
    engines/mtropolis/runtime.cpp


diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 4532071e49f..8aeea9908d2 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -2665,6 +2665,7 @@ bool VisualElement::loadCommon(const Common::String &name, uint32 guid, const Da
 	_isHidden = ((elementFlags & Data::ElementFlags::kHidden) != 0);
 	_streamLocator = streamLocator;
 	_sectionID = sectionID;
+	_layer = layer;
 
 	return true;
 }


Commit: d93775e31e3c7d1af502e6957e737adea5a643c6
    https://github.com/scummvm/scummvm/commit/d93775e31e3c7d1af502e6957e737adea5a643c6
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Implement more miniscript

Changed paths:
    engines/mtropolis/debug.cpp
    engines/mtropolis/debug.h
    engines/mtropolis/element_factory.cpp
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/miniscript.h
    engines/mtropolis/modifier_factory.cpp
    engines/mtropolis/modifier_factory.h
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/notes.txt
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/debug.cpp b/engines/mtropolis/debug.cpp
index 3a68b9a8b09..bc8ecb3a07f 100644
--- a/engines/mtropolis/debug.cpp
+++ b/engines/mtropolis/debug.cpp
@@ -137,9 +137,6 @@ void Debugger::vnotifyFmt(DebugSeverity severity, const char* fmt, va_list args)
 }
 
 void Debugger::refreshSceneStatus() {
-	const int sceneWindowWidth = 256;
-	const int sceneWindowHeight = 32;
-
 	const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
 
 	Common::Array<Common::String> sceneStrs;
diff --git a/engines/mtropolis/debug.h b/engines/mtropolis/debug.h
index 9defffb009a..e73f9bd4880 100644
--- a/engines/mtropolis/debug.h
+++ b/engines/mtropolis/debug.h
@@ -31,12 +31,6 @@
 #define MTROPOLIS_DEBUG_VTHREAD_STACKS
 #define MTROPOLIS_DEBUG_ENABLE
 
-namespace Graphics {
-
-class MacFontManager;
-
-} // End of namespace Graphics
-
 namespace MTropolis {
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
@@ -110,7 +104,6 @@ private:
 	bool _paused;
 	Runtime *_runtime;
 	Common::SharedPtr<Window> _sceneStatusWindow;
-	Common::SharedPtr<Graphics::MacFontManager> _macFontMan;
 	Common::Array<ToastNotification> _toastNotifications;
 };
 
diff --git a/engines/mtropolis/element_factory.cpp b/engines/mtropolis/element_factory.cpp
index b8e9d03b92b..650ae80fddc 100644
--- a/engines/mtropolis/element_factory.cpp
+++ b/engines/mtropolis/element_factory.cpp
@@ -44,6 +44,8 @@ Common::SharedPtr<Element> ElementFactory<TElement, TElementData>::createElement
 
 	if (!element->load(context, static_cast<const TElementData &>(dataObject)))
 		element.reset();
+	else
+		element->setSelfReference(element);
 
 	return Common::SharedPtr<Element>(element);
 }
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 8c3bb896700..ba3bcf019ef 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -32,7 +32,21 @@ namespace MTropolis {
 MiniscriptInstruction::~MiniscriptInstruction() {
 }
 
-MiniscriptReferences::MiniscriptReferences(const Common::Array<LocalRef> &localRefs, const Common::Array<GlobalRef> &globalRefs) : _localRefs(localRefs), _globalRefs(globalRefs) {
+MiniscriptReferences::MiniscriptReferences(const Common::Array<LocalRef> &localRefs) : _localRefs(localRefs) {
+}
+
+void MiniscriptReferences::linkInternalReferences(ObjectLinkingScope *scope) {
+	// Resolve using name lookups since there are some known cases where the GUID is broken
+	// e.g. "bArriveFromCutScene" in "Set bArriveFromCutScene on PE" in Obsidian
+	for (Common::Array<LocalRef>::iterator it = _localRefs.begin(), itEnd = _localRefs.end(); it != itEnd; ++it) {
+		it->resolution = scope->resolve(it->guid, it->name, false);
+	}
+}
+
+Common::WeakPtr<RuntimeObject> MiniscriptReferences::getRefByIndex(uint index) const {
+	if (index >= _localRefs.size())
+		return Common::WeakPtr<RuntimeObject>();
+	return _localRefs[index].resolution;
 }
 
 MiniscriptProgram::MiniscriptProgram(const Common::SharedPtr<Common::Array<uint8> > &programData, const Common::Array<MiniscriptInstruction *> &instructions, const Common::Array<Attribute> &attributes)
@@ -45,6 +59,9 @@ MiniscriptProgram::~MiniscriptProgram() {
 		(*it)->~MiniscriptInstruction();
 }
 
+const Common::Array<MiniscriptInstruction *> &MiniscriptProgram::getInstructions() const {
+	return _instructions;
+}
 
 template<class T>
 struct MiniscriptInstructionLoader {
@@ -79,7 +96,7 @@ bool MiniscriptInstructionLoader<MiniscriptInstructions::BuiltinFunc>::loadInstr
 		return false;
 
 	if (functionID < 1 || functionID > 20)
-		return false;	// Unknown function
+		return false; // Unknown function
 
 	new (dest) MiniscriptInstructions::BuiltinFunc(static_cast<MiniscriptInstructions::BuiltinFunc::BuiltinFunctionID>(functionID));
 	return true;
@@ -113,10 +130,10 @@ bool MiniscriptInstructionLoader<MiniscriptInstructions::Jump>::loadInstruction(
 
 	bool isConditional = (jumpFlags == 2);
 	if (jumpFlags != 1 && jumpFlags != 2)
-		return false;	// Don't recognize this flag combination
+		return false; // Don't recognize this flag combination
 
 	if (instrOffset == 0)
-		return false;	// Not valid
+		return false; // Not valid
 
 	new (dest) MiniscriptInstructions::Jump(instrOffset, isConditional);
 	return true;
@@ -211,24 +228,21 @@ bool MiniscriptInstructionFactory<T>::create(void *dest, uint32 instrFlags, Data
 }
 
 template<class T>
-void MiniscriptInstructionFactory<T>::getSizeAndAlignment(size_t& outSize, size_t& outAlignment) const {
+void MiniscriptInstructionFactory<T>::getSizeAndAlignment(size_t &outSize, size_t &outAlignment) const {
 	outSize = sizeof(T);
 	outAlignment = AlignmentHelper<T>::getAlignment();
 }
 
 template<class T>
-inline IMiniscriptInstructionFactory* MiniscriptInstructionFactory<T>::getInstance() {
+inline IMiniscriptInstructionFactory *MiniscriptInstructionFactory<T>::getInstance() {
 	return &_instance;
 }
 
 template<class T>
 MiniscriptInstructionFactory<T> MiniscriptInstructionFactory<T>::_instance;
 
-
 bool MiniscriptParser::parse(const Data::MiniscriptProgram &program, Common::SharedPtr<MiniscriptProgram> &outProgram, Common::SharedPtr<MiniscriptReferences> &outReferences) {
 	Common::Array<MiniscriptReferences::LocalRef> localRefs;
-	Common::Array<MiniscriptReferences::GlobalRef> globalRefs;
-	Common::HashMap<uint32, size_t> globalGUIDToGlobalRefIndex;
 	Common::Array<MiniscriptProgram::Attribute> attributes;
 	Common::SharedPtr<Common::Array<uint8> > programDataPtr;
 	Common::Array<MiniscriptInstruction *> miniscriptInstructions;
@@ -236,7 +250,7 @@ bool MiniscriptParser::parse(const Data::MiniscriptProgram &program, Common::Sha
 	// If the program is empty then just return an empty program
 	if (program.bytecode.size() == 0 || program.numOfInstructions == 0) {
 		outProgram = Common::SharedPtr<MiniscriptProgram>(new MiniscriptProgram(programDataPtr, miniscriptInstructions, attributes));
-		outReferences = Common::SharedPtr<MiniscriptReferences>(new MiniscriptReferences(localRefs, globalRefs));
+		outReferences = Common::SharedPtr<MiniscriptReferences>(new MiniscriptReferences(localRefs));
 		return true;
 	}
 
@@ -338,36 +352,16 @@ bool MiniscriptParser::parse(const Data::MiniscriptProgram &program, Common::Sha
 
 			return false;
 		}
-
-		// Allocate the static GUID in the reference set
-		if (rawInstruction.opcode == 0x192) {
-			MiniscriptInstructions::PushGlobal *instr = static_cast<MiniscriptInstructions::PushGlobal *>(miniscriptInstructions[i]);
-			uint32 staticGUID = instr->getStaticGUID();
-			Common::HashMap<uint32, size_t>::const_iterator refIt = globalGUIDToGlobalRefIndex.find(staticGUID);
-			if (refIt == globalGUIDToGlobalRefIndex.end()) {
-				const size_t index = globalRefs.size();
-
-				MiniscriptReferences::GlobalRef globalRef;
-				globalRef.guid = staticGUID;
-
-				globalRefs.push_back(globalRef);
-				globalGUIDToGlobalRefIndex[staticGUID] = index;
-
-				instr->setReferenceSetIndex(index);
-			} else {
-				instr->setReferenceSetIndex(refIt->_value);
-			}
-		}
 	}
 
 	// Done
 	outProgram = Common::SharedPtr<MiniscriptProgram>(new MiniscriptProgram(programDataPtr, miniscriptInstructions, attributes));
-	outReferences = Common::SharedPtr<MiniscriptReferences>(new MiniscriptReferences(localRefs, globalRefs));
+	outReferences = Common::SharedPtr<MiniscriptReferences>(new MiniscriptReferences(localRefs));
 
 	return true;
 }
 
-IMiniscriptInstructionFactory* MiniscriptParser::resolveOpcode(uint16 opcode) {
+IMiniscriptInstructionFactory *MiniscriptParser::resolveOpcode(uint16 opcode) {
 	switch (opcode) {
 	case 0x834:
 		return MiniscriptInstructionFactory<MiniscriptInstructions::Set>::getInstance();
@@ -438,6 +432,11 @@ IMiniscriptInstructionFactory* MiniscriptParser::resolveOpcode(uint16 opcode) {
 
 namespace MiniscriptInstructions {
 
+MiniscriptInstructionOutcome UnimplementedInstruction::execute(MiniscriptThread *thread) const {
+	thread->error("Unimplemented instruction");
+	return kMiniscriptInstructionOutcomeFailed;
+}
+
 Send::Send(const Event &evt) : _evt(evt) {
 }
 
@@ -469,19 +468,138 @@ PushValue::PushValue(DataType dataType, const void *value, bool isLValue)
 	}
 }
 
-PushGlobal::PushGlobal(uint32 guid, bool isLValue) : _guid(guid), _refSetIndex(0), _isLValue(isLValue) {
+MiniscriptInstructionOutcome PushValue::execute(MiniscriptThread *thread) const {
+	DynamicValue value;
+
+	switch (_dataType) {
+	case DataType::kDataTypeNull:
+		value.clear();
+		break;
+	case DataType::kDataTypeDouble:
+		value.setFloat(_value.f);
+		break;
+	case DataType::kDataTypeBool:
+		value.setFloat(_value.b);
+		break;
+	case DataType::kDataTypeLocalRef:
+		value.setObject(thread->getRefs()->getRefByIndex(_value.ref));
+		break;
+	case DataType::kDataTypeGlobalRef:
+		thread->error("Global references are not implemented");
+		return kMiniscriptInstructionOutcomeFailed;
+	case DataType::kDataTypeLabel: {
+		MTropolis::Label label;
+		label.id = _value.lbl.id;
+		label.superGroupID = _value.lbl.superGroup;
+		value.setLabel(label);
+	} break;
+	default:
+		assert(false);
+		break;
+	}
+
+	if (_isLValue)
+		thread->pushLValue(value);
+	else
+		thread->pushRValue(value);
+
+	return kMiniscriptInstructionOutcomeContinue;
 }
 
-uint32 PushGlobal::getStaticGUID() const {
-	return _guid;
+PushGlobal::PushGlobal(uint32 globalID, bool isLValue) : _globalID(globalID), _isLValue(isLValue) {
 }
 
-void PushGlobal::setReferenceSetIndex(size_t refSetIndex) {
-	_refSetIndex = refSetIndex;
+MiniscriptInstructionOutcome PushGlobal::execute(MiniscriptThread *thread) const {
+	DynamicValue value;
+	switch (_globalID) {
+	case kGlobalRefElement:
+	case kGlobalRefSection:
+	case kGlobalRefSubsection:
+	case kGlobalRefScene:
+	case kGlobalRefProject:
+		return executeFindFilteredParent(thread);
+	case kGlobalRefIncomingData:
+		value = thread->getMessageProperties()->getValue();
+		break;
+	case kGlobalRefSource:
+		value.setObject(thread->getMessageProperties()->getSource());
+		break;
+	case kGlobalRefMouse:
+		thread->error("'mouse' global ref not yet implemented");
+		return kMiniscriptInstructionOutcomeFailed;
+	case kGlobalRefTicks:
+		value.setInt(thread->getRuntime()->getPlayTime() * 60 / 1000);
+		break;
+	case kGlobalRefSharedScene:
+		value.setObject(thread->getRuntime()->getActiveSharedScene());
+		break;
+	case kGlobalRefActiveScene:
+		value.setObject(thread->getRuntime()->getActiveMainScene());
+		break;
+	default:
+		assert(false);
+		thread->error("Unknown global ref type");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	if (_isLValue)
+		thread->pushLValue(value);
+	else
+		thread->pushRValue(value);
+
+	return kMiniscriptInstructionOutcomeContinue;
 }
 
-size_t PushGlobal::getReferenceSetIndex() const {
-	return _refSetIndex;
+MiniscriptInstructionOutcome PushGlobal::executeFindFilteredParent(MiniscriptThread *thread) const {
+	Common::WeakPtr<RuntimeObject> ref = thread->getModifier()->getSelfReference();
+	for (;;) {
+		Common::SharedPtr<RuntimeObject> obj = ref.lock();
+		if (!obj)
+			break;
+
+		bool isMatch = false;
+		switch (_globalID) {
+		case kGlobalRefElement:
+			isMatch = obj->isElement();
+			break;
+		case kGlobalRefSection:
+			isMatch = obj->isSection();
+			break;
+		case kGlobalRefSubsection:
+			isMatch = obj->isSubsection();
+			break;
+		case kGlobalRefScene:
+			// FIXME: Need better detection of scenes
+			isMatch = obj->isElement() && static_cast<Element *>(obj.get())->getParent()->isSubsection();
+			break;
+		case kGlobalRefProject:
+			isMatch = obj->isProject();
+			break;
+		default:
+			break;
+		};
+
+		if (isMatch)
+			break;
+		else if (obj->isElement()) {
+			ref = static_cast<Element *>(obj.get())->getParent()->getSelfReference();
+		} else if (obj->isModifier()) {
+			ref = static_cast<Modifier *>(obj.get())->getParent();
+		} else {
+			ref.reset();
+			break;
+		}
+	}
+
+	DynamicValue value;
+	value.setObject(ref);
+
+	if (_isLValue)
+		thread->pushLValue(value);
+	else
+		thread->pushRValue(value);
+
+	return kMiniscriptInstructionOutcomeContinue;
 }
 
 PushString::PushString(const Common::String &str) : _str(str) {
@@ -492,4 +610,123 @@ Jump::Jump(uint32 instrOffset, bool isConditional) : _instrOffset(instrOffset),
 
 } // End of namespace MiniscriptInstructions
 
+MiniscriptThread::MiniscriptThread(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msgProps, const Common::SharedPtr<MiniscriptProgram> &program, const Common::SharedPtr<MiniscriptReferences> &refs, Modifier *modifier)
+	: _runtime(runtime), _msgProps(msgProps), _program(program), _refs(refs), _modifier(modifier), _currentInstruction(0), _failed(false) {
+}
+
+void MiniscriptThread::runOnVThread(VThread &vthread, const Common::SharedPtr<MiniscriptThread> &thread) {
+	ResumeTaskData *taskData = vthread.pushTask(resumeTask);
+	taskData->thread = thread;
+}
+
+void MiniscriptThread::error(const Common::String &message) {
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	_runtime->debugGetDebugger()->notify(kDebugSeverityError, Common::String("Miniscript error: " + message));
+#endif
+	warning("Miniscript error: %s", message.c_str());
+
+	// This should be redundant
+	_failed = true;
+}
+
+const Common::SharedPtr<MiniscriptProgram> &MiniscriptThread::getProgram() const {
+	return _program;
+}
+
+const Common::SharedPtr<MiniscriptReferences> &MiniscriptThread::getRefs() const {
+	return _refs;
+}
+
+Modifier *MiniscriptThread::getModifier() const {
+	return _modifier;
+}
+
+const Common::SharedPtr<MessageProperties>& MiniscriptThread::getMessageProperties() const {
+	return _msgProps;
+}
+
+Runtime *MiniscriptThread::getRuntime() const {
+	return _runtime;
+}
+
+void MiniscriptThread::pushLValue(const DynamicValue &value) {
+	_stack.push_back(StackValue());
+
+	StackValue &stackValue = _stack.back();
+	stackValue.value = value;
+	stackValue.type = kStackValueTypeLValue;
+	stackValue.attribIndex = 0;
+}
+
+void MiniscriptThread::pushRValue(const DynamicValue &value) {
+	_stack.push_back(StackValue());
+
+	StackValue &stackValue = _stack.back();
+	stackValue.value = value;
+	stackValue.type = kStackValueTypeRValue;
+	stackValue.attribIndex = 0;
+}
+
+void MiniscriptThread::pushLValueAttrib(const DynamicValue &value, uint attributeIndex) {
+	_stack.push_back(StackValue());
+
+	StackValue &stackValue = _stack.back();
+	stackValue.value = value;
+	stackValue.type = kStackValueTypeLValueAttrib;
+	stackValue.attribIndex = 0;
+}
+
+void MiniscriptThread::popValues(size_t count) {
+	while (count--)
+		_stack.pop_back();
+}
+
+size_t MiniscriptThread::getStackSize() const {
+	return _stack.size();
+}
+
+MiniscriptThread::StackValue &MiniscriptThread::getStackValueFromTop(size_t offset) {
+	assert(offset < _stack.size());
+	return _stack[_stack.size() - 1 - offset];
+}
+
+VThreadState MiniscriptThread::resumeTask(const ResumeTaskData &data) {
+	return data.thread->resume(data);
+}
+
+VThreadState MiniscriptThread::resume(const ResumeTaskData &taskData) {
+	const Common::Array<MiniscriptInstruction *> &instrsArray = _program->getInstructions();
+
+	if (instrsArray.size() == 0)
+		return kVThreadReturn;
+
+	MiniscriptInstruction *const *instrs = &instrsArray[0];
+	size_t numInstrs = instrsArray.size();
+
+	if (_currentInstruction >= numInstrs || _failed)
+		return kVThreadReturn;
+
+	// Requeue now so that any VThread tasks queued by instructions run in front of the resume
+	{
+		ResumeTaskData *requeueData = _runtime->getVThread().pushTask(resumeTask);
+		requeueData->thread = taskData.thread;
+	}
+
+	while (_currentInstruction < numInstrs && !_failed) {
+		MiniscriptInstruction *instr = instrs[_currentInstruction++];
+
+		MiniscriptInstructionOutcome outcome = instr->execute(this);
+		if (outcome == kMiniscriptInstructionOutcomeFailed) {
+			// Should this also interrupt the message dispatch?
+			_failed = true;
+			return kVThreadReturn;
+		}
+
+		if (outcome == kMiniscriptInstructionOutcomeYieldToVThread)
+			return kVThreadReturn;
+	}
+
+	return kVThreadReturn;
+}
+
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/miniscript.h b/engines/mtropolis/miniscript.h
index 934a79b085c..ee7a9f71475 100644
--- a/engines/mtropolis/miniscript.h
+++ b/engines/mtropolis/miniscript.h
@@ -27,12 +27,21 @@
 
 namespace MTropolis {
 
+class MiniscriptThread;
 struct MiniscriptVM;
 struct IMiniscriptInstructionFactory;
 
+enum MiniscriptInstructionOutcome {
+	kMiniscriptInstructionOutcomeContinue,			// Continue executing next instruction
+	kMiniscriptInstructionOutcomeYieldToVThread,	// Instruction pushed a VThread task
+	kMiniscriptInstructionOutcomeFailed,			// Instruction errored
+};
+
 class MiniscriptInstruction {
 public:
 	virtual ~MiniscriptInstruction();
+
+	virtual MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const = 0;
 };
 
 class MiniscriptReferences {
@@ -40,17 +49,18 @@ public:
 	struct LocalRef {
 		uint32 guid;
 		Common::String name;
+		Common::WeakPtr<RuntimeObject> resolution;
 	};
 
-	struct GlobalRef {
-		uint32 guid;
-	};
+	explicit MiniscriptReferences(const Common::Array<LocalRef> &localRefs);
+
+	void linkInternalReferences(ObjectLinkingScope *scope);
 
-	MiniscriptReferences(const Common::Array<LocalRef> &localRefs, const Common::Array<GlobalRef> &globalRefs);
+	Common::WeakPtr<RuntimeObject> getRefByIndex(uint index) const;
 
 private:
 	Common::Array<LocalRef> _localRefs;
-	Common::Array<GlobalRef> _globalRefs;
+
 };
 
 class MiniscriptProgram {
@@ -63,6 +73,8 @@ public:
 	MiniscriptProgram(const Common::SharedPtr<Common::Array<uint8> > &programData, const Common::Array<MiniscriptInstruction *> &instructions, const Common::Array<Attribute> &attributes);
 	~MiniscriptProgram();
 
+	const Common::Array<MiniscriptInstruction *> &getInstructions() const;
+
 private:
 	Common::SharedPtr<Common::Array<uint8> > _programData;
 	Common::Array<MiniscriptInstruction *> _instructions;
@@ -78,6 +90,8 @@ public:
 
 namespace MiniscriptInstructions {
 	class UnimplementedInstruction : public MiniscriptInstruction {
+	private:
+		virtual MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
 	};
 
 	class Set : public UnimplementedInstruction {
@@ -201,7 +215,7 @@ namespace MiniscriptInstructions {
 	class ListCreate : public UnimplementedInstruction {
 	};
 
-	class PushValue : public UnimplementedInstruction {
+	class PushValue : public MiniscriptInstruction {
 	public:
 		enum DataType {
 			kDataTypeNull,
@@ -220,6 +234,8 @@ namespace MiniscriptInstructions {
 		PushValue(DataType dataType, const void *value, bool isLValue);
 
 	private:
+		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
+
 		union ValueUnion {
 			bool b;
 			double f;
@@ -232,17 +248,30 @@ namespace MiniscriptInstructions {
 		bool _isLValue;
 	};
 
-	class PushGlobal : public UnimplementedInstruction {
+	class PushGlobal : public MiniscriptInstruction {
 	public:
-		explicit PushGlobal(uint32 guid, bool isLValue);
+		explicit PushGlobal(uint32 globalID, bool isLValue);
 
-		uint32 getStaticGUID() const;
-		void setReferenceSetIndex(size_t refSetIndex);
-		size_t getReferenceSetIndex() const;
+		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
 
 	private:
-		uint32 _guid;
-		size_t _refSetIndex;
+		enum {
+			kGlobalRefElement = 1,
+			kGlobalRefSubsection = 2,
+			kGlobalRefSource = 3,
+			kGlobalRefIncomingData = 4,
+			kGlobalRefMouse = 5,
+			kGlobalRefTicks = 6,
+			kGlobalRefScene = 7,
+			kGlobalRefSharedScene = 8,
+			kGlobalRefSection = 9,
+			kGlobalRefProject = 10,
+			kGlobalRefActiveScene = 11,
+		};
+
+		MiniscriptInstructionOutcome executeFindFilteredParent(MiniscriptThread *thread) const;
+
+		uint32 _globalID;
 		bool _isLValue;
 	};
 
@@ -264,6 +293,59 @@ namespace MiniscriptInstructions {
 	};
 } // End of namespace MiniscriptInstructions
 
+class MiniscriptThread {
+public:
+	enum StackValueType {
+		kStackValueTypeLValue,
+		kStackValueTypeRValue,
+		kStackValueTypeLValueAttrib,
+	};
+
+	struct StackValue {
+		DynamicValue value;
+
+		StackValueType type;
+		uint attribIndex;
+	};
+
+	MiniscriptThread(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msgProps, const Common::SharedPtr<MiniscriptProgram> &program, const Common::SharedPtr<MiniscriptReferences> &refs, Modifier *modifier);
+
+	static void runOnVThread(VThread &vthread, const Common::SharedPtr<MiniscriptThread> &thread);
+
+	void error(const Common::String &message);
+
+	const Common::SharedPtr<MiniscriptProgram> &getProgram() const;
+	const Common::SharedPtr<MiniscriptReferences> &getRefs() const;
+	Modifier *getModifier() const;
+	const Common::SharedPtr<MessageProperties> &getMessageProperties() const;
+	Runtime *getRuntime() const;
+
+	void pushLValue(const DynamicValue &value);
+	void pushRValue(const DynamicValue &value);
+	void pushLValueAttrib(const DynamicValue &value, uint attributeIndex);
+	void popValues(size_t count);
+	size_t getStackSize() const;
+	StackValue &getStackValueFromTop(size_t offset);
+
+private:
+	struct ResumeTaskData {
+		Common::SharedPtr<MiniscriptThread> thread;
+	};
+
+	static VThreadState resumeTask(const ResumeTaskData &data);
+	VThreadState resume(const ResumeTaskData &data);
+
+	Common::SharedPtr<MiniscriptProgram> _program;
+	Common::SharedPtr<MiniscriptReferences> _refs;
+	Common::SharedPtr<MessageProperties> _msgProps;
+	Modifier *_modifier;
+	Runtime *_runtime;
+	Common::Array<StackValue> _stack;
+
+	size_t _currentInstruction;
+	bool _failed;
+};
+
 } // End of namespace MTropolis
 
 #endif
diff --git a/engines/mtropolis/modifier_factory.cpp b/engines/mtropolis/modifier_factory.cpp
index 0d79bbc5048..8221d07b8f2 100644
--- a/engines/mtropolis/modifier_factory.cpp
+++ b/engines/mtropolis/modifier_factory.cpp
@@ -46,6 +46,8 @@ Common::SharedPtr<Modifier> ModifierFactory<TModifier, TModifierData>::createMod
 
 	if (!modifier->load(context, static_cast<const TModifierData &>(dataObject)))
 		modifier.reset();
+	else
+		modifier->setSelfReference(modifier);
 
 	return Common::SharedPtr<Modifier>(modifier);
 }
diff --git a/engines/mtropolis/modifier_factory.h b/engines/mtropolis/modifier_factory.h
index d4c1786b423..ca63b957009 100644
--- a/engines/mtropolis/modifier_factory.h
+++ b/engines/mtropolis/modifier_factory.h
@@ -76,8 +76,10 @@ Common::SharedPtr<Modifier> PlugInModifierFactory<TModifier, TModifierData>::cre
 
 	PlugInModifierLoaderContext plugInContext(context, plugInModifierData, &_plugIn);
 
-	if (!modifier->load(plugInContext, static_cast<const TModifierData &>(*plugInModifierData.plugInData.get())))
+	if (!static_cast<Modifier *>(modifier.get())->loadPlugInHeader(plugInContext) || !modifier->load(plugInContext, static_cast<const TModifierData &>(*plugInModifierData.plugInData.get())))
 		modifier.reset();
+	else
+		modifier->setSelfReference(modifier);
 
 	return Common::SharedPtr<Modifier>(modifier);
 }
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 029dabb0b35..1e5ee43b6e7 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -53,6 +53,7 @@ const Common::Array<Common::SharedPtr<Modifier> > &BehaviorModifier::getModifier
 
 void BehaviorModifier::appendModifier(const Common::SharedPtr<Modifier> &modifier) {
 	_children.push_back(modifier);
+	modifier->setParent(getSelfReference());
 }
 
 IModifierContainer* BehaviorModifier::getChildContainer() {
@@ -81,6 +82,19 @@ bool MiniscriptModifier::load(ModifierLoaderContext &context, const Data::Minisc
 	return true;
 }
 
+bool MiniscriptModifier::respondsToEvent(const Event &evt) const {
+	return _enableWhen.respondsTo(evt);
+}
+
+VThreadState MiniscriptModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	if (_enableWhen.respondsTo(msg->getEvent())) {
+		Common::SharedPtr<MiniscriptThread> thread(new MiniscriptThread(runtime, msg, _program, _references, this));
+		MiniscriptThread::runOnVThread(runtime->getVThread(), thread);
+	}
+
+	return kVThreadReturn;
+}
+
 Common::SharedPtr<Modifier> MiniscriptModifier::shallowClone() const {
 	MiniscriptModifier *clonePtr = new MiniscriptModifier(*this);
 	Common::SharedPtr<Modifier> clone(clonePtr);
@@ -91,6 +105,10 @@ Common::SharedPtr<Modifier> MiniscriptModifier::shallowClone() const {
 	return clone;
 }
 
+void MiniscriptModifier::linkInternalReferences(ObjectLinkingScope* scope) {
+	_references->linkInternalReferences(scope);
+}
+
 bool MessengerModifier::load(ModifierLoaderContext &context, const Data::MessengerModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -518,6 +536,7 @@ const Common::Array<Common::SharedPtr<Modifier> > &CompoundVariableModifier::get
 
 void CompoundVariableModifier::appendModifier(const Common::SharedPtr<Modifier>& modifier) {
 	_children.push_back(modifier);
+	modifier->setParent(getSelfReference());
 }
 
 void CompoundVariableModifier::visitInternalReferences(IStructuralReferenceVisitor *visitor) {
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 169759f3d18..1abb240dd30 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -30,6 +30,7 @@ namespace MTropolis {
 struct ModifierLoaderContext;
 class MiniscriptProgram;
 class MiniscriptReferences;
+class MiniscriptThread;
 
 class BehaviorModifier : public Modifier, public IModifierContainer {
 public:
@@ -58,12 +59,17 @@ class MiniscriptModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::MiniscriptModifier &data);
 
+	bool respondsToEvent(const Event &evt) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Miniscript Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusPartial; }
 #endif
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	void linkInternalReferences(ObjectLinkingScope *scope) override;
 
 	Event _enableWhen;
 
diff --git a/engines/mtropolis/notes.txt b/engines/mtropolis/notes.txt
index 7bda07dfdb1..e947bc19513 100644
--- a/engines/mtropolis/notes.txt
+++ b/engines/mtropolis/notes.txt
@@ -12,8 +12,7 @@ formation of the scene structure.
 
 The third step is "materialization" which converts loaded objects into a
 ready-to-use state.  This involves replacing any non-variable aliases with a
-copy of the original, assigning new GUIDs to the new objects, and remapping
-any internal references to the corresponding visible objects.
+copy of the original, and assigning new GUIDs to the new objects.
 
 Objects are only ever materialized once.  Aliasable variables in the global
 modifier table are materialized when the project is loaded, while everything
@@ -24,6 +23,13 @@ again, instead they should be fixed up using an object reference remap table,
 shallowClone, and visitInternalReferences.  Cloning is not currently supported
 because Obsidian doesn't use it.
 
+An important aspect of this loading process that GUIDs are resolved in the
+scope of where they are inserted.  This is necessary because mTropolis allows
+objects to be converted into aliases anywhere that they occur and will NOT
+do anything to patch up GUID references from where they were, so the GUID
+needs to be resolvable from the location that the modifier exists.
+
+
 
 Scene transitions:
 
@@ -61,3 +67,20 @@ This has a bunch of confirmed broken cases:
   doubled-up events, modifiers not working, and other chaos.  Currently the
   mTropolis engine errors out if you attempt this.
 - Probably a bunch of other cases.
+
+
+Object reference liveness:
+
+A lot of things internally go off of an assumption that structural objects and
+modifiers are NEVER deleted unless the VThread task queue is empty.  Basically that
+means that if any messages are queued or any actions are in the middle of being
+performed, then objects may not be deleted.  The only thing that can delete an
+object is a scheduled Teardown.
+
+Currently this is unused, but mTropolis supports a "kill" event type which will
+remove an object when sent to it, and a "kill" attribute that removes an object
+when it is assigned to.
+
+A big implication of this is that it's okay to use raw pointers to scene objects
+in VThread tasks.  In particular, tasks calling member functions are OK to
+schedule.
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 8aeea9908d2..31c3a381734 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -41,6 +41,38 @@
 
 namespace MTropolis {
 
+class ModifierInnerScopeBuilder : public IStructuralReferenceVisitor {
+public:
+	ModifierInnerScopeBuilder(ObjectLinkingScope *scope);
+
+	void visitChildStructuralRef(Common::SharedPtr<Structural> &structural) override;
+	void visitChildModifierRef(Common::SharedPtr<Modifier> &modifier) override;
+	void visitWeakStructuralRef(Common::SharedPtr<Structural> &structural) override;
+	void visitWeakModifierRef(Common::SharedPtr<Modifier> &modifier) override;
+
+private:
+	ObjectLinkingScope *_scope;
+};
+
+ModifierInnerScopeBuilder::ModifierInnerScopeBuilder(ObjectLinkingScope *scope) : _scope(scope) {
+}
+
+void ModifierInnerScopeBuilder::visitChildStructuralRef(Common::SharedPtr<Structural> &structural) {
+	assert(false);
+}
+
+void ModifierInnerScopeBuilder::visitChildModifierRef(Common::SharedPtr<Modifier> &modifier) {
+	_scope->addObject(modifier->getStaticGUID(), modifier->getName(), modifier);
+}
+
+void ModifierInnerScopeBuilder::visitWeakStructuralRef(Common::SharedPtr<Structural> &structural) {
+	assert(false);
+}
+
+void ModifierInnerScopeBuilder::visitWeakModifierRef(Common::SharedPtr<Modifier> &modifier) {
+	// Do nothing
+}
+
 class ModifierChildMaterializer : public IStructuralReferenceVisitor {
 public:
 	ModifierChildMaterializer(Runtime *runtime, ObjectLinkingScope *outerScope);
@@ -75,6 +107,26 @@ void ModifierChildMaterializer::visitWeakModifierRef(Common::SharedPtr<Modifier>
 	// Do nothing
 }
 
+Common::String toCaseInsensitive(const Common::String &str) {
+	uint strLen = str.size();
+	if (strLen == 0)
+		return str;
+
+	// TODO: Figure out how this is supposed to behave with respect to non-ASCII characters
+	Common::Array<char> lowered;
+	lowered.resize(strLen);
+
+	for (uint i = 0; i < strLen; i++) {
+		char c = str[i];
+		if (c >= 'A' && c <= 'Z') {
+			c = static_cast<char>(c - 'A' + 'a');
+		}
+		lowered[i] = c;
+	}
+
+	return Common::String(&lowered[0], strLen);
+}
+
 bool Point16::load(const Data::Point &point) {
 	x = point.x;
 	y = point.y;
@@ -159,7 +211,10 @@ void DynamicListDefaultSetter::defaultSet(Event &value) {
 void DynamicListDefaultSetter::defaultSet(Common::String &value) {
 }
 
-void DynamicListDefaultSetter::defaultSet(DynamicList &value) {
+void DynamicListDefaultSetter::defaultSet(Common::SharedPtr<DynamicList> &value) {
+}
+
+void DynamicListDefaultSetter::defaultSet(Common::WeakPtr<RuntimeObject> &value) {
 }
 
 bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const int32 *&outPtr) {
@@ -225,13 +280,20 @@ bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const C
 	return true;
 }
 
-bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const DynamicList *&outPtr) {
-	if (dynValue.getType() != DynamicValueTypes::kBoolean)
+bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const Common::SharedPtr<DynamicList> *&outPtr) {
+	if (dynValue.getType() != DynamicValueTypes::kList)
 		return false;
 	outPtr = &dynValue.getList();
 	return true;
 }
 
+bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const Common::WeakPtr<RuntimeObject> *&outPtr) {
+	if (dynValue.getType() != DynamicValueTypes::kObject)
+		return false;
+	outPtr = &dynValue.getObject();
+	return true;
+}
+
 DynamicListContainer<void>::DynamicListContainer() : _size(0) {
 }
 
@@ -395,14 +457,8 @@ bool DynamicList::setAtIndex(size_t index, const DynamicValue &value) {
 
 DynamicList &DynamicList::operator=(const DynamicList &other) {
 	if (this != &other) {
-		if (_type == DynamicValueTypes::kList && other._type == DynamicValueTypes::kList) {
-			// In this case, one operand may be inside of the other operand, so we need to copy instead of clear
-			DynamicList listClone(*this);
-			swap(listClone);
-		} else {
-			clear();
-			initFromOther(other);
-		}
+		clear();
+		initFromOther(other);
 	}
 
 	return *this;
@@ -476,7 +532,10 @@ bool DynamicList::changeToType(DynamicValueTypes::DynamicValueType type) {
 		_container = new DynamicListContainer<Common::String>();
 		break;
 	case DynamicValueTypes::kList:
-		_container = new DynamicListContainer<DynamicList>();
+		_container = new DynamicListContainer<Common::SharedPtr<DynamicList> >();
+		break;
+	case DynamicValueTypes::kObject:
+		_container = new DynamicListContainer<Common::WeakPtr<RuntimeObject> >();
 		break;
 	}
 
@@ -678,19 +737,107 @@ const bool &DynamicValue::getBool() const {
 	return _value.asBool;
 }
 
-const DynamicList &DynamicValue::getList() const {
+const Common::SharedPtr<DynamicList> &DynamicValue::getList() const {
 	assert(_type == DynamicValueTypes::kList);
-	return *_value.asList;
+	return _list;
 }
 
-void DynamicValue::swap(DynamicValue &other) {
-	DynamicValueTypes::DynamicValueType tempType = _type;
-	_type = other._type;
-	other._type = tempType;
+const Common::WeakPtr<RuntimeObject> &DynamicValue::getObject() const {
+	assert(_type == DynamicValueTypes::kObject);
+	return _obj;
+}
+
+void DynamicValue::setInt(int32 value) {
+	if (_type != DynamicValueTypes::kInteger)
+		clear();
+	_type = DynamicValueTypes::kInteger;
+	_value.asInt = value;
+}
+
+void DynamicValue::setFloat(double value) {
+	if (_type != DynamicValueTypes::kFloat)
+		clear();
+	_type = DynamicValueTypes::kFloat;
+	_value.asFloat = value;
+}
+
+void DynamicValue::setPoint(const Point16 &value) {
+	if (_type != DynamicValueTypes::kPoint)
+		clear();
+	_type = DynamicValueTypes::kPoint;
+	_value.asPoint = value;
+}
+
+void DynamicValue::setIntRange(const IntRange &value) {
+	if (_type != DynamicValueTypes::kIntegerRange)
+		clear();
+	_type = DynamicValueTypes::kIntegerRange;
+	_value.asIntRange = value;
+}
+
+void DynamicValue::setVector(const AngleMagVector &value) {
+	if (_type != DynamicValueTypes::kVector)
+		clear();
+	_type = DynamicValueTypes::kVector;
+	_value.asVector = value;
+}
+
+void DynamicValue::setLabel(const Label &value) {
+	if (_type != DynamicValueTypes::kLabel)
+		clear();
+	_type = DynamicValueTypes::kLabel;
+	_value.asLabel = value;
+}
+
+void DynamicValue::setEvent(const Event &value) {
+	if (_type != DynamicValueTypes::kEvent)
+		clear();
+	_type = DynamicValueTypes::kEvent;
+	_value.asEvent = value;
+}
+
+void DynamicValue::setVarReference(const VarReference &value) {
+	if (_type != DynamicValueTypes::kVariableReference)
+		clear();
+	_type = DynamicValueTypes::kVariableReference;
+	_value.asVarReference.guid = value.guid;
+	_value.asVarReference.source = &_str;
+	_str = *value.source;
+}
+
+void DynamicValue::setString(const Common::String &value) {
+	if (_type != DynamicValueTypes::kString)
+		clear();
+	_type = DynamicValueTypes::kString;
+	_str = value;
+}
 
-	Common::String tempStr = _str;
-	_str = other._str;
-	other._str = tempStr;
+void DynamicValue::setBool(bool value) {
+	if (_type != DynamicValueTypes::kBoolean)
+		clear();
+	_type = DynamicValueTypes::kBoolean;
+	_value.asBool = value;
+}
+
+void DynamicValue::setList(const Common::SharedPtr<DynamicList> &value) {
+	if (_type != DynamicValueTypes::kList)
+		clear();
+	_type = DynamicValueTypes::kList;
+	_list = value;
+}
+
+void DynamicValue::setObject(const Common::WeakPtr<RuntimeObject> &value) {
+	if (_type != DynamicValueTypes::kObject)
+		clear();
+	_type = DynamicValueTypes::kObject;
+	_obj = value;
+}
+
+void DynamicValue::swap(DynamicValue &other) {
+	internalSwap(_type, other._type);
+	internalSwap(_str, other._str);
+	internalSwap(_list, other._list);
+	internalSwap(_obj, other._obj);
 
 	ValueUnion tempValue;
 	memcpy(&tempValue, &_value, sizeof(ValueUnion));
@@ -740,7 +887,9 @@ bool DynamicValue::operator==(const DynamicValue &other) const {
 	case DynamicValueTypes::kBoolean:
 		return _value.asBool == other._value.asBool;
 	case DynamicValueTypes::kList:
-		return (*_value.asList) == (*other._value.asList);
+		return (*_list.get()) == (*other._list.get());
+	case DynamicValueTypes::kObject:
+		return _obj == other._obj;
 	default:
 		break;
 	}
@@ -750,9 +899,8 @@ bool DynamicValue::operator==(const DynamicValue &other) const {
 }
 
 void DynamicValue::clear() {
-	if (_type == DynamicValueTypes::kList)
-		delete _value.asList;
-
+	_list.reset();
+	_obj.reset();
 	_str.clear();
 	_type = DynamicValueTypes::kNull;
 }
@@ -797,7 +945,10 @@ void DynamicValue::initFromOther(const DynamicValue &other) {
 		_value.asBool = other._value.asBool;
 		break;
 	case DynamicValueTypes::kList:
-		_value.asList = new DynamicList(*other._value.asList);
+		_list = other._list;
+		break;
+	case DynamicValueTypes::kObject:
+		_obj = other._obj;
 		break;
 	default:
 		assert(false);
@@ -857,6 +1008,10 @@ Event Event::create(EventIDs::EventID eventType, uint32 eventInfo) {
 	return evt;
 }
 
+bool Event::respondsTo(const Event &otherEvent) const {
+	return (*this) == otherEvent;
+}
+
 bool Event::load(const Data::Event &data) {
 	eventType = static_cast<EventIDs::EventID>(data.eventID);
 	eventInfo = data.eventInfo;
@@ -958,9 +1113,10 @@ const Common::Array<Common::SharedPtr<Modifier> >& SimpleModifierContainer::getM
 
 void SimpleModifierContainer::appendModifier(const Common::SharedPtr<Modifier> &modifier) {
 	_modifiers.push_back(modifier);
+	if (modifier)
+		modifier->setParent(nullptr);
 }
 
-
 RuntimeObject::RuntimeObject() : _guid(0), _runtimeGUID(0) {
 }
 
@@ -979,6 +1135,33 @@ void RuntimeObject::setRuntimeGUID(uint32 runtimeGUID) {
 	_runtimeGUID = runtimeGUID;
 }
 
+void RuntimeObject::setSelfReference(const Common::WeakPtr<RuntimeObject> &selfReference) {
+	_selfReference = selfReference;
+}
+
+const Common::WeakPtr<RuntimeObject>& RuntimeObject::getSelfReference() const {
+	return _selfReference;
+}
+
+bool RuntimeObject::isProject() const {
+	return false;
+}
+
+bool RuntimeObject::isSection() const {
+	return false;
+}
+
+bool RuntimeObject::isSubsection() const {
+	return false;
+}
+
+bool RuntimeObject::isModifier() const {
+	return false;
+}
+
+bool RuntimeObject::isElement() const {
+	return false;
+}
 
 MessageProperties::MessageProperties(const Event &evt, const DynamicValue &value, const Common::WeakPtr<RuntimeObject> &source)
 	: _evt(evt), _value(value), _source(source) {
@@ -1038,7 +1221,7 @@ void Structural::removeChild(Structural* child) {
 	}
 }
 
-Structural* Structural::getParent() const {
+Structural *Structural::getParent() const {
 	return _parent;
 }
 
@@ -1056,13 +1239,14 @@ const Common::Array<Common::SharedPtr<Modifier> > &Structural::getModifiers() co
 
 void Structural::appendModifier(const Common::SharedPtr<Modifier> &modifier) {
 	_modifiers.push_back(modifier);
+	modifier->setParent(getSelfReference());
 }
 
 bool Structural::respondsToEvent(const Event &evt) const {
 	return false;
 }
 
-VThreadState Structural::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) const {
+VThreadState Structural::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
 	assert(false);
 	return kVThreadError;
 }
@@ -1115,7 +1299,7 @@ void Structural::materializeDescendents(Runtime *runtime, ObjectLinkingScope *ou
 			(*it) = clonedModifier;
 			modifier = clonedModifier.get();
 		}
-		modifierScope->addObject(modifierGUID, *it);
+		modifierScope->addObject(modifierGUID, modifier->getName(), *it);
 	}
 
 	for (Common::Array<Common::SharedPtr<Modifier> >::const_iterator it = _modifiers.begin(), itEnd = _modifiers.end(); it != itEnd; ++it) {
@@ -1128,7 +1312,7 @@ void Structural::materializeDescendents(Runtime *runtime, ObjectLinkingScope *ou
 	const Common::Array<Common::SharedPtr<Structural> > &children = this->getChildren();
 	for (Common::Array<Common::SharedPtr<Structural> >::const_iterator it = children.begin(), itEnd = children.end(); it != itEnd; ++it) {
 		Structural *child = it->get();
-		structuralScope->addObject(child->getStaticGUID(), *it);
+		structuralScope->addObject(child->getStaticGUID(), child->getName(), *it);
 	}
 
 	for (Common::Array<Common::SharedPtr<Structural> >::const_iterator it = children.begin(), itEnd = children.end(); it != itEnd; ++it) {
@@ -1180,8 +1364,56 @@ void ObjectLinkingScope::setParent(ObjectLinkingScope *parent) {
 	_parent = parent;
 }
 
-void ObjectLinkingScope::addObject(uint32 guid, const Common::WeakPtr<RuntimeObject> &object) {
+void ObjectLinkingScope::addObject(uint32 guid, const Common::String &name, const Common::WeakPtr<RuntimeObject> &object) {
 	_guidToObject[guid] = object;
+
+	if (name.size() > 0)
+		_nameToObject[toCaseInsensitive(name)] = object;
+}
+
+Common::WeakPtr<RuntimeObject> ObjectLinkingScope::resolve(uint32 staticGUID) const {
+	if (staticGUID == 0)
+		return Common::WeakPtr<RuntimeObject>();
+
+	Common::HashMap<uint32, Common::WeakPtr<RuntimeObject> >::const_iterator it = _guidToObject.find(staticGUID);
+	if (it != _guidToObject.end()) {
+		return it->_value;
+	} else {
+		if (_parent)
+			return _parent->resolve(staticGUID);
+
+		warning("Couldn't resolve static GUID %x", staticGUID);
+		return Common::WeakPtr<RuntimeObject>();
+	}
+}
+
+Common::WeakPtr<RuntimeObject> ObjectLinkingScope::resolve(const Common::String &name, bool isNameAlreadyInsensitive) const {
+	const Common::String *namePtr = &name;
+	Common::String madeInsensitive;
+
+	if (!isNameAlreadyInsensitive) {
+		madeInsensitive = toCaseInsensitive(name);
+		namePtr = &madeInsensitive;
+	}
+
+	Common::HashMap<Common::String, Common::WeakPtr<RuntimeObject> >::const_iterator it = _nameToObject.find(*namePtr);
+	if (it != _nameToObject.end()) {
+		return it->_value;
+	} else {
+		if (_parent)
+			return _parent->resolve(*namePtr, true);
+
+		warning("Couldn't resolve object name '%s'", name.c_str());
+		return Common::WeakPtr<RuntimeObject>();
+	}
+}
+
+Common::WeakPtr<RuntimeObject> ObjectLinkingScope::resolve(uint32 staticGUID, const Common::String &name, bool isNameAlreadyInsensitive) const {
+	Common::WeakPtr<RuntimeObject> byGUIDResult = resolve(staticGUID);
+	if (byGUIDResult)
+		return byGUIDResult;
+	else
+		return resolve(name, isNameAlreadyInsensitive);
 }
 
 void ObjectLinkingScope::reset() {
@@ -1408,10 +1640,11 @@ void Runtime::runFrame(uint32 msec) {
 			_macFontMan.reset(new Graphics::MacFontManager(0, desc->getLanguage()));
 
 			_project.reset(new Project());
+			_project->setSelfReference(_project);
 
 			_project->loadFromDescription(*desc);
 
-			_rootLinkingScope.addObject(_project->getStaticGUID(), _project);
+			_rootLinkingScope.addObject(_project->getStaticGUID(), _project->getName(), _project);
 
 			// We have to materialize global variables because they are not cloned from aliases.
 			debug(1, "Materializing global variables...");
@@ -1529,8 +1762,6 @@ void Runtime::drawFrame(OSystem* system) {
 			continue;
 
 		system->copyRectToScreen(surface.getBasePtr(srcLeft, srcTop), surface.pitch, destLeft, destTop, destRight - destLeft, destBottom - destTop);
-
-		int n = 0;
 	}
 
 	system->updateScreen();
@@ -1538,6 +1769,7 @@ void Runtime::drawFrame(OSystem* system) {
 
 Common::SharedPtr<Structural> Runtime::findDefaultSharedSceneForScene(Structural *scene) {
 	Structural *subsection = scene->getParent();
+
 	const Common::Array<Common::SharedPtr<Structural> > &children = subsection->getChildren();
 	if (children.size() == 0 || children[0].get() == scene)
 		return Common::SharedPtr<Structural>();
@@ -1773,6 +2005,7 @@ void Runtime::executeHighLevelSceneTransition(const HighLevelSceneTransition &tr
 
 void Runtime::executeSharedScenePostSceneChangeActions() {
 	Structural *subsection = _activeMainScene->getParent();
+
 	const Common::Array<Common::SharedPtr<Structural> > &subsectionScenes = subsection->getChildren();
 
 	_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSharedSceneSceneChanged, 0), _activeSharedScene.get(), false, true))));
@@ -1964,6 +2197,10 @@ uint64 Runtime::getPlayTime() const {
 	return _playTime;
 }
 
+VThread& Runtime::getVThread() const {
+	return *_vthread.get();
+}
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 
 void Runtime::debugSetEnabled(bool enabled) {
@@ -2217,6 +2454,10 @@ const ProjectPresentationSettings& Project::getPresentationSettings() const {
 	return _presentationSettings;
 }
 
+bool Project::isProject() const {
+	return true;
+}
+
 void Project::openSegmentStream(int segmentIndex) {
 	if (segmentIndex < 0 || static_cast<size_t>(segmentIndex) > _segments.size()) {
 		error("Invalid segment index %i", segmentIndex);
@@ -2500,6 +2741,8 @@ void Project::loadContextualObject(ChildLoaderStack &stack, const Data::DataObje
 				const Data::SectionStructuralDef &sectionObject = static_cast<const Data::SectionStructuralDef &>(dataObject);
 
 				Common::SharedPtr<Structural> section(new Section());
+				section->setSelfReference(section);
+
 				if (!static_cast<Section *>(section.get())->load(sectionObject))
 					error("Failed to load section");
 
@@ -2534,6 +2777,8 @@ void Project::loadContextualObject(ChildLoaderStack &stack, const Data::DataObje
 				const Data::SubsectionStructuralDef &subsectionObject = static_cast<const Data::SubsectionStructuralDef &>(dataObject);
 
 				Common::SharedPtr<Structural> subsection(new Subsection());
+				subsection->setSelfReference(subsection);
+
 				if (!static_cast<Subsection *>(subsection.get())->load(subsectionObject))
 					error("Failed to load subsection");
 
@@ -2621,6 +2866,10 @@ bool Section::load(const Data::SectionStructuralDef &data) {
 	return true;
 }
 
+bool Section::isSection() const {
+	return true;
+}
+
 ObjectLinkingScope *Section::getPersistentStructuralScope() {
 	return &_structuralScope;
 }
@@ -2636,6 +2885,10 @@ bool Subsection::load(const Data::SubsectionStructuralDef &data) {
 	return true;
 }
 
+bool Subsection::isSubsection() const {
+	return true;
+}
+
 ObjectLinkingScope *Subsection::getSceneLoadMaterializeScope() {
 	return getPersistentStructuralScope();
 }
@@ -2648,6 +2901,10 @@ ObjectLinkingScope *Subsection::getPersistentModifierScope() {
 	return &_modifierScope;
 }
 
+bool Element::isElement() const {
+	return true;
+}
+
 uint32 Element::getStreamLocator() const {
 	return _streamLocator;
 }
@@ -2683,7 +2940,7 @@ bool ModifierFlags::load(const uint32 dataModifierFlags) {
 	return true;
 }
 
-Modifier::Modifier() {
+Modifier::Modifier() : _parent(nullptr) {
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	_debugger = nullptr;
 #endif
@@ -2697,10 +2954,16 @@ Modifier::~Modifier() {
 }
 
 void Modifier::materialize(Runtime *runtime, ObjectLinkingScope *outerScope) {
-	ModifierChildMaterializer childMaterializer(runtime, outerScope);
+	ObjectLinkingScope innerScope;
+	innerScope.setParent(outerScope);
+
+	ModifierInnerScopeBuilder innerScopeBuilder(&innerScope);
+	this->visitInternalReferences(&innerScopeBuilder);
+
+	ModifierChildMaterializer childMaterializer(runtime, &innerScope);
 	this->visitInternalReferences(&childMaterializer);
 
-	linkInternalReferences();
+	linkInternalReferences(outerScope);
 	setRuntimeGUID(runtime->allocateRuntimeGUID());
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
@@ -2717,6 +2980,10 @@ bool Modifier::isVariable() const {
 	return false;
 }
 
+bool Modifier::isModifier() const {
+	return true;
+}
+
 IModifierContainer *Modifier::getMessagePropagationContainer() {
 	return nullptr;
 }
@@ -2725,11 +2992,19 @@ IModifierContainer *Modifier::getChildContainer() {
 	return nullptr;
 }
 
+const Common::WeakPtr<RuntimeObject>& Modifier::getParent() const {
+	return _parent;
+}
+
+void Modifier::setParent(const Common::WeakPtr<RuntimeObject> &parent) {
+	_parent = parent;
+}
+
 bool Modifier::respondsToEvent(const Event &evt) const {
 	return false;
 }
 
-VThreadState Modifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) const {
+VThreadState Modifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
 	// If you're here, a message type was reported as responsive by respondsToEvent but consumeMessage wasn't overrided
 	assert(false);
 	return kVThreadError;
@@ -2746,6 +3021,13 @@ const Common::String& Modifier::getName() const {
 void Modifier::visitInternalReferences(IStructuralReferenceVisitor *visitor) {
 }
 
+bool Modifier::loadPlugInHeader(const PlugInModifierLoaderContext &plugInContext) {
+	_guid = plugInContext.plugInModifierData.guid;
+	_name = plugInContext.plugInModifierData.name;
+
+	return true;
+}
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 
 SupportStatus Modifier::debugGetSupportStatus() const {
@@ -2783,7 +3065,7 @@ bool Modifier::loadTypicalHeader(const Data::TypicalModifierHeader &typicalHeade
 	return true;
 }
 
-void Modifier::linkInternalReferences() {
+void Modifier::linkInternalReferences(ObjectLinkingScope *scope) {
 }
 
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 91fd7f413dc..db9bb68fa4e 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -57,6 +57,7 @@ class CursorGraphicCollection;
 class Element;
 class MessageDispatch;
 class Modifier;
+class PlugInModifier;
 class RuntimeObject;
 class PlugIn;
 class Project;
@@ -71,6 +72,11 @@ struct IPlugInModifierFactory;
 struct IPlugInModifierFactoryAndDataFactory;
 struct MessageProperties;
 struct ModifierLoaderContext;
+struct PlugInModifierLoaderContext;
+template<typename TElement, typename TElementData> class ElementFactory;
+
+
+Common::String toCaseInsensitive(const Common::String &str);
 
 enum ColorDepthMode {
 	kColorDepthMode1Bit,
@@ -103,6 +109,7 @@ enum DynamicValueType {
 	kIncomingData,
 	kString,
 	kList,
+	kObject,
 
 	kEmpty,
 };
@@ -263,6 +270,10 @@ struct Event {
 	static Event create();
 	static Event create(EventIDs::EventID eventType, uint32 eventInfo);
 
+	// Returns true if this event, interpreted as a filter, recognizes another event.
+	// Handles cases where eventInfo is ignored (hopefully).
+	bool respondsTo(const Event &otherEvent) const;
+
 	bool load(const Data::Event &data);
 
 	inline bool operator==(const Event &other) const {
@@ -339,7 +350,8 @@ struct DynamicListDefaultSetter {
 	static void defaultSet(Label &value);
 	static void defaultSet(Event &value);
 	static void defaultSet(Common::String &value);
-	static void defaultSet(DynamicList &value);
+	static void defaultSet(Common::SharedPtr<DynamicList> &value);
+	static void defaultSet(Common::WeakPtr<RuntimeObject> &value);
 };
 
 struct DynamicListValueImporter {
@@ -352,7 +364,8 @@ struct DynamicListValueImporter {
 	static bool importValue(const DynamicValue &dynValue, const Label *&outPtr);
 	static bool importValue(const DynamicValue &dynValue, const Event *&outPtr);
 	static bool importValue(const DynamicValue &dynValue, const Common::String *&outPtr);
-	static bool importValue(const DynamicValue &dynValue, const DynamicList *&outPtr);
+	static bool importValue(const DynamicValue &dynValue, const Common::SharedPtr<DynamicList> *&outPtr);
+	static bool importValue(const DynamicValue &dynValue, const Common::WeakPtr<RuntimeObject> *&outPtr);
 };
 
 template<class T>
@@ -472,6 +485,8 @@ struct DynamicList {
 
 	void swap(DynamicList &other);
 
+	Common::SharedPtr<DynamicList> clone() const;
+
 private:
 	void clear();
 	void initFromOther(const DynamicList &other);
@@ -481,6 +496,8 @@ private:
 	DynamicListContainerBase *_container;
 };
 
+// Dynamic value container.  Somewhat importantly, lists stored in dynamic values
+// are BY REFERENCE and must be cloned as necessary.
 struct DynamicValue {
 	DynamicValue();
 	DynamicValue(const DynamicValue &other);
@@ -501,7 +518,23 @@ struct DynamicValue {
 	const VarReference &getVarReference() const;
 	const Common::String &getString() const;
 	const bool &getBool() const;
-	const DynamicList &getList() const;
+	const Common::SharedPtr<DynamicList> &getList() const;
+	const Common::WeakPtr<RuntimeObject> &getObject() const;
+
+	void clear();
+
+	void setInt(int32 value);
+	void setFloat(double value);
+	void setPoint(const Point16 &value);
+	void setIntRange(const IntRange &value);
+	void setVector(const AngleMagVector &value);
+	void setLabel(const Label &value);
+	void setEvent(const Event &value);
+	void setVarReference(const VarReference &value);
+	void setString(const Common::String &value);
+	void setBool(bool value);
+	void setList(const Common::SharedPtr<DynamicList> &value);
+	void setObject(const Common::WeakPtr<RuntimeObject> &value);
 
 	DynamicValue &operator=(const DynamicValue &other);
 
@@ -523,15 +556,22 @@ private:
 		Event asEvent;
 		Point16 asPoint;
 		bool asBool;
-		DynamicList *asList;
 	};
 
-	void clear();
+	template<class T>
+	void internalSwap(T &a, T &b) {
+		T temp = a;
+		a = b;
+		b = temp;
+	}
+
 	void initFromOther(const DynamicValue &other);
 
 	DynamicValueTypes::DynamicValueType _type;
 	ValueUnion _value;
 	Common::String _str;
+	Common::SharedPtr<DynamicList> _list;
+	Common::WeakPtr<RuntimeObject> _obj;
 };
 
 struct MessengerSendSpec {
@@ -649,12 +689,16 @@ public:
 	~ObjectLinkingScope();
 
 	void setParent(ObjectLinkingScope *parent);
-	void addObject(uint32 guid, const Common::WeakPtr<RuntimeObject> &object);
+	void addObject(uint32 guid, const Common::String &name, const Common::WeakPtr<RuntimeObject> &object);
+	Common::WeakPtr<RuntimeObject> resolve(uint32 staticGUID) const;
+	Common::WeakPtr<RuntimeObject> resolve(const Common::String &name, bool isNameAlreadyInsensitive) const;
+	Common::WeakPtr<RuntimeObject> resolve(uint32 staticGUID, const Common::String &name, bool isNameAlreadyInsensitive) const;
 
 	void reset();
 
 private:
 	Common::HashMap<uint32, Common::WeakPtr<RuntimeObject> > _guidToObject;
+	Common::HashMap<Common::String, Common::WeakPtr<RuntimeObject> > _nameToObject;
 	ObjectLinkingScope *_parent;
 };
 
@@ -770,6 +814,8 @@ public:
 	uint64 getRealTime() const;
 	uint64 getPlayTime() const;
 
+	VThread &getVThread() const;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	void debugSetEnabled(bool enabled);
 	void debugBreak();
@@ -874,6 +920,9 @@ private:
 };
 
 class RuntimeObject {
+	template<typename TElement, typename TElementData>
+	friend class ElementFactory;
+
 public:
 	RuntimeObject();
 	virtual ~RuntimeObject();
@@ -883,12 +932,23 @@ public:
 
 	void setRuntimeGUID(uint32 runtimeGUID);
 
+	void setSelfReference(const Common::WeakPtr<RuntimeObject> &selfReference);
+	const Common::WeakPtr<RuntimeObject> &getSelfReference() const;
+
+	virtual bool isProject() const;
+	virtual bool isSection() const;
+	virtual bool isSubsection() const;
+	virtual bool isModifier() const;
+	virtual bool isElement() const;
+
 protected:
 	// This is the static GUID stored in the data, it is not guaranteed
 	// to be globally unique at runtime.  In particular, cloning an object
 	// and using aliased modifiers will cause multiple objects with the same
+	// static GUID to exist with separate runtime GUIDs.
 	uint32 _guid;
 	uint32 _runtimeGUID;
+	Common::WeakPtr<RuntimeObject> _selfReference;
 };
 
 struct MessageProperties {
@@ -914,7 +974,7 @@ struct IStructuralReferenceVisitor {
 struct IMessageConsumer {
 	// These should only be implemented as direct responses - child traversal is handled by the message propagation process
 	virtual bool respondsToEvent(const Event &evt) const = 0;
-	virtual VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) const = 0;
+	virtual VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) = 0;
 };
 
 class Structural : public RuntimeObject, public IModifierContainer, public IMessageConsumer, public IDebuggable {
@@ -937,7 +997,7 @@ public:
 	void appendModifier(const Common::SharedPtr<Modifier> &modifier) override;
 
 	bool respondsToEvent(const Event &evt) const override;
-	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
 
 	void materializeSelfAndDescendents(Runtime *runtime, ObjectLinkingScope *outerScope);
 	void materializeDescendents(Runtime *runtime, ObjectLinkingScope *outerScope);
@@ -1051,6 +1111,8 @@ public:
 
 	const ProjectPresentationSettings &getPresentationSettings() const;
 
+	bool isProject() const override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Project"; }
 #endif
@@ -1148,6 +1210,8 @@ class Section : public Structural {
 public:
 	bool load(const Data::SectionStructuralDef &data);
 
+	bool isSection() const;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Section"; }
 #endif
@@ -1166,6 +1230,8 @@ public:
 
 	ObjectLinkingScope *getSceneLoadMaterializeScope();
 
+	bool isSubsection() const override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Subsection"; }
 #endif
@@ -1181,6 +1247,7 @@ private:
 class Element : public Structural {
 public:
 	virtual bool isVisual() const = 0;
+	bool isElement() const override;
 
 	uint32 getStreamLocator() const;
 
@@ -1224,13 +1291,17 @@ public:
 
 	virtual bool isAlias() const;
 	virtual bool isVariable() const;
+	bool isModifier() const override;
 
 	// This should only return a propagation container if messages should actually be propagated (i.e. NOT switched-off behaviors!)
 	virtual IModifierContainer *getMessagePropagationContainer();
 	virtual IModifierContainer *getChildContainer();
 
+	const Common::WeakPtr<RuntimeObject> &getParent() const;
+	void setParent(const Common::WeakPtr<RuntimeObject> &parent);
+
 	bool respondsToEvent(const Event &evt) const override;
-	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
 
 	void setName(const Common::String &name);
 	const Common::String &getName() const;
@@ -1238,8 +1309,13 @@ public:
 	// Shallow clones only need to copy the object.  Descendent copies are done using visitInternalReferences.
 	virtual Common::SharedPtr<Modifier> shallowClone() const = 0;
 
+	// Visits any internal references in the object.
+	// Any references to other elements owned by the object MUST be SharedPtr, any references to non-owned objects
+	// MUST be WeakPtr, in order for the cloning and materialization logic to work correctly.
 	virtual void visitInternalReferences(IStructuralReferenceVisitor *visitor);
 
+	bool loadPlugInHeader(const PlugInModifierLoaderContext &plugInContext);
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	SupportStatus debugGetSupportStatus() const override;
 	const Common::String &debugGetName() const override;
@@ -1251,11 +1327,14 @@ public:
 
 protected:
 	bool loadTypicalHeader(const Data::TypicalModifierHeader &typicalHeader);
-	virtual void linkInternalReferences();
+
+	// Links any references contained in the object, resolving static GUIDs to runtime object references
+	virtual void linkInternalReferences(ObjectLinkingScope *scope);
 
 	Common::String _name;
 	ModifierFlags _modifierFlags;
-	
+	Common::WeakPtr<RuntimeObject> _parent;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	Common::SharedPtr<DebugInspector> _debugInspector;
 	Debugger *_debugger;


Commit: b6204b1f28d1dcd7425c4cc205aef4793fd388f9
    https://github.com/scummvm/scummvm/commit/b6204b1f28d1dcd7425c4cc205aef4793fd388f9
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Adjusted framerate control

Changed paths:
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index 0ef4a3926a6..14ca6becf03 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -241,6 +241,8 @@ void MTropolisEngine::handleEvents() {
 Common::Error MTropolisEngine::run() {
 	int preferredWidth = 1024;
 	int preferredHeight = 768;
+	bool enableFrameRateLimit = true;
+	uint expectedFrameRate = 60;
 	ColorDepthMode preferredColorDepthMode = kColorDepthMode8Bit;
 
 	_runtime.reset(new Runtime());
@@ -413,16 +415,92 @@ Common::Error MTropolisEngine::run() {
 	}
 #endif
 
-	uint32 prevTimeStamp = getTotalPlayTime();
+	uint32 prevTimeStamp = _system->getMillis();
 
+	int32 earliness = 0;	// In thousandths of a frame
+	const int32 aheadLimit = 5000;
+	const int32 behindLimit = -5000;
+	const int32 jitterTolerance = 500;	// Half a frame
+	const int skippedFrameLimit = 10;
+	uint32 limitRollingAccumulatedThousandthsOfFrames = 0;
+	uint32 limitRollingAccumulatedMSec = 0;
+	bool paused = false;
+
+	int frameCounter = 0;
+	int numSkippedFrames = 0;
 	while (!shouldQuit()) {
-		const uint32 frameTime = getTotalPlayTime();
-		const uint32 elapsedThisFrame = frameTime - prevTimeStamp;
+		handleEvents();
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
+		if (_runtime->debugGetDebugger())
+			paused = _runtime->debugGetDebugger()->isPaused();
+#endif
+
+		// Scheduling is a bit tricky here because the project frame can suspend mid-execution.
+		bool skipDraw = false;
+		bool forceDraw = false;
+		bool frameWasRun = false;
+		if (!paused)
+			frameWasRun = _runtime->runProjectFrame();
+
+		uint32 frameTime = _system->getMillis();
+		const uint32 realElapsedThisFrame = frameTime - prevTimeStamp;
 		prevTimeStamp = frameTime;
 
-		handleEvents();
-		_runtime->runFrame(elapsedThisFrame);
-		_runtime->drawFrame(_system);
+		if (frameWasRun) {
+			frameCounter++;
+			uint32 timeAdvance = realElapsedThisFrame;
+
+			if (enableFrameRateLimit) {
+				earliness += 1000;
+				uint32 durationInThousandsthsOfAFrame = realElapsedThisFrame * expectedFrameRate;
+				earliness -= static_cast<int32>(durationInThousandsthsOfAFrame);
+				if (earliness > aheadLimit) {
+					earliness = aheadLimit;
+				} else if (earliness < behindLimit) {
+					earliness = behindLimit;
+				}
+
+				if (earliness < -1000) {
+					if (numSkippedFrames < skippedFrameLimit)
+						skipDraw = true; // If we're lagging behind then don't draw
+				} else if (earliness > jitterTolerance) {
+					// If we're ahead by enough then pause.  Tolerate some slight addition to avoid
+					// degenerate cases if the timer wobbles near a frame edge.
+					uint millis = earliness / expectedFrameRate;
+					if (millis)
+						_system->delayMillis(millis);
+				}
+
+				limitRollingAccumulatedThousandthsOfFrames += 1000;
+
+				const uint32 prevRollingTime = limitRollingAccumulatedMSec;
+
+				// Resolve the actual msec to increment via rolling timers.
+				// Round up so that if time is divided by 60 then to derive tick count then it will always be in sync.
+				limitRollingAccumulatedMSec = (limitRollingAccumulatedThousandthsOfFrames + expectedFrameRate - 1) / expectedFrameRate;
+
+				timeAdvance = limitRollingAccumulatedMSec - prevRollingTime;
+
+				if (limitRollingAccumulatedThousandthsOfFrames >= expectedFrameRate * 1000) {
+					const int32 secondsToRemove = limitRollingAccumulatedThousandthsOfFrames / (expectedFrameRate * 1000);
+					limitRollingAccumulatedThousandthsOfFrames -= secondsToRemove * 1000 * expectedFrameRate;
+					limitRollingAccumulatedMSec -= secondsToRemove * 1000;
+				}
+			}
+
+			_runtime->advanceProjectTime(timeAdvance);
+		}
+
+		_runtime->runRealFrame(realElapsedThisFrame);
+
+		if (forceDraw || !skipDraw) {
+			_runtime->drawFrame(_system);
+			numSkippedFrames = 0;
+		} else {
+			if (frameWasRun)
+				numSkippedFrames++;
+		}
 	}
 
 	_runtime.release();
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 31c3a381734..5dde99c4f69 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -1613,22 +1613,21 @@ Runtime::Runtime() : _nextRuntimeGUID(1), _realDisplayMode(kColorDepthModeInvali
 	}
 }
 
-void Runtime::runFrame(uint32 msec) {
+void Runtime::runRealFrame(uint32 msec) {
 	_realTime += msec;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
-	if (_debugger) {
+	if (_debugger)
 		_debugger->runFrame(msec);
-		if (_debugger->isPaused())
-			return;
-	}
 #endif
+}
 
+bool Runtime::runProjectFrame() {
 	for (;;) {
 		VThreadState state = _vthread->step();
 		if (state != kVThreadReturn) {
 			// Still doing blocking tasks
-			return;
+			return false;
 		}
 
 		if (_queuedProjectDesc) {
@@ -1710,6 +1709,11 @@ void Runtime::runFrame(uint32 msec) {
 		break;
 	}
 
+	// Frame completed
+	return true;
+}
+
+void Runtime::advanceProjectTime(uint32 msec) {
 	_playTime += msec;
 }
 
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index db9bb68fa4e..0d602fc47fa 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -780,7 +780,9 @@ class Runtime {
 public:
 	Runtime();
 
-	void runFrame(uint32 msec);
+	void runRealFrame(uint32 msec);
+	bool runProjectFrame();
+	void advanceProjectTime(uint32 msec);
 	void drawFrame(OSystem *osystem);
 	void queueProject(const Common::SharedPtr<ProjectDescription> &desc);
 


Commit: 88a4890c19d058978d65a3f5c68ff876b7251761
    https://github.com/scummvm/scummvm/commit/88a4890c19d058978d65a3f5c68ff876b7251761
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Miniscript GetChild instruction

Changed paths:
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/miniscript.h
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index ba3bcf019ef..2261c4683a2 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -63,6 +63,10 @@ const Common::Array<MiniscriptInstruction *> &MiniscriptProgram::getInstructions
 	return _instructions;
 }
 
+const Common::Array<MiniscriptProgram::Attribute> &MiniscriptProgram::getAttributes() const {
+	return _attributes;
+}
+
 template<class T>
 struct MiniscriptInstructionLoader {
 	static bool loadInstruction(void *dest, uint32 instrFlags, Data::DataReader &instrDataReader);
@@ -447,6 +451,54 @@ GetChild::GetChild(uint32 attribute, bool isLValue, bool isIndexed)
 	: _attribute(attribute), _isLValue(isLValue), _isIndexed(isIndexed) {
 }
 
+MiniscriptInstructionOutcome GetChild::execute(MiniscriptThread *thread) const {
+	if (_isIndexed) {
+		if (thread->getStackSize() < 2) {
+			thread->error("Stack underflow");
+			return kMiniscriptInstructionOutcomeFailed;
+		}
+
+		if (!thread->getStackValueFromTop(0).isRValue()) {
+			MiniscriptInstructionOutcome outcome = thread->convertToRValue(0);
+			if (outcome != kMiniscriptInstructionOutcomeContinue)
+				return outcome;
+		}
+
+		if (!thread->getStackValueFromTop(1).isRValue()) {
+			MiniscriptInstructionOutcome outcome = thread->convertToRValue(1);
+			if (outcome != kMiniscriptInstructionOutcomeContinue)
+				return outcome;
+		}
+
+		DynamicValue index = thread->getStackValueFromTop(0).value;
+		thread->popValues(1);
+
+		// Transform into result LValue in place
+		MiniscriptThread::StackValue &result = thread->getStackValueFromTop(0);
+		result.attribIndex = this->_attribute;
+		result.index = index;
+		result.type = MiniscriptThread::kStackValueTypeLValueAttribIndex;
+	} else {
+		if (thread->getStackSize() < 1) {
+			thread->error("Stack underflow");
+			return kMiniscriptInstructionOutcomeFailed;
+		}
+
+		if (!thread->getStackValueFromTop(0).isRValue()) {
+			MiniscriptInstructionOutcome outcome = thread->convertToRValue(0);
+			if (outcome != kMiniscriptInstructionOutcomeContinue)
+				return outcome;
+		}
+
+		// Transform into result LValue in place
+		MiniscriptThread::StackValue &result = thread->getStackValueFromTop(0);
+		result.attribIndex = this->_attribute;
+		result.type = MiniscriptThread::kStackValueTypeLValueAttribIndex;
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 PushValue::PushValue(DataType dataType, const void *value, bool isLValue)
 	: _dataType(dataType), _isLValue(isLValue) {
 	switch (dataType) {
@@ -498,10 +550,7 @@ MiniscriptInstructionOutcome PushValue::execute(MiniscriptThread *thread) const
 		break;
 	}
 
-	if (_isLValue)
-		thread->pushLValue(value);
-	else
-		thread->pushRValue(value);
+	thread->pushRValue(value);
 
 	return kMiniscriptInstructionOutcomeContinue;
 }
@@ -542,10 +591,7 @@ MiniscriptInstructionOutcome PushGlobal::execute(MiniscriptThread *thread) const
 		return kMiniscriptInstructionOutcomeFailed;
 	}
 
-	if (_isLValue)
-		thread->pushLValue(value);
-	else
-		thread->pushRValue(value);
+	thread->pushRValue(value);
 
 	return kMiniscriptInstructionOutcomeContinue;
 }
@@ -594,10 +640,7 @@ MiniscriptInstructionOutcome PushGlobal::executeFindFilteredParent(MiniscriptThr
 	DynamicValue value;
 	value.setObject(ref);
 
-	if (_isLValue)
-		thread->pushLValue(value);
-	else
-		thread->pushRValue(value);
+	thread->pushRValue(value);
 
 	return kMiniscriptInstructionOutcomeContinue;
 }
@@ -621,7 +664,8 @@ void MiniscriptThread::runOnVThread(VThread &vthread, const Common::SharedPtr<Mi
 
 void MiniscriptThread::error(const Common::String &message) {
 #ifdef MTROPOLIS_DEBUG_ENABLE
-	_runtime->debugGetDebugger()->notify(kDebugSeverityError, Common::String("Miniscript error: " + message));
+	if (_runtime->debugGetDebugger())
+		_runtime->debugGetDebugger()->notify(kDebugSeverityError, Common::String("Miniscript error: " + message));
 #endif
 	warning("Miniscript error: %s", message.c_str());
 
@@ -641,7 +685,7 @@ Modifier *MiniscriptThread::getModifier() const {
 	return _modifier;
 }
 
-const Common::SharedPtr<MessageProperties>& MiniscriptThread::getMessageProperties() const {
+const Common::SharedPtr<MessageProperties> &MiniscriptThread::getMessageProperties() const {
 	return _msgProps;
 }
 
@@ -649,15 +693,6 @@ Runtime *MiniscriptThread::getRuntime() const {
 	return _runtime;
 }
 
-void MiniscriptThread::pushLValue(const DynamicValue &value) {
-	_stack.push_back(StackValue());
-
-	StackValue &stackValue = _stack.back();
-	stackValue.value = value;
-	stackValue.type = kStackValueTypeLValue;
-	stackValue.attribIndex = 0;
-}
-
 void MiniscriptThread::pushRValue(const DynamicValue &value) {
 	_stack.push_back(StackValue());
 
@@ -690,6 +725,126 @@ MiniscriptThread::StackValue &MiniscriptThread::getStackValueFromTop(size_t offs
 	return _stack[_stack.size() - 1 - offset];
 }
 
+MiniscriptInstructionOutcome MiniscriptThread::convertToRValue(size_t offset) {
+	assert(offset < _stack.size());
+	StackValue &stackValue = _stack[_stack.size() - 1 - offset];
+
+	if (stackValue.type == kStackValueTypeRValue)
+		return kMiniscriptInstructionOutcomeContinue;
+
+	const DynamicValue *index = nullptr;
+	if (stackValue.type == kStackValueTypeLValueAttribIndex)
+		index = &stackValue.index;
+	else {
+		assert(stackValue.type == kStackValueTypeLValueAttrib);
+	}
+
+	const Common::Array<MiniscriptProgram::Attribute> &attribs = this->_program->getAttributes();
+	if (stackValue.attribIndex >= attribs.size()) {
+		this->error("Invalid attribute index");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	const Common::String &attrib = attribs[stackValue.attribIndex].name;
+
+	const DynamicValue &value = stackValue.value;
+
+	DynamicValue resultValue;
+
+	switch (value.getType()) {
+	case DynamicValueTypes::kPoint: {
+			if (index == nullptr && attrib == "x")
+				resultValue.setInt(value.getPoint().x);
+			else if (index == nullptr && attrib == "y")
+				resultValue.setInt(value.getPoint().y);
+			else {
+				this->error("Unable to read attribute '" + attrib + "' from point");
+				return kMiniscriptInstructionOutcomeFailed;
+			}
+		} break;
+	case DynamicValueTypes::kIntegerRange: {
+			if (index == nullptr && attrib == "start")
+				resultValue.setInt(value.getIntRange().min);
+			else if (index == nullptr && attrib == "y")
+				resultValue.setInt(value.getIntRange().max);
+			else {
+				this->error("Unable to read attribute '" + attrib + "' from integer range");
+				return kMiniscriptInstructionOutcomeFailed;
+			}
+		} break;
+	case DynamicValueTypes::kVector: {
+			if (index == nullptr && attrib == "angle")
+				resultValue.setFloat(value.getVector().angleRadians * (180.8 / M_PI));
+			else if (index == nullptr && attrib == "magnitude")
+				resultValue.setFloat(value.getVector().magnitude);
+			else {
+				this->error("Unable to read attribute '" + attrib + "' from vector");
+				return kMiniscriptInstructionOutcomeFailed;
+			}
+		} break;
+	case DynamicValueTypes::kIncomingData: {
+			resultValue = _msgProps->getValue();
+		} break;
+	case DynamicValueTypes::kList: {
+			DynamicList &list = *value.getList().get();
+			if (index != nullptr && attrib == "value") {
+				int32 indexValue = 0;
+				if (index->getType() == DynamicValueTypes::kFloat) {
+					double f = index->getFloat();
+					double flooredF = floor(f);
+					if (!isfinite(f) || f != flooredF || flooredF < 1.0 || flooredF > static_cast<double>(INT32_MAX)) {
+						this->error("Unable to convert index to a valid value");
+						return kMiniscriptInstructionOutcomeFailed;
+					}
+					indexValue = static_cast<int32>(flooredF);
+				} else if (index->getType() == DynamicValueTypes::kInteger) {
+					indexValue = index->getInt();
+				} else {
+					this->error("List index was an invalid type");
+					return kMiniscriptInstructionOutcomeFailed;
+				}
+
+				if (indexValue < 1 || static_cast<size_t>(indexValue) > list.getSize()) {
+					this->error("List index out of bounds");
+					return kMiniscriptInstructionOutcomeFailed;
+				}
+
+				if (!list.setAtIndex(static_cast<size_t>(indexValue - 1), value)) {
+					this->error("List is the wrong type");
+					return kMiniscriptInstructionOutcomeFailed;
+				}
+			} else {
+				this->error("Unable to read attribute '" + attrib + "' from list");
+				return kMiniscriptInstructionOutcomeFailed;
+			}
+		} break;
+	case DynamicValueTypes::kObject: {
+			Common::SharedPtr<RuntimeObject> object = value.getObject().lock();
+			if (!object) {
+				this->error("Attempted to read attribute from invalid object");
+				return kMiniscriptInstructionOutcomeFailed;
+			}
+
+			if (!object->readAttribute(resultValue, attrib, index)) {
+				this->error("Unable to read attribute '" + attrib + "' from object");
+				return kMiniscriptInstructionOutcomeFailed;
+			}
+		} break;
+	default:
+		this->error("Type has no attributes");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	// Get the slot back since it may have moved
+	StackValue &resultSlot = _stack[_stack.size() - 1 - offset];
+	resultSlot.attribIndex = 0;
+	resultSlot.index.clear();
+	resultSlot.value = resultValue;
+	resultSlot.type = kStackValueTypeRValue;
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 VThreadState MiniscriptThread::resumeTask(const ResumeTaskData &data) {
 	return data.thread->resume(data);
 }
diff --git a/engines/mtropolis/miniscript.h b/engines/mtropolis/miniscript.h
index ee7a9f71475..81a8637acc7 100644
--- a/engines/mtropolis/miniscript.h
+++ b/engines/mtropolis/miniscript.h
@@ -74,6 +74,7 @@ public:
 	~MiniscriptProgram();
 
 	const Common::Array<MiniscriptInstruction *> &getInstructions() const;
+	const Common::Array<Attribute> &getAttributes() const;
 
 private:
 	Common::SharedPtr<Common::Array<uint8> > _programData;
@@ -199,11 +200,13 @@ namespace MiniscriptInstructions {
 	class VectorCreate : public UnimplementedInstruction {
 	};
 
-	class GetChild : public UnimplementedInstruction {
+	class GetChild : public MiniscriptInstruction {
 	public:
 		GetChild(uint32 attribute, bool isLValue, bool isIndexed);
 
 	private:
+		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
+
 		uint32 _attribute;
 		bool _isLValue;
 		bool _isIndexed;
@@ -296,16 +299,20 @@ namespace MiniscriptInstructions {
 class MiniscriptThread {
 public:
 	enum StackValueType {
-		kStackValueTypeLValue,
-		kStackValueTypeRValue,
 		kStackValueTypeLValueAttrib,
+		kStackValueTypeLValueAttribIndex,
+		kStackValueTypeRValue,
 	};
 
 	struct StackValue {
 		DynamicValue value;
+		DynamicValue index;	// Sigh
 
 		StackValueType type;
 		uint attribIndex;
+
+		inline bool isRValue() const { return this->type == kStackValueTypeRValue; }
+		inline bool isLValue() const { return this->type == kStackValueTypeLValueAttrib || this->type == kStackValueTypeLValueAttribIndex; }
 	};
 
 	MiniscriptThread(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msgProps, const Common::SharedPtr<MiniscriptProgram> &program, const Common::SharedPtr<MiniscriptReferences> &refs, Modifier *modifier);
@@ -320,13 +327,14 @@ public:
 	const Common::SharedPtr<MessageProperties> &getMessageProperties() const;
 	Runtime *getRuntime() const;
 
-	void pushLValue(const DynamicValue &value);
 	void pushRValue(const DynamicValue &value);
 	void pushLValueAttrib(const DynamicValue &value, uint attributeIndex);
 	void popValues(size_t count);
 	size_t getStackSize() const;
 	StackValue &getStackValueFromTop(size_t offset);
 
+	MiniscriptInstructionOutcome convertToRValue(size_t offset);
+
 private:
 	struct ResumeTaskData {
 		Common::SharedPtr<MiniscriptThread> thread;
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 1e5ee43b6e7..933b23617f0 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -73,7 +73,7 @@ void BehaviorModifier::visitInternalReferences(IStructuralReferenceVisitor* visi
 
 // Miniscript modifier
 bool MiniscriptModifier::load(ModifierLoaderContext &context, const Data::MiniscriptModifier &data) {
-	if (!_enableWhen.load(data.enableWhen))
+	if (!this->loadTypicalHeader(data.modHeader) || !_enableWhen.load(data.enableWhen))
 		return false;
 
 	if (!MiniscriptParser::parse(data.program, _program, _references))
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 5dde99c4f69..65cfaaee94b 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -455,6 +455,13 @@ bool DynamicList::setAtIndex(size_t index, const DynamicValue &value) {
 	}
 }
 
+size_t DynamicList::getSize() const {
+	if (!_container)
+		return 0;
+	else
+		return _container->getSize();
+}
+
 DynamicList &DynamicList::operator=(const DynamicList &other) {
 	if (this != &other) {
 		clear();
@@ -908,7 +915,7 @@ void DynamicValue::clear() {
 void DynamicValue::initFromOther(const DynamicValue &other) {
 	assert(_type == DynamicValueTypes::kNull);
 
-	switch (_type) {
+	switch (other._type) {
 	case DynamicValueTypes::kNull:
 	case DynamicValueTypes::kIncomingData:
 		break;
@@ -1163,6 +1170,14 @@ bool RuntimeObject::isElement() const {
 	return false;
 }
 
+bool RuntimeObject::readAttribute(DynamicValue &result, const Common::String &attrib, const DynamicValue *optionalIndex) {
+	return false;
+}
+
+bool RuntimeObject::setAttribute(const Common::String &attrib, const DynamicValue *optionalIndex, const DynamicValue &value) {
+	return false;
+}
+
 MessageProperties::MessageProperties(const Event &evt, const DynamicValue &value, const Common::WeakPtr<RuntimeObject> &source)
 	: _evt(evt), _value(value), _source(source) {
 }
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 0d602fc47fa..1f7908e2098 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -475,6 +475,7 @@ struct DynamicList {
 	const Common::Array<bool> &getBool() const;
 
 	bool setAtIndex(size_t index, const DynamicValue &value);
+	size_t getSize() const;
 
 	DynamicList &operator=(const DynamicList &other);
 
@@ -943,6 +944,9 @@ public:
 	virtual bool isModifier() const;
 	virtual bool isElement() const;
 
+	virtual bool readAttribute(DynamicValue &result, const Common::String &attrib, const DynamicValue *optionalIndex);
+	virtual bool setAttribute(const Common::String &attrib, const DynamicValue *optionalIndex, const DynamicValue &value);
+
 protected:
 	// This is the static GUID stored in the data, it is not guaranteed
 	// to be globally unique at runtime.  In particular, cloning an object


Commit: 7ee9d0224ea1214161bb6e382ab389b47ac54adf
    https://github.com/scummvm/scummvm/commit/7ee9d0224ea1214161bb6e382ab389b47ac54adf
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: More Miniscript work, overhaul lvalue handling to handle nested members (e.g. element.pos.x)

Changed paths:
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/miniscript.h
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/notes.txt
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 2261c4683a2..d6fe2d549c5 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -441,6 +441,37 @@ MiniscriptInstructionOutcome UnimplementedInstruction::execute(MiniscriptThread
 	return kMiniscriptInstructionOutcomeFailed;
 }
 
+
+MiniscriptInstructionOutcome Set::execute(MiniscriptThread *thread) const {
+	if (thread->getStackSize() < 2) {
+		thread->error("Stack underflow");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	// Convert value
+	MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	const MiniscriptStackValue &srcValue = thread->getStackValueFromTop(0);
+	MiniscriptStackValue &target = thread->getStackValueFromTop(1);
+
+	if (target.value.getType() != DynamicValueTypes::kWriteProxy) {
+		thread->error("Can't assign to rvalue");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	const DynamicValueWriteProxy &proxy = target.value.getWriteProxy();
+	if (!proxy.ifc->write(srcValue.value, proxy.objectRef, proxy.ptrOrOffset)) {
+		thread->error("Failed to assign value");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	thread->popValues(2);
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 Send::Send(const Event &evt) : _evt(evt) {
 }
 
@@ -452,48 +483,156 @@ GetChild::GetChild(uint32 attribute, bool isLValue, bool isIndexed)
 }
 
 MiniscriptInstructionOutcome GetChild::execute(MiniscriptThread *thread) const {
+	const Common::Array<MiniscriptProgram::Attribute> &attribs = thread->getProgram()->getAttributes();
+	if (_attribute >= attribs.size()) {
+		thread->error("Invalid attribute index");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	const Common::String &attrib = attribs[_attribute].name;
+
 	if (_isIndexed) {
 		if (thread->getStackSize() < 2) {
 			thread->error("Stack underflow");
 			return kMiniscriptInstructionOutcomeFailed;
 		}
 
-		if (!thread->getStackValueFromTop(0).isRValue()) {
-			MiniscriptInstructionOutcome outcome = thread->convertToRValue(0);
-			if (outcome != kMiniscriptInstructionOutcomeContinue)
-				return outcome;
-		}
+		// Convert index
+		MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0);
+		if (outcome != kMiniscriptInstructionOutcomeContinue)
+			return outcome;
+
+		const MiniscriptStackValue &indexSlot = thread->getStackValueFromTop(0);
+		MiniscriptStackValue &indexableValueSlot = thread->getStackValueFromTop(1);
+
+		if (_isLValue) {
+			if (indexableValueSlot.value.getType() == DynamicValueTypes::kObject) {
+				Common::SharedPtr<RuntimeObject> obj = indexableValueSlot.value.getObject().lock();
+				if (!obj) {
+					thread->error("Tried to write '" + attrib + "' to an invalid object reference");
+					return kMiniscriptInstructionOutcomeFailed;
+				}
 
-		if (!thread->getStackValueFromTop(1).isRValue()) {
-			MiniscriptInstructionOutcome outcome = thread->convertToRValue(1);
+				DynamicValueWriteProxy proxy;
+				if (!obj->writeRefAttributeIndexed(proxy, attrib, indexSlot.value)) {
+					thread->error("Failed to get a writeable reference to attribute '" + attrib + "'");
+					return kMiniscriptInstructionOutcomeFailed;
+				}
+			} else if (indexableValueSlot.value.getType() == DynamicValueTypes::kWriteProxy) {
+				const DynamicValueWriteProxy &proxy = indexableValueSlot.value.getWriteProxy();
+				if (!proxy.ifc->refAttribIndexed(indexableValueSlot.value, proxy.objectRef, proxy.ptrOrOffset, attrib, indexSlot.value)) {
+					thread->error("Can't write to attribute '" + attrib + "'");
+					return kMiniscriptInstructionOutcomeFailed;
+				}
+			} else {
+				thread->error("Tried to l-value index something that was not writeable");
+				return kMiniscriptInstructionOutcomeFailed;
+			}
+		} else {
+			outcome = readRValueAttribIndexed(thread, indexableValueSlot.value, attrib, indexSlot.value);
 			if (outcome != kMiniscriptInstructionOutcomeContinue)
 				return outcome;
 		}
 
-		DynamicValue index = thread->getStackValueFromTop(0).value;
 		thread->popValues(1);
-
-		// Transform into result LValue in place
-		MiniscriptThread::StackValue &result = thread->getStackValueFromTop(0);
-		result.attribIndex = this->_attribute;
-		result.index = index;
-		result.type = MiniscriptThread::kStackValueTypeLValueAttribIndex;
 	} else {
 		if (thread->getStackSize() < 1) {
 			thread->error("Stack underflow");
 			return kMiniscriptInstructionOutcomeFailed;
 		}
 
-		if (!thread->getStackValueFromTop(0).isRValue()) {
-			MiniscriptInstructionOutcome outcome = thread->convertToRValue(0);
+		MiniscriptStackValue &indexableValueSlot = thread->getStackValueFromTop(0);
+
+		if (_isLValue) {
+			if (indexableValueSlot.value.getType() == DynamicValueTypes::kObject) {
+				Common::SharedPtr<RuntimeObject> obj = indexableValueSlot.value.getObject().lock();
+				if (!obj) {
+					thread->error("Tried to read '" + attrib + "' to an invalid object reference");
+					return kMiniscriptInstructionOutcomeFailed;
+				}
+
+				if (!obj->readAttribute(indexableValueSlot.value, attrib)) {
+					thread->error("Failed to read attribute '" + attrib + "'");
+					return kMiniscriptInstructionOutcomeFailed;
+				}
+			} else if (indexableValueSlot.value.getType() == DynamicValueTypes::kWriteProxy) {
+				const DynamicValueWriteProxy &proxy = indexableValueSlot.value.getWriteProxy();
+				if (!proxy.ifc->refAttrib(indexableValueSlot.value, proxy.objectRef, proxy.ptrOrOffset, attrib)) {
+					thread->error("Can't write to attribute '" + attrib + "'");
+					return kMiniscriptInstructionOutcomeFailed;
+				}
+			} else {
+				thread->error("Tried to l-value index something that was not writeable");
+				return kMiniscriptInstructionOutcomeFailed;
+			}
+		} else {
+			MiniscriptInstructionOutcome outcome = readRValueAttrib(thread, indexableValueSlot.value, attrib);
 			if (outcome != kMiniscriptInstructionOutcomeContinue)
 				return outcome;
 		}
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome GetChild::readRValueAttrib(MiniscriptThread *thread, DynamicValue &valueSrcDest, const Common::String &attrib) const {
+	switch (valueSrcDest.getType()) {
+	case DynamicValueTypes::kIntegerRange:
+		if (attrib == "start")
+			valueSrcDest.setInt(valueSrcDest.getIntRange().min);
+		else if (attrib == "end")
+			valueSrcDest.setInt(valueSrcDest.getIntRange().max);
+		else {
+			thread->error(Common::String("Integer range has no attribute '") + attrib + "'");
+			return kMiniscriptInstructionOutcomeFailed;
+		}
+		break;
 
-		// Transform into result LValue in place
-		MiniscriptThread::StackValue &result = thread->getStackValueFromTop(0);
-		result.attribIndex = this->_attribute;
-		result.type = MiniscriptThread::kStackValueTypeLValueAttribIndex;
+	case DynamicValueTypes::kVector:
+		if (attrib == "angle")
+			valueSrcDest.setInt(valueSrcDest.getVector().angleRadians * (180.0 / M_PI));
+		else if (attrib == "magnitude")
+			valueSrcDest.setInt(valueSrcDest.getVector().magnitude);
+		else {
+			thread->error(Common::String("Vector has no attribute '") + attrib + "'");
+			return kMiniscriptInstructionOutcomeFailed;
+		}
+		break;
+	case DynamicValueTypes::kObject: {
+			Common::SharedPtr<RuntimeObject> obj = valueSrcDest.getObject().lock();
+			if (!obj) {
+				thread->error("Unable to read attrib '" + attrib + "' from invalid object");
+				return kMiniscriptInstructionOutcomeFailed;
+			} else if (!obj->readAttribute(valueSrcDest, attrib)) {
+				thread->error("Unable to read attrib '" + attrib + "'");
+				return kMiniscriptInstructionOutcomeFailed;
+			}
+		} break;
+	default:
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome GetChild::readRValueAttribIndexed(MiniscriptThread *thread, DynamicValue &valueSrcDest, const Common::String &attrib, const DynamicValue &index) const {
+	switch (valueSrcDest.getType()) {
+	case DynamicValueTypes::kList:
+		if (attrib == "value") {
+			// Hold list ref since it may get released by the read operation
+			Common::SharedPtr<DynamicList> list = valueSrcDest.getList();
+			size_t realIndex = 0;
+			if (!DynamicList::dynamicValueToIndex(realIndex, index)) {
+				thread->error("Unable to list value at specified index");
+				return kMiniscriptInstructionOutcomeFailed;
+			}
+		} else {
+			thread->error("Unable to read list attribute '" + attrib + "'");
+			return kMiniscriptInstructionOutcomeFailed;
+		}
+		break;
+	default:
+		return kMiniscriptInstructionOutcomeFailed;
 	}
 
 	return kMiniscriptInstructionOutcomeContinue;
@@ -550,7 +689,7 @@ MiniscriptInstructionOutcome PushValue::execute(MiniscriptThread *thread) const
 		break;
 	}
 
-	thread->pushRValue(value);
+	thread->pushValue(value);
 
 	return kMiniscriptInstructionOutcomeContinue;
 }
@@ -591,7 +730,7 @@ MiniscriptInstructionOutcome PushGlobal::execute(MiniscriptThread *thread) const
 		return kMiniscriptInstructionOutcomeFailed;
 	}
 
-	thread->pushRValue(value);
+	thread->pushValue(value);
 
 	return kMiniscriptInstructionOutcomeContinue;
 }
@@ -640,7 +779,7 @@ MiniscriptInstructionOutcome PushGlobal::executeFindFilteredParent(MiniscriptThr
 	DynamicValue value;
 	value.setObject(ref);
 
-	thread->pushRValue(value);
+	thread->pushValue(value);
 
 	return kMiniscriptInstructionOutcomeContinue;
 }
@@ -693,22 +832,11 @@ Runtime *MiniscriptThread::getRuntime() const {
 	return _runtime;
 }
 
-void MiniscriptThread::pushRValue(const DynamicValue &value) {
-	_stack.push_back(StackValue());
-
-	StackValue &stackValue = _stack.back();
-	stackValue.value = value;
-	stackValue.type = kStackValueTypeRValue;
-	stackValue.attribIndex = 0;
-}
-
-void MiniscriptThread::pushLValueAttrib(const DynamicValue &value, uint attributeIndex) {
-	_stack.push_back(StackValue());
+void MiniscriptThread::pushValue(const DynamicValue &value) {
+	_stack.push_back(MiniscriptStackValue());
 
-	StackValue &stackValue = _stack.back();
+	MiniscriptStackValue &stackValue = _stack.back();
 	stackValue.value = value;
-	stackValue.type = kStackValueTypeLValueAttrib;
-	stackValue.attribIndex = 0;
 }
 
 void MiniscriptThread::popValues(size_t count) {
@@ -720,128 +848,40 @@ size_t MiniscriptThread::getStackSize() const {
 	return _stack.size();
 }
 
-MiniscriptThread::StackValue &MiniscriptThread::getStackValueFromTop(size_t offset) {
+MiniscriptStackValue &MiniscriptThread::getStackValueFromTop(size_t offset) {
 	assert(offset < _stack.size());
 	return _stack[_stack.size() - 1 - offset];
 }
 
-MiniscriptInstructionOutcome MiniscriptThread::convertToRValue(size_t offset) {
+MiniscriptInstructionOutcome MiniscriptThread::dereferenceRValue(size_t offset) {
 	assert(offset < _stack.size());
-	StackValue &stackValue = _stack[_stack.size() - 1 - offset];
-
-	if (stackValue.type == kStackValueTypeRValue)
-		return kMiniscriptInstructionOutcomeContinue;
-
-	const DynamicValue *index = nullptr;
-	if (stackValue.type == kStackValueTypeLValueAttribIndex)
-		index = &stackValue.index;
-	else {
-		assert(stackValue.type == kStackValueTypeLValueAttrib);
-	}
-
-	const Common::Array<MiniscriptProgram::Attribute> &attribs = this->_program->getAttributes();
-	if (stackValue.attribIndex >= attribs.size()) {
-		this->error("Invalid attribute index");
-		return kMiniscriptInstructionOutcomeFailed;
-	}
-
-	const Common::String &attrib = attribs[stackValue.attribIndex].name;
-
-	const DynamicValue &value = stackValue.value;
-
-	DynamicValue resultValue;
-
-	switch (value.getType()) {
-	case DynamicValueTypes::kPoint: {
-			if (index == nullptr && attrib == "x")
-				resultValue.setInt(value.getPoint().x);
-			else if (index == nullptr && attrib == "y")
-				resultValue.setInt(value.getPoint().y);
-			else {
-				this->error("Unable to read attribute '" + attrib + "' from point");
-				return kMiniscriptInstructionOutcomeFailed;
-			}
-		} break;
-	case DynamicValueTypes::kIntegerRange: {
-			if (index == nullptr && attrib == "start")
-				resultValue.setInt(value.getIntRange().min);
-			else if (index == nullptr && attrib == "y")
-				resultValue.setInt(value.getIntRange().max);
-			else {
-				this->error("Unable to read attribute '" + attrib + "' from integer range");
-				return kMiniscriptInstructionOutcomeFailed;
-			}
-		} break;
-	case DynamicValueTypes::kVector: {
-			if (index == nullptr && attrib == "angle")
-				resultValue.setFloat(value.getVector().angleRadians * (180.8 / M_PI));
-			else if (index == nullptr && attrib == "magnitude")
-				resultValue.setFloat(value.getVector().magnitude);
-			else {
-				this->error("Unable to read attribute '" + attrib + "' from vector");
-				return kMiniscriptInstructionOutcomeFailed;
-			}
-		} break;
-	case DynamicValueTypes::kIncomingData: {
-			resultValue = _msgProps->getValue();
-		} break;
-	case DynamicValueTypes::kList: {
-			DynamicList &list = *value.getList().get();
-			if (index != nullptr && attrib == "value") {
-				int32 indexValue = 0;
-				if (index->getType() == DynamicValueTypes::kFloat) {
-					double f = index->getFloat();
-					double flooredF = floor(f);
-					if (!isfinite(f) || f != flooredF || flooredF < 1.0 || flooredF > static_cast<double>(INT32_MAX)) {
-						this->error("Unable to convert index to a valid value");
-						return kMiniscriptInstructionOutcomeFailed;
-					}
-					indexValue = static_cast<int32>(flooredF);
-				} else if (index->getType() == DynamicValueTypes::kInteger) {
-					indexValue = index->getInt();
-				} else {
-					this->error("List index was an invalid type");
-					return kMiniscriptInstructionOutcomeFailed;
-				}
+	MiniscriptStackValue &stackValue = _stack[_stack.size() - 1 - offset];
 
-				if (indexValue < 1 || static_cast<size_t>(indexValue) > list.getSize()) {
-					this->error("List index out of bounds");
-					return kMiniscriptInstructionOutcomeFailed;
-				}
-
-				if (!list.setAtIndex(static_cast<size_t>(indexValue - 1), value)) {
-					this->error("List is the wrong type");
-					return kMiniscriptInstructionOutcomeFailed;
+	switch (stackValue.value.getType()) {
+	case DynamicValueTypes::kObject: {
+			Common::SharedPtr<RuntimeObject> obj = stackValue.value.getObject().lock();
+			if (obj && obj->isModifier()) {
+				const Modifier *modifier = static_cast<const Modifier *>(obj.get());
+				if (modifier->isVariable()) {
+					static_cast<const VariableModifier *>(modifier)->getValue(stackValue.value);
 				}
-			} else {
-				this->error("Unable to read attribute '" + attrib + "' from list");
-				return kMiniscriptInstructionOutcomeFailed;
 			}
 		} break;
-	case DynamicValueTypes::kObject: {
-			Common::SharedPtr<RuntimeObject> object = value.getObject().lock();
-			if (!object) {
-				this->error("Attempted to read attribute from invalid object");
-				return kMiniscriptInstructionOutcomeFailed;
-			}
-
-			if (!object->readAttribute(resultValue, attrib, index)) {
-				this->error("Unable to read attribute '" + attrib + "' from object");
+	case DynamicValueTypes::kWriteProxy:
+		this->error("Attempted to dereference an lvalue proxy");
+		return kMiniscriptInstructionOutcomeFailed;
+	case DynamicValueTypes::kReadProxy: {
+			const DynamicValueReadProxy &readProxy = stackValue.value.getReadProxy();
+			if (!readProxy.ifc->read(stackValue.value, readProxy.objectRef, readProxy.ptrOrOffset)) {
+				this->error("Failed to access a proxy value");
 				return kMiniscriptInstructionOutcomeFailed;
 			}
-		} break;
+		}
+		break;
 	default:
-		this->error("Type has no attributes");
-		return kMiniscriptInstructionOutcomeFailed;
+		break;
 	}
 
-	// Get the slot back since it may have moved
-	StackValue &resultSlot = _stack[_stack.size() - 1 - offset];
-	resultSlot.attribIndex = 0;
-	resultSlot.index.clear();
-	resultSlot.value = resultValue;
-	resultSlot.type = kStackValueTypeRValue;
-
 	return kMiniscriptInstructionOutcomeContinue;
 }
 
@@ -884,4 +924,16 @@ VThreadState MiniscriptThread::resume(const ResumeTaskData &taskData) {
 	return kVThreadReturn;
 }
 
+MiniscriptInstructionOutcome MiniscriptThread::tryLoadVariable(MiniscriptStackValue &stackValue) {
+	if (stackValue.value.getType() == DynamicValueTypes::kObject) {
+		Common::SharedPtr<RuntimeObject> obj = stackValue.value.getObject().lock();
+		if (obj && obj->isModifier() && static_cast<Modifier *>(obj.get())->isVariable()) {
+			VariableModifier *varMod = static_cast<VariableModifier *>(obj.get());
+			varMod->getValue(stackValue.value);
+		}
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/miniscript.h b/engines/mtropolis/miniscript.h
index 81a8637acc7..10f6e57121b 100644
--- a/engines/mtropolis/miniscript.h
+++ b/engines/mtropolis/miniscript.h
@@ -28,7 +28,7 @@
 namespace MTropolis {
 
 class MiniscriptThread;
-struct MiniscriptVM;
+struct MiniscriptStackValue;
 struct IMiniscriptInstructionFactory;
 
 enum MiniscriptInstructionOutcome {
@@ -95,7 +95,9 @@ namespace MiniscriptInstructions {
 		virtual MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
 	};
 
-	class Set : public UnimplementedInstruction {
+	class Set : public MiniscriptInstruction {
+	private:
+		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
 	};
 
 	class Send : public UnimplementedInstruction {
@@ -206,6 +208,8 @@ namespace MiniscriptInstructions {
 
 	private:
 		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
+		MiniscriptInstructionOutcome readRValueAttrib(MiniscriptThread *thread, DynamicValue &valueSrcDest, const Common::String &attrib) const;
+		MiniscriptInstructionOutcome readRValueAttribIndexed(MiniscriptThread *thread, DynamicValue &valueSrcDest, const Common::String &attrib, const DynamicValue &index) const;
 
 		uint32 _attribute;
 		bool _isLValue;
@@ -296,24 +300,13 @@ namespace MiniscriptInstructions {
 	};
 } // End of namespace MiniscriptInstructions
 
-class MiniscriptThread {
-public:
-	enum StackValueType {
-		kStackValueTypeLValueAttrib,
-		kStackValueTypeLValueAttribIndex,
-		kStackValueTypeRValue,
-	};
-
-	struct StackValue {
-		DynamicValue value;
-		DynamicValue index;	// Sigh
 
-		StackValueType type;
-		uint attribIndex;
+struct MiniscriptStackValue {
+	DynamicValue value;
+};
 
-		inline bool isRValue() const { return this->type == kStackValueTypeRValue; }
-		inline bool isLValue() const { return this->type == kStackValueTypeLValueAttrib || this->type == kStackValueTypeLValueAttribIndex; }
-	};
+class MiniscriptThread {
+public:
 
 	MiniscriptThread(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msgProps, const Common::SharedPtr<MiniscriptProgram> &program, const Common::SharedPtr<MiniscriptReferences> &refs, Modifier *modifier);
 
@@ -327,13 +320,12 @@ public:
 	const Common::SharedPtr<MessageProperties> &getMessageProperties() const;
 	Runtime *getRuntime() const;
 
-	void pushRValue(const DynamicValue &value);
-	void pushLValueAttrib(const DynamicValue &value, uint attributeIndex);
+	void pushValue(const DynamicValue &value);
 	void popValues(size_t count);
 	size_t getStackSize() const;
-	StackValue &getStackValueFromTop(size_t offset);
+	MiniscriptStackValue &getStackValueFromTop(size_t offset);
 
-	MiniscriptInstructionOutcome convertToRValue(size_t offset);
+	MiniscriptInstructionOutcome dereferenceRValue(size_t offset);
 
 private:
 	struct ResumeTaskData {
@@ -343,12 +335,14 @@ private:
 	static VThreadState resumeTask(const ResumeTaskData &data);
 	VThreadState resume(const ResumeTaskData &data);
 
+	MiniscriptInstructionOutcome tryLoadVariable(MiniscriptStackValue &stackValue);
+
 	Common::SharedPtr<MiniscriptProgram> _program;
 	Common::SharedPtr<MiniscriptReferences> _refs;
 	Common::SharedPtr<MessageProperties> _msgProps;
 	Modifier *_modifier;
 	Runtime *_runtime;
-	Common::Array<StackValue> _stack;
+	Common::Array<MiniscriptStackValue> _stack;
 
 	size_t _currentInstruction;
 	bool _failed;
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 933b23617f0..5e57f141d9f 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -558,6 +558,19 @@ bool BooleanVariableModifier::load(ModifierLoaderContext &context, const Data::B
 	return true;
 }
 
+bool BooleanVariableModifier::setValue(const DynamicValue &value) {
+	if (value.getType() == DynamicValueTypes::kBoolean)
+		_value = value.getBool();
+	else
+		return false;
+
+	return true;
+}
+
+void BooleanVariableModifier::getValue(DynamicValue &dest) const {
+	dest.setBool(_value);
+}
+
 Common::SharedPtr<Modifier> BooleanVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new BooleanVariableModifier(*this));
 }
@@ -571,6 +584,21 @@ bool IntegerVariableModifier::load(ModifierLoaderContext& context, const Data::I
 	return true;
 }
 
+bool IntegerVariableModifier::setValue(const DynamicValue &value) {
+	if (value.getType() == DynamicValueTypes::kFloat)
+		_value = static_cast<int32>(floor(value.getFloat() + 0.5));
+	else if (value.getType() == DynamicValueTypes::kInteger)
+		_value = value.getInt();
+	else
+		return false;
+
+	return true;
+}
+
+void IntegerVariableModifier::getValue(DynamicValue &dest) const {
+	dest.setInt(_value);
+}
+
 Common::SharedPtr<Modifier> IntegerVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new IntegerVariableModifier(*this));
 }
@@ -585,6 +613,19 @@ bool IntegerRangeVariableModifier::load(ModifierLoaderContext& context, const Da
 	return true;
 }
 
+bool IntegerRangeVariableModifier::setValue(const DynamicValue &value) {
+	if (value.getType() == DynamicValueTypes::kIntegerRange)
+		_range = value.getIntRange();
+	else
+		return false;
+
+	return true;
+}
+
+void IntegerRangeVariableModifier::getValue(DynamicValue &dest) const {
+	dest.setIntRange(_range);
+}
+
 Common::SharedPtr<Modifier> IntegerRangeVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new IntegerRangeVariableModifier(*this));
 }
@@ -599,6 +640,19 @@ bool VectorVariableModifier::load(ModifierLoaderContext &context, const Data::Ve
 	return true;
 }
 
+bool VectorVariableModifier::setValue(const DynamicValue &value) {
+	if (value.getType() == DynamicValueTypes::kVector)
+		_vector = value.getVector();
+	else
+		return false;
+
+	return true;
+}
+
+void VectorVariableModifier::getValue(DynamicValue &dest) const {
+	dest.setVector(_vector);
+}
+
 Common::SharedPtr<Modifier> VectorVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new VectorVariableModifier(*this));
 }
@@ -613,6 +667,19 @@ bool PointVariableModifier::load(ModifierLoaderContext &context, const Data::Poi
 	return true;
 }
 
+bool PointVariableModifier::setValue(const DynamicValue &value) {
+	if (value.getType() == DynamicValueTypes::kPoint)
+		_value = value.getPoint();
+	else
+		return false;
+
+	return true;
+}
+
+void PointVariableModifier::getValue(DynamicValue &dest) const {
+	dest.setPoint(_value);
+}
+
 Common::SharedPtr<Modifier> PointVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new PointVariableModifier(*this));
 }
@@ -626,6 +693,21 @@ bool FloatingPointVariableModifier::load(ModifierLoaderContext &context, const D
 	return true;
 }
 
+bool FloatingPointVariableModifier::setValue(const DynamicValue &value) {
+	if (value.getType() == DynamicValueTypes::kInteger)
+		_value = value.getInt();
+	else if (value.getType() == DynamicValueTypes::kFloat)
+		_value = value.getFloat();
+	else
+		return false;
+
+	return true;
+}
+
+void FloatingPointVariableModifier::getValue(DynamicValue &dest) const {
+	dest.setFloat(_value);
+}
+
 Common::SharedPtr<Modifier> FloatingPointVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new FloatingPointVariableModifier(*this));
 }
@@ -639,6 +721,19 @@ bool StringVariableModifier::load(ModifierLoaderContext &context, const Data::St
 	return true;
 }
 
+bool StringVariableModifier::setValue(const DynamicValue &value) {
+	if (value.getType() == DynamicValueTypes::kString)
+		_value = value.getString();
+	else
+		return false;
+
+	return true;
+}
+
+void StringVariableModifier::getValue(DynamicValue &dest) const {
+	dest.setString(_value);
+}
+
 Common::SharedPtr<Modifier> StringVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new StringVariableModifier(*this));
 }
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 1abb240dd30..bd4691c87e9 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -515,7 +515,12 @@ private:
 	Common::Array<Point16> _polyPoints;
 };
 
-class CompoundVariableModifier : public VariableModifier, public IModifierContainer {
+// Compound variable modifiers are not true variable modifiers.
+// They aren't treated as values by Miniscript and they aren't
+// treated as unique objects by aliases.  The only way that
+// they behave like variable modifiers is that it's legal to
+// put them inside of CompoundVariableModifiers.
+class CompoundVariableModifier : public Modifier, public IModifierContainer {
 public:
 	bool load(ModifierLoaderContext &context, const Data::CompoundVariableModifier &data);
 
@@ -539,6 +544,9 @@ class BooleanVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::BooleanVariableModifier &data);
 
+	bool setValue(const DynamicValue &value) override;
+	void getValue(DynamicValue &dest) const override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Boolean Variable Modifier"; }
 #endif
@@ -553,6 +561,9 @@ class IntegerVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::IntegerVariableModifier &data);
 
+	bool setValue(const DynamicValue &value) override;
+	void getValue(DynamicValue &dest) const override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Integer Variable Modifier"; }
 #endif
@@ -567,6 +578,9 @@ class IntegerRangeVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::IntegerRangeVariableModifier &data);
 
+	bool setValue(const DynamicValue &value) override;
+	void getValue(DynamicValue &dest) const override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Integer Range Variable Modifier"; }
 #endif
@@ -581,6 +595,9 @@ class VectorVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::VectorVariableModifier &data);
 
+	bool setValue(const DynamicValue &value) override;
+	void getValue(DynamicValue &dest) const override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Vector Variable Modifier"; }
 #endif
@@ -595,6 +612,9 @@ class PointVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::PointVariableModifier &data);
 
+	bool setValue(const DynamicValue &value) override;
+	void getValue(DynamicValue &dest) const override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Point Variable Modifier"; }
 #endif
@@ -609,6 +629,9 @@ class FloatingPointVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::FloatingPointVariableModifier &data);
 
+	bool setValue(const DynamicValue &value) override;
+	void getValue(DynamicValue &dest) const override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Floating Point Variable Modifier"; }
 #endif
@@ -623,6 +646,9 @@ class StringVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::StringVariableModifier &data);
 
+	bool setValue(const DynamicValue &value) override;
+	void getValue(DynamicValue &dest) const override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "String Variable Modifier"; }
 #endif
diff --git a/engines/mtropolis/notes.txt b/engines/mtropolis/notes.txt
index e947bc19513..86f42ebca3b 100644
--- a/engines/mtropolis/notes.txt
+++ b/engines/mtropolis/notes.txt
@@ -84,3 +84,15 @@ when it is assigned to.
 A big implication of this is that it's okay to use raw pointers to scene objects
 in VThread tasks.  In particular, tasks calling member functions are OK to
 schedule.
+
+
+Miniscript:
+
+Miniscript has a lot of weird quirks.  Most symbols inside of a script are resolved
+as element members, so "WorldManager" in a script doesn't reference some global
+object named WorldManager, it compiles into the equivalent of "element.worldmanager"
+and the actual WorldManager is resolved via hierarchical lookup when attempting to
+read the attribute from the element.
+
+Also, "this" is not reserved - if you look up the "this" attribute from ANYTHING
+then it will resolve to the modifier executing the script.
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index eccb6d8716d..6ed854ec325 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -82,6 +82,9 @@ Common::SharedPtr<Modifier> MediaCueMessengerModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new MediaCueMessengerModifier(*this));
 }
 
+ObjectReferenceVariableModifier::ObjectReferenceVariableModifier() : _isResolved(false) {
+}
+
 bool ObjectReferenceVariableModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::ObjectReferenceVariableModifier &data) {
 	if (data.setToSourceParentWhen.type != Data::PlugInTypeTaggedValue::kEvent)
 		return false;
@@ -92,10 +95,39 @@ bool ObjectReferenceVariableModifier::load(const PlugInModifierLoaderContext &co
 		return false;
 
 	_objectPath = data.objectPath.str;
+	_isResolved = false;
 
 	return true;
 }
 
+bool ObjectReferenceVariableModifier::setValue(const DynamicValue &value) {
+	if (value.getType() == DynamicValueTypes::kNull) {
+		_object.reset();
+		_objectPath.clear();
+		_isResolved = true;
+	} else if (value.getType() == DynamicValueTypes::kObject) {
+		_object = value.getObject();
+		_objectPath.clear();
+		_isResolved = true;
+	} else {
+		return false;
+	}
+
+	return true;
+}
+
+void ObjectReferenceVariableModifier::getValue(DynamicValue &dest) const {
+	if (_isResolved) {
+		if (!_object)
+			dest.clear();
+		else
+			dest.setObject(_object);
+	} else {
+		error("Resolving default objects from variable modifiers is not implemented!");
+		dest.clear();
+	}
+}
+
 Common::SharedPtr<Modifier> ObjectReferenceVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new ObjectReferenceVariableModifier(*this));
 }
@@ -146,6 +178,9 @@ Common::SharedPtr<Modifier> MidiModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new MidiModifier(*this));
 }
 
+ListVariableModifier::ListVariableModifier() : _list(new DynamicList()) {
+}
+
 bool ListVariableModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::ListVariableModifier &data) {
 	if (!data.havePersistentData || data.numValues == 0)
 		return true;	// If the list is empty then we don't care, the actual value type is irrelevant because it can be reassigned
@@ -197,7 +232,7 @@ bool ListVariableModifier::load(const PlugInModifierLoaderContext &context, cons
 			return false;
 		}
 
-		if (!_list.setAtIndex(i, dynValue)) {
+		if (!_list->setAtIndex(i, dynValue)) {
 			warning("Failed to initialize list modifier, value was rejected");
 			return false;
 		}
@@ -206,6 +241,25 @@ bool ListVariableModifier::load(const PlugInModifierLoaderContext &context, cons
 	return true;
 }
 
+bool ListVariableModifier::setValue(const DynamicValue &value) {
+	if (value.getType() == DynamicValueTypes::kList)
+		_list = value.getList()->clone();
+	else
+		return false;
+
+	return true;
+}
+
+void ListVariableModifier::getValue(DynamicValue &dest) const {
+	dest.setList(_list);
+}
+
+
+ListVariableModifier::ListVariableModifier(const ListVariableModifier &other) {
+	if (other._list)
+		_list = other._list->clone();
+}
+
 Common::SharedPtr<Modifier> ListVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new ListVariableModifier(*this));
 }
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index 3183e81e53f..41156684896 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -87,8 +87,13 @@ private:
 
 class ObjectReferenceVariableModifier : public VariableModifier {
 public:
+	ObjectReferenceVariableModifier();
+
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::ObjectReferenceVariableModifier &data);
 
+	bool setValue(const DynamicValue &value) override;
+	void getValue(DynamicValue &dest) const override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Object Reference Variable Modifier"; }
 #endif
@@ -97,7 +102,10 @@ private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
 	Event _setToSourceParentWhen;
-	Common::String _objectPath;
+
+	mutable Common::WeakPtr<RuntimeObject> _object;
+	mutable Common::String _objectPath;
+	mutable bool _isResolved;
 };
 
 class MidiModifier : public Modifier {
@@ -149,16 +157,24 @@ private:
 
 class ListVariableModifier : public VariableModifier {
 public:
+	ListVariableModifier();
+
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::ListVariableModifier &data);
 
+	bool setValue(const DynamicValue &value) override;
+	void getValue(DynamicValue &dest) const override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "List Variable Modifier"; }
 #endif
 
 private:
+	ListVariableModifier(const ListVariableModifier &other);
+	ListVariableModifier &operator=(const ListVariableModifier &other);
+
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
-	DynamicList _list;
+	Common::SharedPtr<DynamicList> _list;
 };
 
 class SysInfoModifier : public Modifier {
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 65cfaaee94b..024958962e4 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -294,6 +294,51 @@ bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const C
 	return true;
 }
 
+
+void DynamicListValueExporter::exportValue(DynamicValue &dynValue, const int32 &value) {
+	dynValue.setInt(value);
+}
+
+void DynamicListValueExporter::exportValue(DynamicValue &dynValue, const double &value) {
+	dynValue.setFloat(value);
+}
+
+void DynamicListValueExporter::exportValue(DynamicValue &dynValue, const Point16 &value) {
+	dynValue.setPoint(value);
+}
+
+void DynamicListValueExporter::exportValue(DynamicValue &dynValue, const IntRange &value) {
+	dynValue.setIntRange(value);
+}
+
+void DynamicListValueExporter::exportValue(DynamicValue &dynValue, const bool &value) {
+	dynValue.setBool(value);
+}
+
+void DynamicListValueExporter::exportValue(DynamicValue &dynValue, const AngleMagVector &value) {
+	dynValue.setVector(value);
+}
+
+void DynamicListValueExporter::exportValue(DynamicValue &dynValue, const Label &value) {
+	dynValue.setLabel(value);
+}
+
+void DynamicListValueExporter::exportValue(DynamicValue &dynValue, const Event &value) {
+	dynValue.setEvent(value);
+}
+
+void DynamicListValueExporter::exportValue(DynamicValue &dynValue, const Common::String &value) {
+	dynValue.setString(value);
+}
+
+void DynamicListValueExporter::exportValue(DynamicValue &dynValue, const Common::SharedPtr<DynamicList> &value) {
+	dynValue.setList(value);
+}
+
+void DynamicListValueExporter::exportValue(DynamicValue &dynValue, const Common::WeakPtr<RuntimeObject> &value) {
+	dynValue.setObject(value);
+}
+
 DynamicListContainer<void>::DynamicListContainer() : _size(0) {
 }
 
@@ -301,6 +346,11 @@ bool DynamicListContainer<void>::setAtIndex(size_t index, const DynamicValue &dy
 	return true;
 }
 
+bool DynamicListContainer<void>::getAtIndex(size_t index, DynamicValue &dynValue) const {
+	dynValue.clear();
+	return true;
+}
+
 void DynamicListContainer<void>::setFrom(const DynamicListContainerBase &other) {
 	_size = other.getSize(); // ... the only thing we have anyway...
 }
@@ -317,6 +367,10 @@ bool DynamicListContainer<void>::compareEqual(const DynamicListContainerBase &ot
 	return true;
 }
 
+DynamicListContainerBase *DynamicListContainer<void>::clone() const {
+	return new DynamicListContainer<void>(*this);
+}
+
 bool DynamicListContainer<VarReference>::setAtIndex(size_t index, const DynamicValue &dynValue) {
 	if (dynValue.getType() != DynamicValueTypes::kVariableReference)
 		return false;
@@ -346,6 +400,15 @@ bool DynamicListContainer<VarReference>::setAtIndex(size_t index, const DynamicV
 	return true;
 }
 
+bool DynamicListContainer<VarReference>::getAtIndex(size_t index, DynamicValue &dynValue) const {
+	// TODO: Refactor this whole thing to use linkInternalReferences
+	if (index >= _array.size())
+		return false;
+
+	assert(false);
+	return false;
+}
+
 void DynamicListContainer<VarReference>::setFrom(const DynamicListContainerBase &other) {
 	const DynamicListContainer<VarReference> &otherTyped = static_cast<const DynamicListContainer<VarReference> &>(other);
 
@@ -367,6 +430,10 @@ bool DynamicListContainer<VarReference>::compareEqual(const DynamicListContainer
 	return _array == otherTyped._array;
 }
 
+DynamicListContainerBase *DynamicListContainer<VarReference>::clone() const {
+	return new DynamicListContainer<VarReference>(*this);
+}
+
 void DynamicListContainer<VarReference>::rebuildStringPointers() {
 	assert(_strings.size() == _array.size());
 
@@ -455,6 +522,13 @@ bool DynamicList::setAtIndex(size_t index, const DynamicValue &value) {
 	}
 }
 
+bool DynamicList::getAtIndex(size_t index, DynamicValue &value) const {
+	if (_container == nullptr || index >= _container->getSize())
+		return false;
+
+	return _container->getAtIndex(index, value);
+}
+
 size_t DynamicList::getSize() const {
 	if (!_container)
 		return 0;
@@ -462,6 +536,22 @@ size_t DynamicList::getSize() const {
 		return _container->getSize();
 }
 
+bool DynamicList::dynamicValueToIndex(size_t &outIndex, const DynamicValue &value) {
+	if (value.getType() == DynamicValueTypes::kFloat) {
+		double rounded = floor(value.getFloat() + 0.5);
+		if (!isfinite(rounded) || rounded < 1.0 || rounded > UINT32_MAX)
+			return false;
+
+		outIndex = static_cast<size_t>(rounded);
+	} else if (value.getType() == DynamicValueTypes::kInteger) {
+		int32 i = value.getInt();
+		if (i < 1)
+			return false;
+		static_cast<size_t>(i - 1);
+	}
+	return true;
+}
+
 DynamicList &DynamicList::operator=(const DynamicList &other) {
 	if (this != &other) {
 		clear();
@@ -500,6 +590,15 @@ void DynamicList::swap(DynamicList &other) {
 	other._container = tempContainer;
 }
 
+Common::SharedPtr<DynamicList> DynamicList::clone() const {
+	Common::SharedPtr<DynamicList> clonedList(new DynamicList());
+
+	if (_container)
+		clonedList->_container = _container->clone();
+
+	return clonedList;
+}
+
 bool DynamicList::changeToType(DynamicValueTypes::DynamicValueType type) {
 	switch (type) {
 	case DynamicValueTypes::kNull:
@@ -754,6 +853,26 @@ const Common::WeakPtr<RuntimeObject> &DynamicValue::getObject() const {
 	return _obj;
 }
 
+const DynamicValueReadProxy& DynamicValue::getReadProxy() const {
+	assert(_type == DynamicValueTypes::kReadProxy);
+	return _value.asReadProxy;
+}
+
+const DynamicValueWriteProxy &DynamicValue::getWriteProxy() const {
+	assert(_type == DynamicValueTypes::kWriteProxy);
+	return _value.asWriteProxy;
+}
+
+const Common::SharedPtr<DynamicList> &DynamicValue::getReadProxyList() const {
+	assert(_type == DynamicValueTypes::kReadProxy);
+	return _list;
+}
+
+const Common::SharedPtr<DynamicList> &DynamicValue::getWriteProxyList() const {
+	assert(_type == DynamicValueTypes::kWriteProxy);
+	return _list;
+}
+
 void DynamicValue::setInt(int32 value) {
 	if (_type != DynamicValueTypes::kInteger)
 		clear();
@@ -833,6 +952,24 @@ void DynamicValue::setList(const Common::SharedPtr<DynamicList> &value) {
 	_list = value;
 }
 
+void DynamicValue::setReadProxy(const Common::SharedPtr<DynamicList> &list, const DynamicValueReadProxy &readProxy) {
+	Common::SharedPtr<DynamicList> listRef = list;	// Back up list ref in case this is a self-assign
+	if (_type != DynamicValueTypes::kReadProxy)
+		clear();
+	_type = DynamicValueTypes::kReadProxy;
+	_value.asReadProxy = readProxy;
+	_list = listRef;
+}
+
+void DynamicValue::setWriteProxy(const Common::SharedPtr<DynamicList> &list, const DynamicValueWriteProxy &writeProxy) {
+	Common::SharedPtr<DynamicList> listRef = list; // Back up list ref in case this is a self-assign
+	if (_type != DynamicValueTypes::kWriteProxy)
+		clear();
+	_type = DynamicValueTypes::kWriteProxy;
+	_value.asWriteProxy = writeProxy;
+	_list = listRef;
+}
+
 void DynamicValue::setObject(const Common::WeakPtr<RuntimeObject> &value) {
 	if (_type != DynamicValueTypes::kObject)
 		clear();
@@ -1170,11 +1307,19 @@ bool RuntimeObject::isElement() const {
 	return false;
 }
 
-bool RuntimeObject::readAttribute(DynamicValue &result, const Common::String &attrib, const DynamicValue *optionalIndex) {
+bool RuntimeObject::readAttribute(DynamicValue &result, const Common::String &attrib) {
+	return false;
+}
+
+bool RuntimeObject::readAttributeIndexed(DynamicValue& result, const Common::String& attrib, const DynamicValue& index) {
+	return false;
+}
+
+bool RuntimeObject::writeRefAttribute(DynamicValueWriteProxy& writeProxy, const Common::String& attrib) {
 	return false;
 }
 
-bool RuntimeObject::setAttribute(const Common::String &attrib, const DynamicValue *optionalIndex, const DynamicValue &value) {
+bool RuntimeObject::writeRefAttributeIndexed(DynamicValueWriteProxy& writeProxy, const Common::String& attrib, const DynamicValue& index) {
 	return false;
 }
 
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 1f7908e2098..04ea8648063 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -54,6 +54,8 @@ namespace MTropolis {
 class Asset;
 class CursorGraphic;
 class CursorGraphicCollection;
+struct DynamicValueReadProxy;
+struct DynamicValueWriteProxy;
 class Element;
 class MessageDispatch;
 class Modifier;
@@ -110,6 +112,8 @@ enum DynamicValueType {
 	kString,
 	kList,
 	kObject,
+	kReadProxy,
+	kWriteProxy,
 
 	kEmpty,
 };
@@ -330,14 +334,40 @@ struct MessageFlags {
 struct DynamicValue;
 struct DynamicList;
 
+struct IDynamicValueReadInterface {
+	virtual bool read(DynamicValue &dest, const void *objectRef, uintptr_t ptrOrOffset) const = 0;
+	virtual bool readAttrib(DynamicValue &dest, const void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const = 0;
+	virtual bool readAttribIndexed(DynamicValue &dest, const void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const = 0;
+};
+
+struct IDynamicValueWriteInterface {
+	virtual bool write(const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const = 0;
+	virtual bool refAttrib(DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const = 0;
+	virtual bool refAttribIndexed(DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const = 0;
+};
+
+struct DynamicValueReadProxy {
+	uintptr_t ptrOrOffset;
+	const void *objectRef;
+	IDynamicValueReadInterface *ifc;
+};
+
+struct DynamicValueWriteProxy {
+	uintptr_t ptrOrOffset;
+	void *objectRef;
+	IDynamicValueWriteInterface *ifc;
+};
+
 class DynamicListContainerBase {
 public:
 	virtual ~DynamicListContainerBase();
 	virtual bool setAtIndex(size_t index, const DynamicValue &dynValue) = 0;
+	virtual bool getAtIndex(size_t index, DynamicValue &dynValue) const = 0;
 	virtual void setFrom(const DynamicListContainerBase &other) = 0; // Only supports setting same type!
 	virtual const void *getConstArrayPtr() const = 0;
 	virtual size_t getSize() const = 0;
 	virtual bool compareEqual(const DynamicListContainerBase &other) const = 0;
+	virtual DynamicListContainerBase *clone() const = 0;
 };
 
 struct DynamicListDefaultSetter {
@@ -368,14 +398,30 @@ struct DynamicListValueImporter {
 	static bool importValue(const DynamicValue &dynValue, const Common::WeakPtr<RuntimeObject> *&outPtr);
 };
 
+struct DynamicListValueExporter {
+	static void exportValue(DynamicValue &dynValue, const int32 &value);
+	static void exportValue(DynamicValue &dynValue, const double &value);
+	static void exportValue(DynamicValue &dynValue, const Point16 &value);
+	static void exportValue(DynamicValue &dynValue, const IntRange &value);
+	static void exportValue(DynamicValue &dynValue, const bool &value);
+	static void exportValue(DynamicValue &dynValue, const AngleMagVector &value);
+	static void exportValue(DynamicValue &dynValue, const Label &value);
+	static void exportValue(DynamicValue &dynValue, const Event &value);
+	static void exportValue(DynamicValue &dynValue, const Common::String &value);
+	static void exportValue(DynamicValue &dynValue, const Common::SharedPtr<DynamicList> &value);
+	static void exportValue(DynamicValue &dynValue, const Common::WeakPtr<RuntimeObject> &value);
+};
+
 template<class T>
 class DynamicListContainer : public DynamicListContainerBase {
 public:
 	bool setAtIndex(size_t index, const DynamicValue &dynValue) override;
+	bool getAtIndex(size_t index, DynamicValue &dynValue) const override;
 	void setFrom(const DynamicListContainerBase &other) override;
 	const void *getConstArrayPtr() const override;
 	size_t getSize() const override;
 	bool compareEqual(const DynamicListContainerBase &other) const override;
+	DynamicListContainerBase *clone() const override;
 
 private:
 	Common::Array<T> _array;
@@ -387,10 +433,12 @@ public:
 	DynamicListContainer();
 
 	bool setAtIndex(size_t index, const DynamicValue &dynValue) override;
+	bool getAtIndex(size_t index, DynamicValue &dynValue) const override;
 	void setFrom(const DynamicListContainerBase &other) override;
 	const void *getConstArrayPtr() const override;
 	size_t getSize() const override;
 	bool compareEqual(const DynamicListContainerBase &other) const override;
+	DynamicListContainerBase *clone() const override;
 
 public:
 	size_t _size;
@@ -400,10 +448,12 @@ template<>
 class DynamicListContainer<VarReference> : public DynamicListContainerBase {
 public:
 	bool setAtIndex(size_t index, const DynamicValue &dynValue) override;
+	bool getAtIndex(size_t index, DynamicValue &dynValue) const override;
 	void setFrom(const DynamicListContainerBase &other) override;
 	const void *getConstArrayPtr() const override;
 	size_t getSize() const override;
 	bool compareEqual(const DynamicListContainerBase &other) const override;
+	DynamicListContainerBase *clone() const override;
 
 private:
 	void rebuildStringPointers();
@@ -435,6 +485,15 @@ bool DynamicListContainer<T>::setAtIndex(size_t index, const DynamicValue &dynVa
 	return true;
 }
 
+template<class T>
+bool DynamicListContainer<T>::getAtIndex(size_t index, DynamicValue &dynValue) const {
+	if (index >= _array.size())
+		return false;
+
+	DynamicListValueExporter::exportValue(dynValue, _array[index]);
+	return true;
+}
+
 template<class T>
 void DynamicListContainer<T>::setFrom(const DynamicListContainerBase &other) {
 	_array = static_cast<const DynamicListContainer<T> &>(other)._array;
@@ -456,6 +515,11 @@ bool DynamicListContainer<T>::compareEqual(const DynamicListContainerBase &other
 	return _array == otherTyped._array;
 }
 
+template<class T>
+DynamicListContainerBase *DynamicListContainer<T>::clone() const {
+	return new DynamicListContainer<T>(*this);
+}
+
 struct DynamicList {
 	DynamicList();
 	DynamicList(const DynamicList &other);
@@ -474,9 +538,12 @@ struct DynamicList {
 	const Common::Array<Common::String> &getString() const;
 	const Common::Array<bool> &getBool() const;
 
+	bool getAtIndex(size_t index, DynamicValue &value) const;
 	bool setAtIndex(size_t index, const DynamicValue &value);
 	size_t getSize() const;
 
+	static bool dynamicValueToIndex(size_t &outIndex, const DynamicValue &value);
+
 	DynamicList &operator=(const DynamicList &other);
 
 	bool operator==(const DynamicList &other) const;
@@ -521,6 +588,10 @@ struct DynamicValue {
 	const bool &getBool() const;
 	const Common::SharedPtr<DynamicList> &getList() const;
 	const Common::WeakPtr<RuntimeObject> &getObject() const;
+	const DynamicValueReadProxy &getReadProxy() const;
+	const DynamicValueWriteProxy &getWriteProxy() const;
+	const Common::SharedPtr<DynamicList> &getReadProxyList() const;
+	const Common::SharedPtr<DynamicList> &getWriteProxyList() const;
 
 	void clear();
 
@@ -536,6 +607,8 @@ struct DynamicValue {
 	void setBool(bool value);
 	void setList(const Common::SharedPtr<DynamicList> &value);
 	void setObject(const Common::WeakPtr<RuntimeObject> &value);
+	void setReadProxy(const Common::SharedPtr<DynamicList> &list, const DynamicValueReadProxy &readProxy);
+	void setWriteProxy(const Common::SharedPtr<DynamicList> &list, const DynamicValueWriteProxy &writeProxy);
 
 	DynamicValue &operator=(const DynamicValue &other);
 
@@ -557,6 +630,8 @@ private:
 		Event asEvent;
 		Point16 asPoint;
 		bool asBool;
+		DynamicValueReadProxy asReadProxy;
+		DynamicValueWriteProxy asWriteProxy;
 	};
 
 	template<class T>
@@ -944,8 +1019,10 @@ public:
 	virtual bool isModifier() const;
 	virtual bool isElement() const;
 
-	virtual bool readAttribute(DynamicValue &result, const Common::String &attrib, const DynamicValue *optionalIndex);
-	virtual bool setAttribute(const Common::String &attrib, const DynamicValue *optionalIndex, const DynamicValue &value);
+	virtual bool readAttribute(DynamicValue &result, const Common::String &attrib);
+	virtual bool readAttributeIndexed(DynamicValue &result, const Common::String &attrib, const DynamicValue &index);
+	virtual bool writeRefAttribute(DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
+	virtual bool writeRefAttributeIndexed(DynamicValueWriteProxy &writeProxy, const Common::String &attrib, const DynamicValue &index);
 
 protected:
 	// This is the static GUID stored in the data, it is not guaranteed
@@ -1350,6 +1427,8 @@ protected:
 class VariableModifier : public Modifier {
 public:
 	virtual bool isVariable() const;
+	virtual bool setValue(const DynamicValue &value) = 0;
+	virtual void getValue(DynamicValue &dest) const = 0;
 };
 
 class Asset {


Commit: 8010e6dff1e56a61af7b87a2595e88d4e1ee9685
    https://github.com/scummvm/scummvm/commit/8010e6dff1e56a61af7b87a2595e88d4e1ee9685
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix point and variable vector factories being reversed

Changed paths:
    engines/mtropolis/modifier_factory.cpp


diff --git a/engines/mtropolis/modifier_factory.cpp b/engines/mtropolis/modifier_factory.cpp
index 8221d07b8f2..7f1d0340f96 100644
--- a/engines/mtropolis/modifier_factory.cpp
+++ b/engines/mtropolis/modifier_factory.cpp
@@ -107,9 +107,9 @@ IModifierFactory *getModifierFactoryForDataObjectType(const Data::DataObjectType
 		return ModifierFactory<IntegerVariableModifier, Data::IntegerVariableModifier>::getInstance();
 	case Data::DataObjectTypes::kIntegerRangeVariableModifier:
 		return ModifierFactory<IntegerRangeVariableModifier, Data::IntegerRangeVariableModifier>::getInstance();
-	case Data::DataObjectTypes::kPointVariableModifier:
-		return ModifierFactory<VectorVariableModifier, Data::VectorVariableModifier>::getInstance();
 	case Data::DataObjectTypes::kVectorVariableModifier:
+		return ModifierFactory<VectorVariableModifier, Data::VectorVariableModifier>::getInstance();
+	case Data::DataObjectTypes::kPointVariableModifier:
 		return ModifierFactory<PointVariableModifier, Data::PointVariableModifier>::getInstance();
 	case Data::DataObjectTypes::kFloatingPointVariableModifier:
 		return ModifierFactory<FloatingPointVariableModifier, Data::FloatingPointVariableModifier>::getInstance();


Commit: 79041d4df461aafd5f489069f929a5d15743c856
    https://github.com/scummvm/scummvm/commit/79041d4df461aafd5f489069f929a5d15743c856
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Implement SysInfo modifier

Changed paths:
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index d6fe2d549c5..fd8caebfc37 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -456,17 +456,30 @@ MiniscriptInstructionOutcome Set::execute(MiniscriptThread *thread) const {
 	const MiniscriptStackValue &srcValue = thread->getStackValueFromTop(0);
 	MiniscriptStackValue &target = thread->getStackValueFromTop(1);
 
-	if (target.value.getType() != DynamicValueTypes::kWriteProxy) {
-		thread->error("Can't assign to rvalue");
-		return kMiniscriptInstructionOutcomeFailed;
-	}
+	if (target.value.getType() == DynamicValueTypes::kWriteProxy) {
+		const DynamicValueWriteProxy &proxy = target.value.getWriteProxy();
+		if (!proxy.ifc->write(srcValue.value, proxy.objectRef, proxy.ptrOrOffset)) {
+			thread->error("Failed to assign value");
+			return kMiniscriptInstructionOutcomeFailed;
+		}
+	} else {
+		VariableModifier *var = nullptr;
+		if (target.value.getType() == DynamicValueTypes::kObject) {
+			Common::SharedPtr<RuntimeObject> obj = target.value.getObject().lock();
+			if (obj && obj->isModifier() && static_cast<const Modifier *>(obj.get())->isVariable())
+				var = static_cast<VariableModifier *>(obj.get());
+		}
 
-	const DynamicValueWriteProxy &proxy = target.value.getWriteProxy();
-	if (!proxy.ifc->write(srcValue.value, proxy.objectRef, proxy.ptrOrOffset)) {
-		thread->error("Failed to assign value");
-		return kMiniscriptInstructionOutcomeFailed;
+		if (var != nullptr) {
+			if (!var->setValue(srcValue.value)) {
+				thread->error("Couldn't assign value to variable, probably wrong type");
+				return kMiniscriptInstructionOutcomeFailed;
+			}
+		} else {
+			thread->error("Can't assign to rvalue");
+			return kMiniscriptInstructionOutcomeFailed;
+		}
 	}
-
 	thread->popValues(2);
 
 	return kMiniscriptInstructionOutcomeContinue;
@@ -514,7 +527,7 @@ MiniscriptInstructionOutcome GetChild::execute(MiniscriptThread *thread) const {
 				}
 
 				DynamicValueWriteProxy proxy;
-				if (!obj->writeRefAttributeIndexed(proxy, attrib, indexSlot.value)) {
+				if (!obj->writeRefAttributeIndexed(thread, proxy, attrib, indexSlot.value)) {
 					thread->error("Failed to get a writeable reference to attribute '" + attrib + "'");
 					return kMiniscriptInstructionOutcomeFailed;
 				}
@@ -551,7 +564,7 @@ MiniscriptInstructionOutcome GetChild::execute(MiniscriptThread *thread) const {
 					return kMiniscriptInstructionOutcomeFailed;
 				}
 
-				if (!obj->readAttribute(indexableValueSlot.value, attrib)) {
+				if (!obj->readAttribute(thread, indexableValueSlot.value, attrib)) {
 					thread->error("Failed to read attribute '" + attrib + "'");
 					return kMiniscriptInstructionOutcomeFailed;
 				}
@@ -603,7 +616,7 @@ MiniscriptInstructionOutcome GetChild::readRValueAttrib(MiniscriptThread *thread
 			if (!obj) {
 				thread->error("Unable to read attrib '" + attrib + "' from invalid object");
 				return kMiniscriptInstructionOutcomeFailed;
-			} else if (!obj->readAttribute(valueSrcDest, attrib)) {
+			} else if (!obj->readAttribute(thread, valueSrcDest, attrib)) {
 				thread->error("Unable to read attrib '" + attrib + "'");
 				return kMiniscriptInstructionOutcomeFailed;
 			}
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 6ed854ec325..ea0d5750241 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -22,6 +22,8 @@
 #include "mtropolis/plugin/standard.h"
 #include "mtropolis/plugins.h"
 
+#include "mtropolis/miniscript.h"
+
 
 namespace MTropolis {
 
@@ -118,7 +120,7 @@ bool ObjectReferenceVariableModifier::setValue(const DynamicValue &value) {
 
 void ObjectReferenceVariableModifier::getValue(DynamicValue &dest) const {
 	if (_isResolved) {
-		if (!_object)
+		if (_object.expired())
 			dest.clear();
 		else
 			dest.setObject(_object);
@@ -268,6 +270,44 @@ bool SysInfoModifier::load(const PlugInModifierLoaderContext &context, const Dat
 	return true;
 }
 
+bool SysInfoModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "bitdepth") {
+		ColorDepthMode colorDepth = thread->getRuntime()->getFakeColorDepth();
+		switch (colorDepth) {
+		case kColorDepthMode1Bit:
+			result.setInt(1);
+			break;
+		case kColorDepthMode2Bit:
+			result.setInt(2);
+			break;
+		case kColorDepthMode4Bit:
+			result.setInt(4);
+			break;
+		case kColorDepthMode8Bit:
+			result.setInt(8);
+			break;
+		case kColorDepthMode16Bit:
+			result.setInt(16);
+			break;
+		case kColorDepthMode32Bit:
+			result.setInt(32);
+			break;
+		default:
+			return false;
+		}
+
+		return true;
+	} else if (attrib == "screensize") {
+		uint16 width, height;
+		thread->getRuntime()->getDisplayResolution(width, height);
+		result.setPoint(Point16::create(width, height));
+		return true;
+	}
+
+	return false;
+}
+
+
 Common::SharedPtr<Modifier> SysInfoModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new SysInfoModifier(*this));
 }
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index 41156684896..ea1cb4055a0 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -29,6 +29,8 @@
 
 namespace MTropolis {
 
+class Runtime;
+
 namespace Standard {
 
 class StandardPlugIn;
@@ -181,6 +183,8 @@ class SysInfoModifier : public Modifier {
 public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::SysInfoModifier &data);
 
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "System Info Modifier"; }
 #endif
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 024958962e4..22bc98aa88d 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -1307,19 +1307,19 @@ bool RuntimeObject::isElement() const {
 	return false;
 }
 
-bool RuntimeObject::readAttribute(DynamicValue &result, const Common::String &attrib) {
+bool RuntimeObject::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
 	return false;
 }
 
-bool RuntimeObject::readAttributeIndexed(DynamicValue& result, const Common::String& attrib, const DynamicValue& index) {
+bool RuntimeObject::readAttributeIndexed(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib, const DynamicValue &index) {
 	return false;
 }
 
-bool RuntimeObject::writeRefAttribute(DynamicValueWriteProxy& writeProxy, const Common::String& attrib) {
+bool RuntimeObject::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
 	return false;
 }
 
-bool RuntimeObject::writeRefAttributeIndexed(DynamicValueWriteProxy& writeProxy, const Common::String& attrib, const DynamicValue& index) {
+bool RuntimeObject::writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib, const DynamicValue &index) {
 	return false;
 }
 
@@ -1570,7 +1570,7 @@ Common::WeakPtr<RuntimeObject> ObjectLinkingScope::resolve(const Common::String
 
 Common::WeakPtr<RuntimeObject> ObjectLinkingScope::resolve(uint32 staticGUID, const Common::String &name, bool isNameAlreadyInsensitive) const {
 	Common::WeakPtr<RuntimeObject> byGUIDResult = resolve(staticGUID);
-	if (byGUIDResult)
+	if (!byGUIDResult.expired())
 		return byGUIDResult;
 	else
 		return resolve(name, isNameAlreadyInsensitive);
@@ -2233,7 +2233,7 @@ void Runtime::queueMessage(const Common::SharedPtr<MessageDispatch>& dispatch) {
 
 void Runtime::ensureMainWindowExists() {
 	// Maybe there's a better spot for this
-	if (!_mainWindow && _project) {
+	if (_mainWindow.expired() && _project) {
 		const ProjectPresentationSettings &presentationSettings = _project->getPresentationSettings();
 
 		int32 centeredX = (static_cast<int32>(_displayWidth) - static_cast<int32>(presentationSettings.width)) / 2;
@@ -2255,7 +2255,7 @@ void Runtime::unloadProject() {
 	_messageQueue.clear();
 	_vthread.reset(new VThread());
 
-	if (_mainWindow) {
+	if (!_mainWindow.expired()) {
 		removeWindow(_mainWindow.lock().get());
 	}
 
@@ -2335,6 +2335,14 @@ void Runtime::getDisplayResolution(uint16 &outWidth, uint16 &outHeight) const {
 	outHeight = _displayHeight;
 }
 
+ColorDepthMode Runtime::getRealColorDepth() const {
+	return _realDisplayMode;
+}
+
+ColorDepthMode Runtime::getFakeColorDepth() const {
+	return _fakeDisplayMode;
+}
+
 const Graphics::PixelFormat& Runtime::getRenderPixelFormat() const {
 	assert(_realDisplayMode != kColorDepthModeInvalid);
 
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 04ea8648063..41a803b29a9 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -58,6 +58,7 @@ struct DynamicValueReadProxy;
 struct DynamicValueWriteProxy;
 class Element;
 class MessageDispatch;
+class MiniscriptThread;
 class Modifier;
 class PlugInModifier;
 class RuntimeObject;
@@ -218,6 +219,13 @@ struct Point16 {
 	inline bool operator!=(const Point16 &other) const {
 		return !((*this) == other);
 	}
+
+	inline static Point16 create(int16 x, int16 y) {
+		Point16 result;
+		result.x = x;
+		result.y = y;
+		return result;
+	}
 };
 
 struct Rect16 {
@@ -883,7 +891,11 @@ public:
 	void setDisplayResolution(uint16 width, uint16 height);
 	void getDisplayResolution(uint16 &outWidth, uint16 &outHeight) const;
 
+	ColorDepthMode getRealColorDepth() const;
+	ColorDepthMode getFakeColorDepth() const;	// Fake color depth that will be reported to scripts
+
 	const Graphics::PixelFormat &getRenderPixelFormat() const;
+
 	const Common::SharedPtr<Graphics::MacFontManager> &getMacFontManager() const;
 
 	const Common::SharedPtr<Structural> &getActiveMainScene() const;
@@ -1019,10 +1031,10 @@ public:
 	virtual bool isModifier() const;
 	virtual bool isElement() const;
 
-	virtual bool readAttribute(DynamicValue &result, const Common::String &attrib);
-	virtual bool readAttributeIndexed(DynamicValue &result, const Common::String &attrib, const DynamicValue &index);
-	virtual bool writeRefAttribute(DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
-	virtual bool writeRefAttributeIndexed(DynamicValueWriteProxy &writeProxy, const Common::String &attrib, const DynamicValue &index);
+	virtual bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
+	virtual bool readAttributeIndexed(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib, const DynamicValue &index);
+	virtual bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
+	virtual bool writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib, const DynamicValue &index);
 
 protected:
 	// This is the static GUID stored in the data, it is not guaranteed


Commit: dc0fe94ef57dc9285039f73d77284f9ce457d5d2
    https://github.com/scummvm/scummvm/commit/dc0fe94ef57dc9285039f73d77284f9ce457d5d2
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Stub out some scene transition behavior

Changed paths:
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index fd8caebfc37..82368993708 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -441,7 +441,6 @@ MiniscriptInstructionOutcome UnimplementedInstruction::execute(MiniscriptThread
 	return kMiniscriptInstructionOutcomeFailed;
 }
 
-
 MiniscriptInstructionOutcome Set::execute(MiniscriptThread *thread) const {
 	if (thread->getStackSize() < 2) {
 		thread->error("Stack underflow");
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index bd4691c87e9..e9c329a6972 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -219,23 +219,6 @@ class SceneTransitionModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::SceneTransitionModifier &data);
 
-	enum TransitionType {
-		kTransitionTypePatternDissolve = 0x0406,
-		kTransitionTypeRandomDissolve  = 0x0410,	// No steps
-		kTransitionTypeFade            = 0x041a,
-		kTransitionTypeSlide           = 0x03e8,	// Directional
-		kTransitionTypePush            = 0x03f2,	// Directional
-		kTransitionTypeZoom            = 0x03fc,
-		kTransitionTypeWipe            = 0x0424,	// Directional
-	};
-
-	enum TransitionDirection {
-		kTransitionDirectionUp = 0x385,
-		kTransitionDirectionDown = 0x385,
-		kTransitionDirectionLeft = 0x386,
-		kTransitionDirectionRight = 0x387,
-	};
-
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Scene Transition Modifier"; }
 #endif
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index 14ca6becf03..a01db25bb86 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -494,7 +494,7 @@ Common::Error MTropolisEngine::run() {
 
 		_runtime->runRealFrame(realElapsedThisFrame);
 
-		if (forceDraw || !skipDraw) {
+		if (forceDraw || !skipDraw || !_runtime->mustDraw()) {
 			_runtime->drawFrame(_system);
 			numSkippedFrames = 0;
 		} else {
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 22bc98aa88d..e87df0b3c73 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -1585,6 +1585,10 @@ LowLevelSceneStateTransitionAction::LowLevelSceneStateTransitionAction(const Com
 	: _actionType(LowLevelSceneStateTransitionAction::kSendMessage), _msg(msg) {
 }
 
+LowLevelSceneStateTransitionAction::LowLevelSceneStateTransitionAction(ActionType actionType)
+	: _actionType(actionType) {
+}
+
 LowLevelSceneStateTransitionAction::LowLevelSceneStateTransitionAction(const LowLevelSceneStateTransitionAction &other)
 	: _actionType(other._actionType), _msg(other._msg), _scene(other._scene) {
 }
@@ -1762,8 +1766,7 @@ Runtime::SceneStackEntry::SceneStackEntry() {
 
 
 Runtime::Runtime() : _nextRuntimeGUID(1), _realDisplayMode(kColorDepthModeInvalid), _fakeDisplayMode(kColorDepthModeInvalid),
-	_displayWidth(1024), _displayHeight(768),
-					 _realTime(0), _playTime(0) {
+					 _displayWidth(1024), _displayHeight(768), _realTime(0), _playTime(0), _sceneTransitionState(kSceneTransitionStateNotTransitioning) {
 	_vthread.reset(new VThread());
 
 	for (int i = 0; i < kColorDepthModeCount; i++) {
@@ -1865,6 +1868,23 @@ bool Runtime::runProjectFrame() {
 			continue;
 		}
 
+		if (_sceneTransitionState == kSceneTransitionStateWaitingForDraw) {
+			if (_sceneTransitionEffect.duration == 0) {
+				// This needs to skip past the transition phase and hit the next condition
+				_sceneTransitionEndTime = _playTime;
+				_sceneTransitionState = kSceneTransitionStateTransitioning;
+			} else {
+				_sceneTransitionState = kSceneTransitionStateDrawingTargetFrame;
+				_sceneTransitionEndTime = _playTime + _sceneTransitionEffect.duration / 10;
+			}
+		}
+
+		if (_sceneTransitionState == kSceneTransitionStateTransitioning && _playTime >= _sceneTransitionEndTime) {
+			_sceneTransitionState = kSceneTransitionStateNotTransitioning;
+			_messageQueue.push_back(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSceneTransitionEnded, 0), _activeMainScene.get(), false, true)));
+			continue;
+		}
+
 		// Ran out of actions
 		break;
 	}
@@ -2050,6 +2070,11 @@ void Runtime::executeCompleteTransitionToScene(const Common::SharedPtr<Structura
 	_activeMainScene = targetScene;
 	_activeSharedScene = targetSharedScene;
 
+	// Scene transitions have to be set up by the destination scene
+	_sceneTransitionState = kSceneTransitionStateWaitingForDraw;
+	_sceneTransitionEffect.transitionType = kTransitionTypeNone;
+	_sceneTransitionEffect.duration = 0;
+
 	executeSharedScenePostSceneChangeActions();
 }
 
@@ -2361,6 +2386,12 @@ const Common::SharedPtr<Structural> &Runtime::getActiveSharedScene() const {
 	return _activeSharedScene;
 }
 
+bool Runtime::mustDraw() const {
+	if (_sceneTransitionState == kSceneTransitionStateWaitingForDraw)
+		return true;
+	return false;
+}
+
 uint64 Runtime::getRealTime() const {
 	return _realTime;
 }
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 41a803b29a9..cf8910c2f36 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -94,6 +94,24 @@ enum ColorDepthMode {
 	kColorDepthModeInvalid,
 };
 
+enum TransitionType {
+	kTransitionTypeNone = 0,
+	kTransitionTypePatternDissolve = 0x0406,
+	kTransitionTypeRandomDissolve = 0x0410, // No steps
+	kTransitionTypeFade = 0x041a,
+	kTransitionTypeSlide = 0x03e8, // Directional
+	kTransitionTypePush = 0x03f2,  // Directional
+	kTransitionTypeZoom = 0x03fc,
+	kTransitionTypeWipe = 0x0424, // Directional
+};
+
+enum TransitionDirection {
+	kTransitionDirectionUp = 0x385,
+	kTransitionDirectionDown = 0x385,
+	kTransitionDirectionLeft = 0x386,
+	kTransitionDirectionRight = 0x387,
+};
+
 namespace DynamicValueTypes {
 
 enum DynamicValueType {
@@ -794,6 +812,7 @@ struct LowLevelSceneStateTransitionAction {
 	};
 
 	explicit LowLevelSceneStateTransitionAction(const Common::SharedPtr<MessageDispatch> &msg);
+	explicit LowLevelSceneStateTransitionAction(ActionType actionType);
 	LowLevelSceneStateTransitionAction(const LowLevelSceneStateTransitionAction &other);
 	LowLevelSceneStateTransitionAction(const Common::SharedPtr<Structural> &scene, ActionType actionType);
 
@@ -823,6 +842,13 @@ struct HighLevelSceneTransition {
 	bool addToReturnList;
 };
 
+struct SceneTransitionEffect {
+	uint32 duration; // 6000000 is maximum
+	uint16 steps;
+	TransitionType transitionType;
+	TransitionDirection transitionDirection;
+};
+
 class MessageDispatch {
 public:
 	MessageDispatch(const Event &evt, Structural *root, bool cascade, bool relay);
@@ -901,6 +927,8 @@ public:
 	const Common::SharedPtr<Structural> &getActiveMainScene() const;
 	const Common::SharedPtr<Structural> &getActiveSharedScene() const;
 
+	bool mustDraw() const;
+
 	uint64 getRealTime() const;
 	uint64 getPlayTime() const;
 
@@ -913,6 +941,13 @@ public:
 #endif
 
 private:
+	enum SceneTransitionState {
+		kSceneTransitionStateNotTransitioning,
+		kSceneTransitionStateWaitingForDraw,
+		kSceneTransitionStateDrawingTargetFrame,
+		kSceneTransitionStateTransitioning,
+	};
+
 	struct SceneStackEntry {
 		SceneStackEntry();
 
@@ -973,6 +1008,10 @@ private:
 	Common::SharedPtr<Structural> _activeSharedScene;
 	Common::Array<SceneReturnListEntry> _sceneReturnList;
 
+	SceneTransitionState _sceneTransitionState;
+	SceneTransitionEffect _sceneTransitionEffect;
+	uint32 _sceneTransitionEndTime;
+
 	Common::WeakPtr<Window> _mainWindow;
 	Common::Array<Common::SharedPtr<Window> > _windows;
 


Commit: b5ebc6eb366481a7a80bb9e730b87ca7f9211f4c
    https://github.com/scummvm/scummvm/commit/b5ebc6eb366481a7a80bb9e730b87ca7f9211f4c
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: More work to get splash screen working

Changed paths:
    engines/mtropolis/asset_factory.cpp
    engines/mtropolis/assets.cpp
    engines/mtropolis/assets.h
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/debug.cpp
    engines/mtropolis/element_factory.cpp
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/miniscript.h
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/asset_factory.cpp b/engines/mtropolis/asset_factory.cpp
index 7e2fbde2067..e67080f720e 100644
--- a/engines/mtropolis/asset_factory.cpp
+++ b/engines/mtropolis/asset_factory.cpp
@@ -61,6 +61,8 @@ IAssetFactory *getAssetFactoryForDataObjectType(const Data::DataObjectTypes::Dat
 		return AssetFactory<ColorTableAsset, Data::ColorTableAsset>::getInstance();
 	case Data::DataObjectTypes::kAudioAsset:
 		return AssetFactory<AudioAsset, Data::AudioAsset>::getInstance();
+	case Data::DataObjectTypes::kMovieAsset:
+		return AssetFactory<MovieAsset, Data::MovieAsset>::getInstance();
 
 	default:
 		return nullptr;
diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index 028a2814d8a..e68f5e6018f 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -73,5 +73,14 @@ bool AudioAsset::load(AssetLoaderContext &context, const Data::AudioAsset &data)
 	return true;
 }
 
+bool MovieAsset::load(AssetLoaderContext &context, const Data::MovieAsset &data) {
+	_moovAtomPos = data.moovAtomPos;
+	_movieDataPos = data.movieDataPos;
+	_movieDataSize = data.movieDataSize;
+	_extFileName = data.extFileName;
+
+	return true;
+}
+
 
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/assets.h b/engines/mtropolis/assets.h
index 3c0a1b3a5c1..7086e4381ce 100644
--- a/engines/mtropolis/assets.h
+++ b/engines/mtropolis/assets.h
@@ -64,6 +64,18 @@ private:
 	Common::Array<CuePoint> _cuePoints;
 };
 
+class MovieAsset : public Asset {
+public:
+	bool load(AssetLoaderContext &context, const Data::MovieAsset &data);
+
+private:
+	uint32 _movieDataPos;
+	uint32 _moovAtomPos;
+	uint32 _movieDataSize;
+
+	Common::String _extFileName;
+};
+
 } // End of namespace MTropolis
 
 #endif
diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index 1a5be4766fa..abdf2d24dd6 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -150,7 +150,8 @@ bool isAsset(DataObjectType type) {
 
 } // End of namespace DataObjectTypes
 
-DataReader::DataReader(Common::SeekableReadStreamEndian &stream, ProjectFormat projectFormat) : _stream(stream), _projectFormat(projectFormat) {
+DataReader::DataReader(int64 globalPosition, Common::SeekableReadStreamEndian &stream, ProjectFormat projectFormat)
+	: _globalPosition(globalPosition), _stream(stream), _projectFormat(projectFormat) {
 }
 
 bool DataReader::readU8(uint8 &value) {
@@ -387,7 +388,7 @@ double XPFloat::toDouble() const {
 	uint64 workMantissa = this->mantissa & (((static_cast<uint64>(1) << 52) - 1u) << 11);
 	workMantissa >>= 11;
 
-	if (mantissa != 0 || exponent != 0) {
+	if (workMantissa != 0 || exponent != 0) {
 		// Adjust exponent
 		exponent -= 15360;
 		if (exponent > 2046) {
@@ -407,7 +408,7 @@ double XPFloat::toDouble() const {
 		}
 	}
 
-	uint64_t recombined = (static_cast<uint64_t>(sign) << 63) | (static_cast<uint64_t>(exponent) << 52) | static_cast<uint64_t>(mantissa);
+	uint64_t recombined = (static_cast<uint64_t>(sign) << 63) | (static_cast<uint64_t>(exponent) << 52) | static_cast<uint64_t>(workMantissa);
 
 	double d;
 	memcpy(&d, &recombined, 8);
@@ -423,17 +424,19 @@ bool Label::load(DataReader &reader) {
 	return reader.readU32(superGroupID) && reader.readU32(labelID);
 }
 
-bool InternalTypeTaggedValue::load(DataReader& reader) {
+bool InternalTypeTaggedValue::load(DataReader &reader) {
 	if (!reader.readU16(type))
 		return false;
 
+	int64 valueGlobalPos = reader.tellGlobal();
+
 	uint8 contents[44];
 	if (!reader.readBytes(contents))
 		return false;
 
 	Common::MemoryReadStreamEndian contentsStream(contents, sizeof(contents), reader.isBigEndian());
 
-	DataReader valueReader(contentsStream, reader.getProjectFormat());
+	DataReader valueReader(valueGlobalPos, contentsStream, reader.getProjectFormat());
 
 	switch (type) {
 	case kNull:
@@ -906,7 +909,7 @@ DataReadErrorCode BehaviorModifier::load(DataReader& reader) {
 	if (lengthOfName > 0 && !reader.readTerminatedStr(name, lengthOfName))
 		return kDataReadErrorReadFailed;
 
-	if (!reader.readU32(flags) || !enableWhen.load(reader) || !disableWhen.load(reader) || !reader.readBytes(unknown7))
+	if (!reader.readU32(behaviorFlags) || !enableWhen.load(reader) || !disableWhen.load(reader) || !reader.readBytes(unknown7))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
@@ -1457,6 +1460,41 @@ DataReadErrorCode ColorTableAsset::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode MovieAsset::load(DataReader &reader) {
+	if (_revision != 0)
+		return kDataReadErrorUnsupportedRevision;
+
+	haveMacPart = false;
+	haveWinPart = false;
+
+	if (!reader.readU32(persistFlags) || !reader.readU32(assetAndDataCombinedSize) || !reader.readBytes(unknown1)
+		|| !reader.readU32(assetID) || !reader.readBytes(unknown1_1) || !reader.readU16(extFileNameLength))
+		return kDataReadErrorReadFailed;
+
+	if (reader.getProjectFormat() == Data::kProjectFormatMacintosh) {
+		haveMacPart = true;
+
+		if (!reader.readBytes(platform.mac.unknown5_1) || !reader.readU32(movieDataSize) || !reader.readBytes(platform.mac.unknown6) || !reader.readU32(moovAtomPos))
+			return kDataReadErrorReadFailed;
+	} else if (reader.getProjectFormat() == Data::kProjectFormatWindows) {
+		haveWinPart = true;
+
+		if (!reader.readBytes(platform.win.unknown3_1) || !reader.readU32(movieDataSize) || !reader.readBytes(platform.win.unknown4) || !reader.readU32(moovAtomPos) || !reader.readBytes(platform.win.unknown7))
+			return kDataReadErrorReadFailed;
+	} else
+		return kDataReadErrorReadFailed;
+
+	if (!reader.readTerminatedStr(extFileName, extFileNameLength))
+		return kDataReadErrorReadFailed;
+
+	movieDataPos = reader.tellGlobal();
+
+	if (!reader.skip(movieDataSize))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode AudioAsset::load(DataReader &reader) {
 	if (_revision != 2)
 		return kDataReadErrorUnsupportedRevision;
@@ -1680,10 +1718,23 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 	case DataObjectTypes::kColorTableAsset:
 		dataObject = new ColorTableAsset();
 		break;
+
+	case DataObjectTypes::kMovieAsset:
+		dataObject = new MovieAsset();
+		break;
+
 	case DataObjectTypes::kAudioAsset:
 		dataObject = new AudioAsset();
 		break;
 
+	case DataObjectTypes::kImageAsset:
+		//dataObject = new ImageAsset();
+		break;
+
+	case DataObjectTypes::kMToonAsset:
+		//dataObject = new MToonAsset();
+		break;
+
 	case DataObjectTypes::kAssetDataChunk:
 		dataObject = new AssetDataChunk();
 		break;
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index 97f48f2cd80..1e4db88e8ca 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -150,15 +150,15 @@ bool isAsset(DataObjectType type);
 
 namespace StructuralFlags {
 	enum StructuralFlags {
+		kHasModifiers = 0x1,
 		kHasChildren = 0x4,
 		kNoMoreSiblings = 0x8,
 	};
 } // End of namespace StructuralFlags
 
 class DataReader {
-
 public:
-	DataReader(Common::SeekableReadStreamEndian &stream, ProjectFormat projectFormat);
+	DataReader(int64 globalPosition, Common::SeekableReadStreamEndian &stream, ProjectFormat projectFormat);
 
 	bool readU8(uint8 &value);
 	bool readU16(uint16 &value);
@@ -188,6 +188,7 @@ public:
 	bool skip(size_t count);
 
 	int64 tell() const;
+	inline int64 tellGlobal() const { return _globalPosition + tell(); }
 
 	ProjectFormat getProjectFormat() const;
 	bool isBigEndian() const;
@@ -197,6 +198,7 @@ private:
 
 	Common::SeekableReadStreamEndian &_stream;
 	ProjectFormat _projectFormat;
+	int64 _globalPosition;
 };
 
 struct Rect {
@@ -670,6 +672,9 @@ protected:
 };
 
 struct BehaviorModifier : public DataObject {
+	enum BehaviorFlags {
+		kBehaviorFlagSwitchable = 1,
+	};
 
 	uint32 modifierFlags;
 	uint32 sizeIncludingTag;
@@ -681,7 +686,7 @@ struct BehaviorModifier : public DataObject {
 	Point editorLayoutPosition;
 	uint16 lengthOfName;
 	uint16 numChildren;
-	uint32 flags;
+	uint32 behaviorFlags;
 	Event enableWhen;
 	Event disableWhen;
 	uint8 unknown7[2];
@@ -1315,6 +1320,44 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct MovieAsset : public DataObject {
+	struct MacPart {
+		uint8 unknown5_1[66];
+		uint8 unknown6[12];
+	};
+
+	struct WinPart {
+		uint8 unknown3_1[32];
+		uint8 unknown4[12];
+		uint8 unknown7[12];
+	};
+
+	union PlatformPart {
+		MacPart mac;
+		WinPart win;
+	};
+
+	uint32 persistFlags;
+	uint32 assetAndDataCombinedSize;
+	uint8 unknown1[4];
+	uint32 assetID;
+	uint8 unknown1_1[4];
+	uint16 extFileNameLength;
+
+	uint32 movieDataPos;
+	uint32 moovAtomPos;
+	uint32 movieDataSize;
+
+	bool haveMacPart;
+	bool haveWinPart;
+	PlatformPart platform;
+
+	Common::String extFileName;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 struct AudioAsset : public DataObject {
 	struct MacPart {
 		uint8 unknown4[4];
diff --git a/engines/mtropolis/debug.cpp b/engines/mtropolis/debug.cpp
index bc8ecb3a07f..57defec3bd8 100644
--- a/engines/mtropolis/debug.cpp
+++ b/engines/mtropolis/debug.cpp
@@ -122,6 +122,8 @@ void Debugger::notify(DebugSeverity severity, const Common::String& str) {
 		Window &window = *_toastNotifications[i].window;
 		window.setPosition(window.getX(), window.getY() - toastNotificationHeight);
 	}
+
+	debug(1, "%s", str.c_str());
 }
 
 void Debugger::notifyFmt(DebugSeverity severity, const char *fmt, ...) {
diff --git a/engines/mtropolis/element_factory.cpp b/engines/mtropolis/element_factory.cpp
index 650ae80fddc..a86f25666df 100644
--- a/engines/mtropolis/element_factory.cpp
+++ b/engines/mtropolis/element_factory.cpp
@@ -62,6 +62,8 @@ IElementFactory *getElementFactoryForDataObjectType(const Data::DataObjectTypes:
 	switch (dataObjectType) {
 	case Data::DataObjectTypes::kGraphicElement:
 		return ElementFactory<GraphicElement, Data::GraphicElement>::getInstance();
+	case Data::DataObjectTypes::kMovieElement:
+		return ElementFactory<MovieElement, Data::MovieElement>::getInstance();
 
 	default:
 		return nullptr;
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 4a10c4acbd1..0e51d0eb66f 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -39,4 +39,71 @@ bool GraphicElement::load(ElementLoaderContext &context, const Data::GraphicElem
 	return true;
 }
 
+MovieElement::MovieElement()
+	: _directToScreen(false), _cacheBitmap(false), _paused(false)
+	, _loop(false), _alternate(false), _playEveryFrame(false), _assetID(0) {
+}
+
+MovieElement::~MovieElement() {
+}
+
+bool MovieElement::load(ElementLoaderContext &context, const Data::MovieElement &data) {
+	if (!loadCommon(data.name, data.guid, data.rect1, data.elementFlags, data.layer, data.streamLocator, data.sectionID))
+		return false;
+
+	_directToScreen = ((data.elementFlags & Data::ElementFlags::kNotDirectToScreen) == 0);
+	_cacheBitmap = ((data.elementFlags & Data::ElementFlags::kCacheBitmap) != 0);
+	_paused = ((data.elementFlags & Data::ElementFlags::kPaused) != 0);
+	_loop = ((data.animationFlags & Data::AnimationFlags::kLoop) != 0);
+	_alternate = ((data.animationFlags & Data::AnimationFlags::kAlternate) != 0);
+	_playEveryFrame = ((data.animationFlags & Data::AnimationFlags::kPlayEveryFrame) != 0);
+	_assetID = data.assetID;
+
+	return true;
+}
+
+bool MovieElement::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "direct") {
+		result.setBool(_directToScreen);
+		return true;
+	}
+
+	if (attrib == "paused") {
+		result.setBool(_paused);
+		return true;
+	}
+
+	return VisualElement::readAttribute(thread, result, attrib);
+}
+
+bool MovieElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
+	if (attrib == "direct") {
+		writeProxy = DynamicValueWriteFuncHelper<MovieElement, &MovieElement::scriptSetDirect>::create(this);
+		return true;
+	}
+
+	if (attrib == "paused") {
+		writeProxy = DynamicValueWriteFuncHelper<MovieElement, &MovieElement::scriptSetPaused>::create(this);
+		return true;
+	}
+	
+	return VisualElement::writeRefAttribute(thread, writeProxy, attrib);
+}
+
+bool MovieElement::scriptSetDirect(const DynamicValue &dest) {
+	if (dest.getType() == DynamicValueTypes::kBoolean) {
+		_directToScreen = dest.getBool();
+		return true;
+	}
+	return false;
+}
+
+bool MovieElement::scriptSetPaused(const DynamicValue& dest) {
+	if (dest.getType() == DynamicValueTypes::kBoolean) {
+		_paused = dest.getBool();
+		return true;
+	}
+	return false;
+}
+
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index fe5b1144ba3..be482254e4d 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -45,6 +45,33 @@ private:
 	bool _cacheBitmap;
 };
 
+class MovieElement : public VisualElement {
+public:
+	MovieElement();
+	~MovieElement();
+
+	bool load(ElementLoaderContext &context, const Data::MovieElement &data);
+
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
+	bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Movie Element"; }
+#endif
+
+private:
+	bool scriptSetDirect(const DynamicValue &dest);
+	bool scriptSetPaused(const DynamicValue &dest);
+
+	bool _directToScreen;
+	bool _cacheBitmap;
+	bool _paused;
+	bool _loop;
+	bool _alternate;
+	bool _playEveryFrame;
+	uint32 _assetID;
+};
+
 } // End of namespace MTropolis
 
 #endif
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 82368993708..76be424d9c4 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -29,6 +29,21 @@
 
 namespace MTropolis {
 
+static bool miniscriptEvaluateTruth(const DynamicValue& value) {
+	// NOTE: Comparing equal to "true" only passes for 1 exactly, but for conditions,
+	// any non-zero value is true.
+	switch (value.getType()) {
+	case DynamicValueTypes::kBoolean:
+		return value.getBool();
+	case DynamicValueTypes::kInteger:
+		return (value.getInt() != 0);
+	case DynamicValueTypes::kFloat:
+		return !(value.getFloat() == 0.0);
+	default:
+		return false;
+	}
+}
+
 MiniscriptInstruction::~MiniscriptInstruction() {
 }
 
@@ -89,7 +104,12 @@ bool MiniscriptInstructionLoader<MiniscriptInstructions::Send>::loadInstruction(
 	if (!evt.load(dataEvent))
 		return false;
 
-	new (dest) MiniscriptInstructions::Send(evt);
+	MessageFlags msgFlags;
+	msgFlags.immediate = ((instrFlags & 0x04) == 0);
+	msgFlags.cascade = ((instrFlags & 0x08) == 0);
+	msgFlags.relay = ((instrFlags & 0x10) == 0);
+
+	new (dest) MiniscriptInstructions::Send(evt, msgFlags);
 	return true;
 }
 
@@ -270,7 +290,7 @@ bool MiniscriptParser::parse(const Data::MiniscriptProgram &program, Common::Sha
 	}
 
 	Common::MemoryReadStreamEndian stream(&program.bytecode[0], program.bytecode.size(), program.isBigEndian);
-	Data::DataReader reader(stream, program.projectFormat);
+	Data::DataReader reader(0, stream, program.projectFormat);
 
 	struct InstructionData {
 		uint16 opcode;
@@ -346,7 +366,7 @@ bool MiniscriptParser::parse(const Data::MiniscriptProgram &program, Common::Sha
 			dataLoc = &rawInstruction.contents[0];
 
 		Common::MemoryReadStreamEndian instrContentsStream(static_cast<const byte *>(dataLoc), rawInstruction.contents.size(), reader.isBigEndian());
-		Data::DataReader instrContentsReader(instrContentsStream, reader.getProjectFormat());
+		Data::DataReader instrContentsReader(0, instrContentsStream, reader.getProjectFormat());
 
 		if (!rawInstruction.instrFactory->create(&programData[baseOffset + rawInstruction.pdPosition], rawInstruction.flags, instrContentsReader, miniscriptInstructions[i])) {
 			// Destroy any already-created instructions
@@ -448,7 +468,7 @@ MiniscriptInstructionOutcome Set::execute(MiniscriptThread *thread) const {
 	}
 
 	// Convert value
-	MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0);
+	MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0, false);
 	if (outcome != kMiniscriptInstructionOutcomeContinue)
 		return outcome;
 
@@ -484,12 +504,308 @@ MiniscriptInstructionOutcome Set::execute(MiniscriptThread *thread) const {
 	return kMiniscriptInstructionOutcomeContinue;
 }
 
-Send::Send(const Event &evt) : _evt(evt) {
+Send::Send(const Event &evt, const MessageFlags &messageFlags) : _evt(evt), _messageFlags(messageFlags) {
+}
+
+MiniscriptInstructionOutcome Send::execute(MiniscriptThread *thread) const {
+	if (thread->getStackSize() < 2) {
+		thread->error("Stack underflow");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0, false);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	outcome = thread->dereferenceRValue(1, true);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	DynamicValue &targetValue = thread->getStackValueFromTop(0).value;
+	DynamicValue &payloadValue = thread->getStackValueFromTop(1).value;
+
+	if (targetValue.getType() != DynamicValueTypes::kObject) {
+		thread->error("Invalid message destination");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	Common::SharedPtr<RuntimeObject> obj = targetValue.getObject().lock();
+
+	Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(_evt, payloadValue, thread->getModifier()->getSelfReference()));
+	Common::SharedPtr<MessageDispatch> dispatch;
+	if (obj->isModifier())
+		dispatch.reset(new MessageDispatch(msgProps, static_cast<Modifier *>(obj.get()), _messageFlags.cascade, _messageFlags.relay));
+	else if (obj->isStructural())
+		dispatch.reset(new MessageDispatch(msgProps, static_cast<Structural *>(obj.get()), _messageFlags.cascade, _messageFlags.relay));
+	else {
+		thread->error("Message destination is not a structural object or modifier");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	thread->popValues(2);
+
+	if (_messageFlags.immediate) {
+		thread->getRuntime()->sendMessageOnVThread(dispatch);
+		return kMiniscriptInstructionOutcomeYieldToVThread;
+	} else {
+		thread->getRuntime()->queueMessage(dispatch);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+}
+
+MiniscriptInstructionOutcome UnorderedCompareInstruction::execute(MiniscriptThread *thread) const {
+	if (thread->getStackSize() < 2) {
+		thread->error("Stack underflow");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0, false);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	outcome = thread->dereferenceRValue(1, false);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	DynamicValue &rs = thread->getStackValueFromTop(0).value;
+	DynamicValue &lsDest = thread->getStackValueFromTop(1).value;
+
+	bool isEqual = false;
+	bool isUndefined = false;
+	switch (lsDest.getType()) {
+	case DynamicValueTypes::kString:
+		if (rs.getType() == DynamicValueTypes::kString)
+			isEqual = caseInsensitiveEqual(lsDest.getString(), rs.getString());
+		break;
+	case DynamicValueTypes::kBoolean: {
+			switch (rs.getType()) {
+			case DynamicValueTypes::kInteger:
+				isEqual = (rs.getInt() == (lsDest.getBool() ? 1 : 0));
+				break;
+			case DynamicValueTypes::kFloat:
+				isEqual = (rs.getFloat() == (lsDest.getBool() ? 1.0 : 0.0));
+				break;
+			case DynamicValueTypes::kBoolean:
+				isEqual = (rs.getBool() == lsDest.getBool());
+				break;
+			}
+		} break;
+	case DynamicValueTypes::kFloat: {
+			if (isnan(lsDest.getFloat()))
+				isUndefined = true;
+			else {
+				switch (rs.getType()) {
+				case DynamicValueTypes::kInteger:
+					isEqual = (rs.getInt() == lsDest.getFloat());
+					break;
+				case DynamicValueTypes::kFloat:
+					if (isnan(rs.getFloat()))
+						isUndefined = true;
+					else
+						isEqual = (rs.getFloat() == lsDest.getFloat());
+					break;
+				case DynamicValueTypes::kBoolean:
+					isEqual = ((rs.getBool() ? 1.0 : 0.0) == lsDest.getFloat());
+					break;
+				}
+			}
+		} break;
+	case DynamicValueTypes::kInteger: {
+			switch (rs.getType()) {
+			case DynamicValueTypes::kInteger:
+				isEqual = (rs.getInt() == lsDest.getInt());
+				break;
+			case DynamicValueTypes::kFloat:
+				if (isnan(rs.getFloat()))
+					isUndefined = true;
+				else
+					isEqual = (rs.getFloat() == lsDest.getInt());
+				break;
+			case DynamicValueTypes::kBoolean:
+				isEqual = ((rs.getBool() ? 1 : 0) == lsDest.getInt());
+				break;
+			}
+		} break;
+	default:
+		isEqual = (lsDest == rs);
+		break;
+	}
+
+	lsDest.setBool(isUndefined == false && this->resolve(isEqual));
+	thread->popValues(1);
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+
+MiniscriptInstructionOutcome And::execute(MiniscriptThread *thread) const {
+	if (thread->getStackSize() < 2) {
+		thread->error("Stack underflow");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0, false);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	outcome = thread->dereferenceRValue(1, false);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	DynamicValue &rs = thread->getStackValueFromTop(0).value;
+	DynamicValue &lsDest = thread->getStackValueFromTop(1).value;
+	lsDest.setBool(miniscriptEvaluateTruth(lsDest) && miniscriptEvaluateTruth(rs));
+
+	thread->popValues(1);
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome Or::execute(MiniscriptThread *thread) const {
+	if (thread->getStackSize() < 2) {
+		thread->error("Stack underflow");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0, false);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	outcome = thread->dereferenceRValue(1, false);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	DynamicValue &rs = thread->getStackValueFromTop(0).value;
+	DynamicValue &lsDest = thread->getStackValueFromTop(1).value;
+	lsDest.setBool(miniscriptEvaluateTruth(lsDest) || miniscriptEvaluateTruth(rs));
+
+	thread->popValues(1);
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome Not::execute(MiniscriptThread *thread) const {
+	if (thread->getStackSize() < 1) {
+		thread->error("Stack underflow");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0, false);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	DynamicValue &value = thread->getStackValueFromTop(0).value;
+	value.setBool(!miniscriptEvaluateTruth(value));
+
+	thread->popValues(1);
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome OrderedCompareInstruction::execute(MiniscriptThread *thread) const {
+	if (thread->getStackSize() < 2) {
+		thread->error("Stack underflow");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0, false);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	outcome = thread->dereferenceRValue(1, false);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	DynamicValue &rs = thread->getStackValueFromTop(0).value;
+	DynamicValue &lsDest = thread->getStackValueFromTop(1).value;
+
+	double leftValue = 0.0;
+	double rightValue = 0.0;
+	if (lsDest.getType() == DynamicValueTypes::kFloat)
+		leftValue = lsDest.getFloat();
+	else if (lsDest.getType() == DynamicValueTypes::kInteger)
+		leftValue = lsDest.getInt();
+	else {
+		thread->error("Left-side value is invalid for comparison");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	if (rs.getType() == DynamicValueTypes::kFloat)
+		rightValue = rs.getFloat();
+	else if (rs.getType() == DynamicValueTypes::kInteger)
+		rightValue = rs.getInt();
+	else {
+		thread->error("Right-side value is invalid for comparison");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	lsDest.setBool(this->compareFloat(leftValue, rightValue));
+	thread->popValues(1);
+
+	return kMiniscriptInstructionOutcomeContinue;
 }
 
 BuiltinFunc::BuiltinFunc(BuiltinFunctionID bfid) : _funcID(bfid) {
 }
 
+MiniscriptInstructionOutcome PointCreate::execute(MiniscriptThread *thread) const {
+	if (thread->getStackSize() < 2) {
+		thread->error("Stack underflow");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0, false);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	outcome = thread->dereferenceRValue(1, false);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	DynamicValue &yVal = thread->getStackValueFromTop(0).value;
+	DynamicValue &xValDest = thread->getStackValueFromTop(1).value;
+
+	int16 coords[2];
+	DynamicValue *coordInputs[2] = {&xValDest, &yVal};
+
+	for (int i = 0; i < 2; i++) {
+		DynamicValue *v = coordInputs[i];
+		DynamicValue listContents;
+
+		if (v->getType() == DynamicValueTypes::kList) {
+			// Yes this is actually allowed
+			const Common::SharedPtr<DynamicList> &list = v->getList();
+			if (list->getSize() != 1 || !list->getAtIndex(0, listContents)) {
+				thread->error("Can't convert list to integer");
+				return kMiniscriptInstructionOutcomeFailed;
+			}
+
+			v = &listContents;
+		}
+
+		switch (v->getType()) {
+		case DynamicValueTypes::kFloat:
+			coords[i] = static_cast<int16>(floor(v->getFloat() + 0.5)) & 0xffff;
+			break;
+		case DynamicValueTypes::kInteger:
+			coords[i] = static_cast<int16>(v->getInt());
+			break;
+		case DynamicValueTypes::kBoolean:
+			coords[i] = (v->getBool()) ? 1 : 0;
+			break;
+		default:
+			thread->error("Invalid input for point creation");
+			return kMiniscriptInstructionOutcomeFailed;
+		}
+	}
+
+	xValDest.setPoint(Point16::create(coords[0], coords[1]));
+
+	thread->popValues(1);
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 GetChild::GetChild(uint32 attribute, bool isLValue, bool isIndexed)
 	: _attribute(attribute), _isLValue(isLValue), _isIndexed(isIndexed) {
 }
@@ -510,7 +826,7 @@ MiniscriptInstructionOutcome GetChild::execute(MiniscriptThread *thread) const {
 		}
 
 		// Convert index
-		MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0);
+		MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0, false);
 		if (outcome != kMiniscriptInstructionOutcomeContinue)
 			return outcome;
 
@@ -563,10 +879,13 @@ MiniscriptInstructionOutcome GetChild::execute(MiniscriptThread *thread) const {
 					return kMiniscriptInstructionOutcomeFailed;
 				}
 
-				if (!obj->readAttribute(thread, indexableValueSlot.value, attrib)) {
+				DynamicValueWriteProxy writeProxy;
+				if (!obj->writeRefAttribute(thread, writeProxy, attrib)) {
 					thread->error("Failed to read attribute '" + attrib + "'");
 					return kMiniscriptInstructionOutcomeFailed;
 				}
+
+				indexableValueSlot.value.setWriteProxy(nullptr, writeProxy);
 			} else if (indexableValueSlot.value.getType() == DynamicValueTypes::kWriteProxy) {
 				const DynamicValueWriteProxy &proxy = indexableValueSlot.value.getWriteProxy();
 				if (!proxy.ifc->refAttrib(indexableValueSlot.value, proxy.objectRef, proxy.ptrOrOffset, attrib)) {
@@ -613,10 +932,10 @@ MiniscriptInstructionOutcome GetChild::readRValueAttrib(MiniscriptThread *thread
 	case DynamicValueTypes::kObject: {
 			Common::SharedPtr<RuntimeObject> obj = valueSrcDest.getObject().lock();
 			if (!obj) {
-				thread->error("Unable to read attrib '" + attrib + "' from invalid object");
+				thread->error("Unable to read attribute '" + attrib + "' from invalid object");
 				return kMiniscriptInstructionOutcomeFailed;
 			} else if (!obj->readAttribute(thread, valueSrcDest, attrib)) {
-				thread->error("Unable to read attrib '" + attrib + "'");
+				thread->error("Unable to read attribute '" + attrib + "'");
 				return kMiniscriptInstructionOutcomeFailed;
 			}
 		} break;
@@ -682,7 +1001,7 @@ MiniscriptInstructionOutcome PushValue::execute(MiniscriptThread *thread) const
 		value.setFloat(_value.f);
 		break;
 	case DataType::kDataTypeBool:
-		value.setFloat(_value.b);
+		value.setBool(_value.b);
 		break;
 	case DataType::kDataTypeLocalRef:
 		value.setObject(thread->getRefs()->getRefByIndex(_value.ref));
@@ -802,6 +1121,30 @@ PushString::PushString(const Common::String &str) : _str(str) {
 Jump::Jump(uint32 instrOffset, bool isConditional) : _instrOffset(instrOffset), _isConditional(isConditional) {
 }
 
+MiniscriptInstructionOutcome Jump::execute(MiniscriptThread *thread) const {
+	if (_isConditional) {
+		if (thread->getStackSize() < 1) {
+			thread->error("Stack underflow");
+			return kMiniscriptInstructionOutcomeFailed;
+		}
+
+		MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0, false);
+		if (outcome != kMiniscriptInstructionOutcomeContinue)
+			return outcome;
+
+		bool isTrue = miniscriptEvaluateTruth(thread->getStackValueFromTop(0).value);
+
+		thread->popValues(1);
+
+		if (!isTrue)
+			thread->jumpOffset(this->_instrOffset);
+	} else {
+		thread->jumpOffset(this->_instrOffset);
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 } // End of namespace MiniscriptInstructions
 
 MiniscriptThread::MiniscriptThread(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msgProps, const Common::SharedPtr<MiniscriptProgram> &program, const Common::SharedPtr<MiniscriptReferences> &refs, Modifier *modifier)
@@ -865,7 +1208,7 @@ MiniscriptStackValue &MiniscriptThread::getStackValueFromTop(size_t offset) {
 	return _stack[_stack.size() - 1 - offset];
 }
 
-MiniscriptInstructionOutcome MiniscriptThread::dereferenceRValue(size_t offset) {
+MiniscriptInstructionOutcome MiniscriptThread::dereferenceRValue(size_t offset, bool cloneLists) {
 	assert(offset < _stack.size());
 	MiniscriptStackValue &stackValue = _stack[_stack.size() - 1 - offset];
 
@@ -890,6 +1233,10 @@ MiniscriptInstructionOutcome MiniscriptThread::dereferenceRValue(size_t offset)
 			}
 		}
 		break;
+	case DynamicValueTypes::kList:
+			if (cloneLists)
+				stackValue.value.setList(stackValue.value.getList()->clone());
+			break;
 	default:
 		break;
 	}
@@ -897,6 +1244,16 @@ MiniscriptInstructionOutcome MiniscriptThread::dereferenceRValue(size_t offset)
 	return kMiniscriptInstructionOutcomeContinue;
 }
 
+void MiniscriptThread::jumpOffset(size_t offset) {
+	if (offset == 0) {
+		this->error("Invalid jump offset");
+		_failed = true;
+		return;
+	}
+
+	_currentInstruction += offset - 1;
+}
+
 VThreadState MiniscriptThread::resumeTask(const ResumeTaskData &data) {
 	return data.thread->resume(data);
 }
diff --git a/engines/mtropolis/miniscript.h b/engines/mtropolis/miniscript.h
index 10f6e57121b..045277f4929 100644
--- a/engines/mtropolis/miniscript.h
+++ b/engines/mtropolis/miniscript.h
@@ -100,12 +100,15 @@ namespace MiniscriptInstructions {
 		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
 	};
 
-	class Send : public UnimplementedInstruction {
+	class Send : public MiniscriptInstruction {
 	public:
-		explicit Send(const Event &evt);
+		explicit Send(const Event &evt, const MessageFlags &messageFlags);
 
 	private:
+		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
+
 		Event _evt;
+		MessageFlags _messageFlags;
 	};
 
 	class Add : public UnimplementedInstruction {
@@ -123,34 +126,68 @@ namespace MiniscriptInstructions {
 	class Pow : public UnimplementedInstruction {
 	};
 
-	class And : public UnimplementedInstruction {
+	class And : public MiniscriptInstruction {
+	private:
+		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
 	};
 
-	class Or : public UnimplementedInstruction {
+	class Or : public MiniscriptInstruction {
+	private:
+		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
 	};
 
 	class Neg : public UnimplementedInstruction {
 	};
 
-	class Not : public UnimplementedInstruction {
+	class Not : public MiniscriptInstruction {
+	private:
+		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
+	};
+
+	class OrderedCompareInstruction : public MiniscriptInstruction{
+	protected:
+		virtual bool compareFloat(double a, double b) const = 0;
+
+	private:
+		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
+	};
+
+	class UnorderedCompareInstruction : public MiniscriptInstruction {
+	protected:
+		virtual bool resolve(bool isEqual) const = 0;
+
+	private:
+		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
 	};
 
-	class CmpEqual : public UnimplementedInstruction {
+	class CmpEqual : public UnorderedCompareInstruction {
+	private:
+		bool resolve(bool isEqual) const override { return isEqual; };
 	};
 
-	class CmpNotEqual : public UnimplementedInstruction {
+	class CmpNotEqual : public UnorderedCompareInstruction {
+	private:
+		bool resolve(bool isEqual) const override { return !isEqual; };
 	};
 
-	class CmpLessOrEqual : public UnimplementedInstruction {
+	class CmpLessOrEqual : public OrderedCompareInstruction {
+	private:
+		bool compareFloat(double a, double b) const override { return a <= b; }
 	};
 
-	class CmpLess : public UnimplementedInstruction {
+	class CmpLess : public OrderedCompareInstruction {
+	private:
+		bool compareFloat(double a, double b) const override { return a < b; }
 	};
 
-	class CmpGreaterOrEqual : public UnimplementedInstruction {
+	class CmpGreaterOrEqual : public OrderedCompareInstruction {
+	private:
+		bool compareFloat(double a, double b) const override { return a >= b; }
 	};
 
-	class CmpGreater : public UnimplementedInstruction {
+	class CmpGreater : public OrderedCompareInstruction {
+	private:
+		bool compareFloat(double a, double b) const override { return a > b; }
 	};
 
 	class BuiltinFunc : public UnimplementedInstruction {
@@ -193,7 +230,9 @@ namespace MiniscriptInstructions {
 	class StrConcat : public UnimplementedInstruction {
 	};
 
-	class PointCreate : public UnimplementedInstruction {
+	class PointCreate : public MiniscriptInstruction {
+	private:
+		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
 	};
 
 	class RangeCreate : public UnimplementedInstruction {
@@ -290,11 +329,13 @@ namespace MiniscriptInstructions {
 		Common::String _str;
 	};
 
-	class Jump : public UnimplementedInstruction {
+	class Jump : public MiniscriptInstruction {
 	public:
 		Jump(uint32 instrOffset, bool isConditional);
 
 	private:
+		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
+
 		uint32 _instrOffset;
 		bool _isConditional;
 	};
@@ -307,7 +348,6 @@ struct MiniscriptStackValue {
 
 class MiniscriptThread {
 public:
-
 	MiniscriptThread(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msgProps, const Common::SharedPtr<MiniscriptProgram> &program, const Common::SharedPtr<MiniscriptReferences> &refs, Modifier *modifier);
 
 	static void runOnVThread(VThread &vthread, const Common::SharedPtr<MiniscriptThread> &thread);
@@ -325,7 +365,9 @@ public:
 	size_t getStackSize() const;
 	MiniscriptStackValue &getStackValueFromTop(size_t offset);
 
-	MiniscriptInstructionOutcome dereferenceRValue(size_t offset);
+	MiniscriptInstructionOutcome dereferenceRValue(size_t offset, bool cloneLists);
+
+	void jumpOffset(size_t offset);
 
 private:
 	struct ResumeTaskData {
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 5e57f141d9f..6948e964a9e 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -31,7 +31,7 @@ bool BehaviorModifier::load(ModifierLoaderContext &context, const Data::Behavior
 	if (data.numChildren > 0) {
 		ChildLoaderContext loaderContext;
 		loaderContext.containerUnion.modifierContainer = this;
-		loaderContext.type = ChildLoaderContext::kTypeModifierList;
+		loaderContext.type = ChildLoaderContext::kTypeCountedModifierList;
 		loaderContext.remainingCount = data.numChildren;
 
 		context.childLoaderStack->contexts.push_back(loaderContext);
@@ -43,6 +43,8 @@ bool BehaviorModifier::load(ModifierLoaderContext &context, const Data::Behavior
 	_guid = data.guid;
 	_name = data.name;
 	_modifierFlags.load(data.modifierFlags);
+	_switchable = ((data.behaviorFlags & Data::BehaviorModifier::kBehaviorFlagSwitchable) != 0);
+	_isEnabled = !_switchable;
 
 	return true;
 }
@@ -56,10 +58,76 @@ void BehaviorModifier::appendModifier(const Common::SharedPtr<Modifier> &modifie
 	modifier->setParent(getSelfReference());
 }
 
+IModifierContainer *BehaviorModifier::getMessagePropagationContainer() {
+	if (_isEnabled)
+		return this;
+	else
+		return nullptr;
+}
+
 IModifierContainer* BehaviorModifier::getChildContainer() {
 	return this;
 }
 
+bool BehaviorModifier::respondsToEvent(const Event &evt) const {
+	if (_switchable) {
+		if (_enableWhen.respondsTo(evt))
+			return true;
+		if (_disableWhen.respondsTo(evt))
+			return true;
+	}
+	return false;
+}
+
+VThreadState BehaviorModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	if (_switchable) {
+		if (_disableWhen.respondsTo(msg->getEvent())) {
+			SwitchTaskData *taskData = runtime->getVThread().pushTask(this, &BehaviorModifier::switchTask);
+			taskData->targetState = false;
+			taskData->eventID = EventIDs::kParentDisabled;
+			taskData->runtime = runtime;
+		}
+		if (_enableWhen.respondsTo(msg->getEvent())) {
+			SwitchTaskData *taskData = runtime->getVThread().pushTask(this, &BehaviorModifier::switchTask);
+			taskData->targetState = true;
+			taskData->eventID = EventIDs::kParentEnabled;
+			taskData->runtime = runtime;
+		}
+	}
+
+	return kVThreadReturn;
+}
+
+VThreadState BehaviorModifier::switchTask(const SwitchTaskData &taskData) {
+	if (_isEnabled != taskData.targetState) {
+		_isEnabled = taskData.targetState;
+
+		if (_children.size() > 0) {
+			PropagateTaskData *propagateData = taskData.runtime->getVThread().pushTask(this, &BehaviorModifier::propagateTask);
+			propagateData->eventID = taskData.eventID;
+			propagateData->index = 0;
+			propagateData->runtime = taskData.runtime;
+		}
+	}
+
+	return kVThreadReturn;
+}
+
+VThreadState BehaviorModifier::propagateTask(const PropagateTaskData &taskData) {
+	if (taskData.index + 1 < _children.size()) {
+		PropagateTaskData *propagateData = taskData.runtime->getVThread().pushTask(this, &BehaviorModifier::propagateTask);
+		propagateData->eventID = taskData.eventID;
+		propagateData->index = taskData.index + 1;
+		propagateData->runtime = taskData.runtime;
+	}
+
+	Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(taskData.eventID, 0), DynamicValue(), this->getSelfReference()));
+	Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, _children[taskData.index].get(), true, true));
+	taskData.runtime->sendMessageOnVThread(dispatch);
+
+	return kVThreadReturn;
+}
+
 Common::SharedPtr<Modifier> BehaviorModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new BehaviorModifier(*this));
 }
@@ -70,7 +138,6 @@ void BehaviorModifier::visitInternalReferences(IStructuralReferenceVisitor* visi
 	}
 }
 
-
 // Miniscript modifier
 bool MiniscriptModifier::load(ModifierLoaderContext &context, const Data::MiniscriptModifier &data) {
 	if (!this->loadTypicalHeader(data.modHeader) || !_enableWhen.load(data.enableWhen))
@@ -156,6 +223,10 @@ uint32 AliasModifier::getAliasID() const {
 	return _aliasID;
 }
 
+bool AliasModifier::isAlias() const {
+	return true;
+}
+
 bool ChangeSceneModifier::load(ModifierLoaderContext& context, const Data::ChangeSceneModifier& data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -172,11 +243,115 @@ bool ChangeSceneModifier::load(ModifierLoaderContext& context, const Data::Chang
 	else
 		return false;
 
+	_targetSectionGUID = data.targetSectionGUID;
+	_targetSubsectionGUID = data.targetSubsectionGUID;
+	_targetSceneGUID = data.targetSceneGUID;
+
 	_addToReturnList = ((data.changeSceneFlags & Data::ChangeSceneModifier::kChangeSceneFlagAddToReturnList) != 0);
 	_addToDestList = ((data.changeSceneFlags & Data::ChangeSceneModifier::kChangeSceneFlagAddToDestList) != 0);
-	_addToWrapAround = ((data.changeSceneFlags & Data::ChangeSceneModifier::kChangeSceneFlagWrapAround) != 0);
+	_wrapAround = ((data.changeSceneFlags & Data::ChangeSceneModifier::kChangeSceneFlagWrapAround) != 0);
+
+	return true;
+}
+
+bool ChangeSceneModifier::respondsToEvent(const Event &evt) const {
+	if (_executeWhen.respondsTo(evt))
+		return true;
+
+	return false;
+}
+
+VThreadState ChangeSceneModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties>& msg) {
+	if (_executeWhen.respondsTo(msg->getEvent())) {
+		Common::SharedPtr<Structural> targetScene;
+		if (_sceneSelectionType == kSceneSelectionTypeSpecific) {
+			Structural *project = runtime->getProject();
+			Structural *section = nullptr;
+			for (Common::Array<Common::SharedPtr<Structural> >::const_iterator it = project->getChildren().begin(), itEnd = project->getChildren().end(); it != itEnd; ++it) {
+				Structural *candidate = it->get();
+				assert(candidate->isSection());
+				if (candidate->getStaticGUID() == _targetSectionGUID) {
+					section = candidate;
+					break;
+				}
+			}
+
+			if (section) {
+				Structural *subsection = nullptr;
+				for (Common::Array<Common::SharedPtr<Structural> >::const_iterator it = section->getChildren().begin(), itEnd = section->getChildren().end(); it != itEnd; ++it) {
+					Structural *candidate = it->get();
+					assert(candidate->isSubsection());
+					if (candidate->getStaticGUID() == _targetSubsectionGUID) {
+						subsection = candidate;
+						break;
+					}
+				}
+
+				if (subsection) {
+					for (Common::Array<Common::SharedPtr<Structural> >::const_iterator it = subsection->getChildren().begin(), itEnd = subsection->getChildren().end(); it != itEnd; ++it) {
+						const Common::SharedPtr<Structural> &candidate = *it;
+						assert(candidate->isElement() && static_cast<const Element *>(candidate.get())->isVisual());
+						if (candidate->getStaticGUID() == _targetSceneGUID) {
+							targetScene = candidate;
+							break;
+						}
+					}
+				} else {
+					warning("Change Scene Modifier failed, subsection could not be resolved");
+				}
+			} else {
+				warning("Change Scene Modifier failed, section could not be resolved");
+			}
+		} else {
+			Structural *mainScene = runtime->getActiveMainScene().get();
+			if (mainScene) {
+				Structural *subsection = mainScene->getParent();
+
+				const Common::Array<Common::SharedPtr<Structural> > &scenes = subsection->getChildren();
+				if (scenes.size() == 1)
+					error("Scene list is invalid");
+
+				size_t sceneIndex = 0;
+				for (size_t i = 1; i < scenes.size(); i++) {
+					if (scenes[i].get() == mainScene) {
+						sceneIndex = i;
+						break;
+					}
+				}
+
+				if (sceneIndex == 0) {
+					warning("Change Scene Modifier failed, couldn't identify current scene's cyclical position");
+				} else {
+					if (_sceneSelectionType == kSceneSelectionTypePrevious) {
+						if (sceneIndex == 1) {
+							if (!_wrapAround)
+								return kVThreadReturn;
+							targetScene = scenes.back();
+						} else {
+							targetScene = scenes[sceneIndex - 1];
+						}
+					} else if (_sceneSelectionType == kSceneSelectionTypeNext) {
+
+						if (sceneIndex == scenes.size() - 1) {
+							if (!_wrapAround)
+								return kVThreadReturn;
+							targetScene = scenes[1];
+						} else {
+							targetScene = scenes[sceneIndex + 1];
+						}
+					}
+				}
+			}
+		}
+
+		if (targetScene) {
+			runtime->addSceneStateTransition(HighLevelSceneTransition(targetScene, HighLevelSceneTransition::kTypeChangeToScene, _addToDestList, _addToReturnList));
+		} else {
+			warning("Change Scene Modifier failed, subsection could not be resolved");
+		}
+	}
 
-	return true;
+	return kVThreadReturn;
 }
 
 Common::SharedPtr<Modifier> ChangeSceneModifier::shallowClone() const {
@@ -512,7 +687,7 @@ bool CompoundVariableModifier::load(ModifierLoaderContext &context, const Data::
 	if (data.numChildren > 0) {
 		ChildLoaderContext loaderContext;
 		loaderContext.containerUnion.modifierContainer = this;
-		loaderContext.type = ChildLoaderContext::kTypeModifierList;
+		loaderContext.type = ChildLoaderContext::kTypeCountedModifierList;
 		loaderContext.remainingCount = data.numChildren;
 
 		context.childLoaderStack->contexts.push_back(loaderContext);
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index e9c329a6972..a95f00ed9e2 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -39,13 +39,33 @@ public:
 	const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const override;
 	void appendModifier(const Common::SharedPtr<Modifier> &modifier) override;
 
+	IModifierContainer *getMessagePropagationContainer() override;
 	IModifierContainer *getChildContainer() override;
 
+	bool respondsToEvent(const Event &evt) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Behavior Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
+	struct SwitchTaskData {
+		bool targetState;
+		EventIDs::EventID eventID;
+		Runtime *runtime;
+	};
+
+	struct PropagateTaskData {
+		size_t index;
+		EventIDs::EventID eventID;
+		Runtime *runtime;
+	};
+
+	VThreadState switchTask(const SwitchTaskData &taskData);
+	VThreadState propagateTask(const PropagateTaskData &taskData);
+
 	Common::SharedPtr<Modifier> shallowClone() const override;
 	void visitInternalReferences(IStructuralReferenceVisitor *visitor) override;
 
@@ -53,6 +73,8 @@ private:
 
 	Event _enableWhen;
 	Event _disableWhen;
+	bool _switchable;
+	bool _isEnabled;
 };
 
 class MiniscriptModifier : public Modifier {
@@ -64,7 +86,7 @@ public:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Miniscript Modifier"; }
-	SupportStatus debugGetSupportStatus() const override { return kSupportStatusPartial; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
@@ -113,8 +135,11 @@ public:
 	bool load(ModifierLoaderContext &context, const Data::AliasModifier &data);
 	uint32 getAliasID() const;
 
+	bool isAlias() const override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Alias Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
@@ -127,8 +152,12 @@ class ChangeSceneModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::ChangeSceneModifier &data);
 
+	bool respondsToEvent(const Event &evt) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Change Scene Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
@@ -147,7 +176,7 @@ private:
 	uint32 _targetSceneGUID;
 	bool _addToReturnList;
 	bool _addToDestList;
-	bool _addToWrapAround;
+	bool _wrapAround;
 };
 
 class SoundEffectModifier : public Modifier {
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index e87df0b3c73..4833ba6cc75 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -107,6 +107,12 @@ void ModifierChildMaterializer::visitWeakModifierRef(Common::SharedPtr<Modifier>
 	// Do nothing
 }
 
+char invariantToLower(char c) {
+	if (c >= 'A' && c <= 'Z')
+		return static_cast<char>(c - 'A' + 'a');
+	return c;
+}
+
 Common::String toCaseInsensitive(const Common::String &str) {
 	uint strLen = str.size();
 	if (strLen == 0)
@@ -116,17 +122,27 @@ Common::String toCaseInsensitive(const Common::String &str) {
 	Common::Array<char> lowered;
 	lowered.resize(strLen);
 
-	for (uint i = 0; i < strLen; i++) {
-		char c = str[i];
-		if (c >= 'A' && c <= 'Z') {
-			c = static_cast<char>(c - 'A' + 'a');
-		}
-		lowered[i] = c;
-	}
+	for (uint i = 0; i < strLen; i++)
+		lowered[i] = invariantToLower(str[i]);
 
 	return Common::String(&lowered[0], strLen);
 }
 
+
+bool caseInsensitiveEqual(const Common::String& str1, const Common::String& str2) {
+	size_t length1 = str1.size();
+	size_t length2 = str2.size();
+	if (length1 != length2)
+		return false;
+
+	for (size_t i = 0; i < length1; i++) {
+		if (invariantToLower(str1[i]) != invariantToLower(str2[i]))
+			return false;
+	}
+
+	return true;
+}
+
 bool Point16::load(const Data::Point &point) {
 	x = point.x;
 	y = point.y;
@@ -1287,6 +1303,10 @@ const Common::WeakPtr<RuntimeObject>& RuntimeObject::getSelfReference() const {
 	return _selfReference;
 }
 
+bool RuntimeObject::isStructural() const {
+	return false;
+}
+
 bool RuntimeObject::isProject() const {
 	return false;
 }
@@ -1355,6 +1375,10 @@ Structural::~Structural() {
 ProjectPresentationSettings::ProjectPresentationSettings() : width(640), height(480), bitsPerPixel(8) {
 }
 
+bool Structural::isStructural() const {
+	return true;
+}
+
 const Common::Array<Common::SharedPtr<Structural> > &Structural::getChildren() const {
 	return _children;
 }
@@ -1381,6 +1405,14 @@ void Structural::removeChild(Structural* child) {
 	}
 }
 
+void Structural::removeAllAssets() {
+	_assets.clear();
+}
+
+void Structural::holdAssets(const Common::Array<Common::SharedPtr<Asset> >& assets) {
+	_assets = assets;
+}
+
 Structural *Structural::getParent() const {
 	return _parent;
 }
@@ -1447,17 +1479,19 @@ void Structural::materializeDescendents(Runtime *runtime, ObjectLinkingScope *ou
 	for (Common::Array<Common::SharedPtr<Modifier> >::iterator it = _modifiers.begin(), itEnd = _modifiers.end(); it != itEnd; ++it) {
 		Modifier *modifier = it->get();
 		uint32 modifierGUID = modifier->getStaticGUID();
-		if (modifier->isAlias() && !modifier->isVariable()) {
+		if (modifier->isAlias()) {
 			Common::SharedPtr<Modifier> templateModifier = runtime->getProject()->resolveAlias(static_cast<AliasModifier *>(modifier)->getAliasID());
 			if (!templateModifier) {
 				error("Failed to resolve alias");
 			}
 
-			Common::SharedPtr<Modifier> clonedModifier = templateModifier->shallowClone();
-			clonedModifier->setName(modifier->getName());
+			if (!modifier->isVariable()) {
+				Common::SharedPtr<Modifier> clonedModifier = templateModifier->shallowClone();
+				clonedModifier->setName(modifier->getName());
 
-			(*it) = clonedModifier;
-			modifier = clonedModifier.get();
+				(*it) = clonedModifier;
+				modifier = clonedModifier.get();
+			}
 		}
 		modifierScope->addObject(modifierGUID, modifier->getName(), *it);
 	}
@@ -1622,10 +1656,8 @@ HighLevelSceneTransition::HighLevelSceneTransition(const Common::SharedPtr<Struc
 	: scene(scene), type(type), addToDestinationScene(addToDestinationScene), addToReturnList(addToReturnList) {
 }
 
-MessageDispatch::MessageDispatch(const Event &evt, Structural *root, bool cascade, bool relay)
-	: _cascade(cascade), _relay(relay), _terminated(false) {
-	_msg.reset(new MessageProperties(evt, DynamicValue(), Common::WeakPtr<RuntimeObject>()));
-
+MessageDispatch::MessageDispatch(const Common::SharedPtr<MessageProperties> &msgProps, Structural *root, bool cascade, bool relay)
+	: _cascade(cascade), _relay(relay), _terminated(false), _msg(msgProps) {
 	PropagationStack topEntry;
 	topEntry.index = 0;
 	topEntry.propagationStage = PropagationStack::kStageSendToStructuralSelf;
@@ -1634,10 +1666,8 @@ MessageDispatch::MessageDispatch(const Event &evt, Structural *root, bool cascad
 	_propagationStack.push_back(topEntry);
 }
 
-MessageDispatch::MessageDispatch(const Event &evt, Modifier *root, bool cascade, bool relay)
-	: _cascade(cascade), _relay(relay), _terminated(false) {
-	_msg.reset(new MessageProperties(evt, DynamicValue(), Common::WeakPtr<RuntimeObject>()));
-
+MessageDispatch::MessageDispatch(const Common::SharedPtr<MessageProperties> &msgProps, Modifier *root, bool cascade, bool relay)
+	: _cascade(cascade), _relay(relay), _terminated(false), _msg(msgProps) {
 	PropagationStack topEntry;
 	topEntry.index = 0;
 	topEntry.propagationStage = PropagationStack::kStageSendToModifier;
@@ -1710,7 +1740,7 @@ VThreadState MessageDispatch::continuePropagating(Runtime *runtime) {
 					PropagationStack childPropagation;
 					childPropagation.propagationStage = PropagationStack::kStageSendToStructuralSelf;
 					childPropagation.index = 0;
-					childPropagation.ptr.structural = structural;
+					childPropagation.ptr.structural = children[stackTop.index++].get();
 					_propagationStack.push_back(childPropagation);
 				}
 			} break;
@@ -1881,7 +1911,7 @@ bool Runtime::runProjectFrame() {
 
 		if (_sceneTransitionState == kSceneTransitionStateTransitioning && _playTime >= _sceneTransitionEndTime) {
 			_sceneTransitionState = kSceneTransitionStateNotTransitioning;
-			_messageQueue.push_back(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSceneTransitionEnded, 0), _activeMainScene.get(), false, true)));
+			queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneTransitionEnded, 0), _activeMainScene.get(), false, true);
 			continue;
 		}
 
@@ -1969,6 +1999,7 @@ void Runtime::executeTeardown(const Teardown &teardown) {
 	if (teardown.onlyRemoveChildren) {
 		structural->removeAllChildren();
 		structural->removeAllModifiers();
+		structural->removeAllAssets();
 	} else {
 		Structural *parent = structural->getParent();
 
@@ -2028,8 +2059,8 @@ void Runtime::executeCompleteTransitionToScene(const Common::SharedPtr<Structura
 		if (stackedScene == targetScene) {
 			sceneAlreadyInStack = true;
 		} else {
-			_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSceneEnded, 0), _activeMainScene.get(), false, true))));
-			_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kParentDisabled, 0), _activeMainScene.get(), true, true))));
+			queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneEnded, 0), _activeMainScene.get(), false, true);
+			queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kParentDisabled, 0), _activeMainScene.get(), true, true);
 			_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(_activeMainScene, LowLevelSceneStateTransitionAction::kUnload));
 
 			if (stackedScene == targetSharedScene)
@@ -2041,14 +2072,14 @@ void Runtime::executeCompleteTransitionToScene(const Common::SharedPtr<Structura
 
 	if (targetSharedScene != _activeSharedScene) {
 		if (_activeSharedScene) {
-			_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSceneEnded, 0), _activeSharedScene.get(), false, true))));
-			_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kParentDisabled, 0), _activeSharedScene.get(), true, true))));
+			queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneEnded, 0), _activeSharedScene.get(), false, true);
+			queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kParentDisabled, 0), _activeSharedScene.get(), true, true);
 			_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(_activeSharedScene, LowLevelSceneStateTransitionAction::kUnload));
 		}
 
 		_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(targetSharedScene, LowLevelSceneStateTransitionAction::kLoad));
-		_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kParentEnabled, 0), targetSharedScene.get(), true, true))));
-		_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSceneStarted, 0), targetSharedScene.get(), false, true))));
+		queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kParentEnabled, 0), targetSharedScene.get(), true, true);
+		queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneStarted, 0), targetSharedScene.get(), false, true);
 
 		SceneStackEntry sharedSceneEntry;
 		sharedSceneEntry.scene = targetScene;
@@ -2058,8 +2089,8 @@ void Runtime::executeCompleteTransitionToScene(const Common::SharedPtr<Structura
 
 	if (!sceneAlreadyInStack) {
 		_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(targetScene, LowLevelSceneStateTransitionAction::kLoad));
-		_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kParentEnabled, 0), targetScene.get(), true, true))));
-		_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSceneStarted, 0), targetScene.get(), false, true))));
+		queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kParentEnabled, 0), targetScene.get(), true, true);
+		queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneStarted, 0), targetScene.get(), false, true);
 
 		SceneStackEntry sceneEntry;
 		sceneEntry.scene = targetScene;
@@ -2103,11 +2134,11 @@ void Runtime::executeHighLevelSceneTransition(const HighLevelSceneTransition &tr
 
 				if (sceneReturn.isAddToDestinationSceneTransition) {
 					// In this case we unload the active main scene and reactivate the old main
-					_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSceneEnded, 0), _activeMainScene.get(), false, true))));
-					_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kParentDisabled, 0), _activeMainScene.get(), true, true))));
+					queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneEnded, 0), _activeMainScene.get(), false, true);
+					queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kParentDisabled, 0), _activeMainScene.get(), true, true);
 					_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(_activeMainScene, LowLevelSceneStateTransitionAction::kUnload));
 
-					_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSceneReactivated, 0), _activeMainScene.get(), false, true))));
+					queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneReactivated, 0), sceneReturn.scene.get(), false, true);
 
 					_activeMainScene = sceneReturn.scene;
 
@@ -2137,18 +2168,18 @@ void Runtime::executeHighLevelSceneTransition(const HighLevelSceneTransition &tr
 					if (_activeMainScene == targetSharedScene)
 						error("Transitioned into scene currently being used as a target scene, this is not supported");
 
-					_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSceneDeactivated, 0), _activeMainScene.get(), false, true))));
+					queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneDeactivated, 0), _activeMainScene.get(), false, true);
 
 					if (targetSharedScene != _activeSharedScene) {
 						if (_activeSharedScene) {
-							_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSceneEnded, 0), _activeSharedScene.get(), false, true))));
-							_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kParentDisabled, 0), _activeSharedScene.get(), true, true))));
+							queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneEnded, 0), _activeSharedScene.get(), false, true);
+							queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kParentDisabled, 0), _activeSharedScene.get(), true, true);
 							_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(_activeSharedScene, LowLevelSceneStateTransitionAction::kUnload));
 						}
 
 						_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(targetSharedScene, LowLevelSceneStateTransitionAction::kLoad));
-						_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kParentEnabled, 0), targetSharedScene.get(), true, true))));
-						_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSceneStarted, 0), targetSharedScene.get(), false, true))));
+						queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kParentEnabled, 0), targetSharedScene.get(), true, true);
+						queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneStarted, 0), targetSharedScene.get(), false, true);
 
 						SceneStackEntry sharedSceneEntry;
 						sharedSceneEntry.scene = targetScene;
@@ -2169,8 +2200,8 @@ void Runtime::executeHighLevelSceneTransition(const HighLevelSceneTransition &tr
 					// This is probably wrong if it's already in the stack, but transitioning to already-in-stack scenes is extremely buggy in mTropolis Player anyway
 					if (!sceneAlreadyInStack) {
 						_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(targetScene, LowLevelSceneStateTransitionAction::kLoad));
-						_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kParentEnabled, 0), targetScene.get(), true, true))));
-						_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSceneStarted, 0), targetScene.get(), false, true))));
+						queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kParentEnabled, 0), targetScene.get(), true, true);
+						queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneStarted, 0), targetScene.get(), false, true);
 
 						SceneStackEntry sceneEntry;
 						sceneEntry.scene = targetScene;
@@ -2197,15 +2228,21 @@ void Runtime::executeSharedScenePostSceneChangeActions() {
 
 	const Common::Array<Common::SharedPtr<Structural> > &subsectionScenes = subsection->getChildren();
 
-	_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSharedSceneSceneChanged, 0), _activeSharedScene.get(), false, true))));
+	queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSharedSceneSceneChanged, 0), _activeSharedScene.get(), false, true);
 	if (subsectionScenes.size() > 1) {
 		if (_activeMainScene == subsectionScenes[subsectionScenes.size() - 1])
-			_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSharedSceneNoNextScene, 0), _activeSharedScene.get(), false, true))));
+			queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSharedSceneNoNextScene, 0), _activeSharedScene.get(), false, true);
 		if (_activeMainScene == subsectionScenes[1])
-			_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(Common::SharedPtr<MessageDispatch>(new MessageDispatch(Event::create(EventIDs::kSharedSceneNoPrevScene, 0), _activeSharedScene.get(), false, true))));
+			queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSharedSceneNoPrevScene, 0), _activeSharedScene.get(), false, true);
 	}
 }
 
+void Runtime::queueEventAsLowLevelSceneStateTransitionAction(const Event &evt, Structural *root, bool cascade, bool relay) {
+	Common::SharedPtr<MessageProperties> props(new MessageProperties(evt, DynamicValue(), Common::WeakPtr<RuntimeObject>()));
+	Common::SharedPtr<MessageDispatch> msg(new MessageDispatch(props, root, cascade, relay));
+	_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(msg));
+}
+
 void Runtime::loadScene(const Common::SharedPtr<Structural>& scene) {
 	debug(1, "Loading scene '%s'", scene->getName().c_str());
 	Element *element = static_cast<Element *>(scene.get());
@@ -2505,7 +2542,7 @@ void Project::loadFromDescription(const ProjectDescription& desc) {
 		error("Unrecognized project segment header");
 	}
 
-	Data::DataReader reader(stream, _projectFormat);
+	Data::DataReader reader(2, stream, _projectFormat);
 
 	Common::SharedPtr<Data::DataObject> dataObject;
 	Data::loadDataObject(_plugInRegistry.getDataLoaderRegistry(), reader, dataObject);
@@ -2578,7 +2615,7 @@ void Project::loadSceneFromStream(const Common::SharedPtr<Structural>& scene, ui
 	openSegmentStream(segmentIndex);
 
 	Common::SeekableSubReadStreamEndian stream(_segments[segmentIndex].weakStream, streamDesc.pos, streamDesc.pos + streamDesc.size, _isBigEndian);
-	Data::DataReader reader(stream, _projectFormat);
+	Data::DataReader reader(streamDesc.pos, stream, _projectFormat);
 
 	const Data::PlugInModifierRegistry &plugInDataLoaderRegistry = _plugInRegistry.getDataLoaderRegistry();
 
@@ -2630,9 +2667,11 @@ void Project::loadSceneFromStream(const Common::SharedPtr<Structural>& scene, ui
 		numObjectsLoaded++;
 	}
 
-	if (loaderStack.contexts.size() != 1 || loaderStack.contexts[0].type != ChildLoaderContext::kTypeFilteredElements) {
+	if ((loaderStack.contexts.size() == 1 && loaderStack.contexts[0].type != ChildLoaderContext::kTypeFilteredElements) || loaderStack.contexts.size() > 1) {
 		error("Scene stream loader finished in an expected state, something didn't finish loading");
 	}
+
+	scene->holdAssets(assetDefLoader.assets);
 }
 
 Common::SharedPtr<Modifier> Project::resolveAlias(uint32 aliasID) const {
@@ -2692,7 +2731,7 @@ void Project::loadBootStream(size_t streamIndex) {
 	openSegmentStream(segmentIndex);
 
 	Common::SeekableSubReadStreamEndian stream(_segments[segmentIndex].weakStream, streamDesc.pos, streamDesc.pos + streamDesc.size, _isBigEndian);
-	Data::DataReader reader(stream, _projectFormat);
+	Data::DataReader reader(streamDesc.pos, stream, _projectFormat);
 
 	ChildLoaderStack loaderStack;
 	AssetDefLoaderContext assetDefLoader;
@@ -2761,6 +2800,8 @@ void Project::loadBootStream(size_t streamIndex) {
 	if (loaderStack.contexts.size() != 1 || loaderStack.contexts[0].type != ChildLoaderContext::kTypeProject) {
 		error("Boot stream loader finished in an expected state, something didn't finish loading");
 	}
+
+	holdAssets(assetDefLoader.assets);
 }
 
 void Project::loadPresentationSettings(const Data::PresentationSettings &presentationSettings) {
@@ -2818,7 +2859,7 @@ void Project::loadGlobalObjectInfo(ChildLoaderStack& loaderStack, const Data::Gl
 		ChildLoaderContext loaderContext;
 		loaderContext.containerUnion.modifierContainer = &_globalModifiers;
 		loaderContext.remainingCount = globalObjectInfo.numGlobalModifiers;
-		loaderContext.type = ChildLoaderContext::kTypeModifierList;
+		loaderContext.type = ChildLoaderContext::kTypeCountedModifierList;
 
 		loaderStack.contexts.push_back(loaderContext);
 	}
@@ -2834,6 +2875,7 @@ Common::SharedPtr<Modifier> Project::loadModifierObject(ModifierLoaderContext &l
 	// Special case for plug-ins
 	if (dataObject.getType() == Data::DataObjectTypes::kPlugInModifier) {
 		const Data::PlugInModifier &plugInData = static_cast<const Data::PlugInModifier &>(dataObject);
+
 		const IPlugInModifierFactory *factory = _plugInRegistry.findPlugInModifierFactory(plugInData.modifierName);
 		if (!factory)
 			error("Unknown or unsupported plug-in modifier type");
@@ -2847,6 +2889,7 @@ Common::SharedPtr<Modifier> Project::loadModifierObject(ModifierLoaderContext &l
 
 		modifier = factory->createModifier(loaderContext, dataObject);
 	}
+	assert(modifier->getModifierFlags().flagsWereLoaded);
 	if (!modifier)
 		error("Modifier object failed to load");
 
@@ -2926,7 +2969,7 @@ void Project::loadContextualObject(ChildLoaderStack &stack, const Data::DataObje
 	// The stack entry must always be popped before loading the object because the load process may descend into more children,
 	// such as when behaviors are nested.
 	switch (topContext.type) {
-	case ChildLoaderContext::kTypeModifierList: {
+	case ChildLoaderContext::kTypeCountedModifierList: {
 			IModifierContainer *container = topContext.containerUnion.modifierContainer;
 
 			if ((--topContext.remainingCount) == 0)
@@ -2936,6 +2979,20 @@ void Project::loadContextualObject(ChildLoaderStack &stack, const Data::DataObje
 
 			container->appendModifier(loadModifierObject(loaderContext, dataObject));
 		} break;
+	case ChildLoaderContext::kTypeFlagTerminatedModifierList: {
+		IModifierContainer *container = topContext.containerUnion.modifierContainer;
+
+			size_t modifierListContextOffset = stack.contexts.size() - 1;
+
+			ModifierLoaderContext loaderContext(&stack);
+
+			Common::SharedPtr<Modifier> modifier = loadModifierObject(loaderContext, dataObject);
+
+			if (modifier->getModifierFlags().isLastModifier)
+				stack.contexts.remove_at(modifierListContextOffset);
+
+			container->appendModifier(modifier);
+		} break;
 	case ChildLoaderContext::kTypeProject: {
 			Structural *project = topContext.containerUnion.structural;
 
@@ -2966,6 +3023,14 @@ void Project::loadContextualObject(ChildLoaderStack &stack, const Data::DataObje
 
 					stack.contexts.push_back(loaderContext);
 				}
+
+				if (sectionObject.structuralFlags & Data::StructuralFlags::kHasModifiers) {
+					ChildLoaderContext loaderContext;
+					loaderContext.containerUnion.modifierContainer = section.get();
+					loaderContext.type = ChildLoaderContext::kTypeFlagTerminatedModifierList;
+
+					stack.contexts.push_back(loaderContext);
+				}
 			} else if (Data::DataObjectTypes::isModifier(dataObjectType)) {
 				ModifierLoaderContext loaderContext(&stack);
 				project->appendModifier(loadModifierObject(loaderContext, dataObject));
@@ -2999,6 +3064,14 @@ void Project::loadContextualObject(ChildLoaderStack &stack, const Data::DataObje
 
 					stack.contexts.push_back(loaderContext);
 				}
+
+				if (subsectionObject.structuralFlags & Data::StructuralFlags::kHasModifiers) {
+					ChildLoaderContext loaderContext;
+					loaderContext.containerUnion.modifierContainer = subsection.get();
+					loaderContext.type = ChildLoaderContext::kTypeFlagTerminatedModifierList;
+
+					stack.contexts.push_back(loaderContext);
+				}
 			} else if (Data::DataObjectTypes::isModifier(dataObjectType)) {
 				ModifierLoaderContext loaderContext(&stack);
 				section->appendModifier(loadModifierObject(loaderContext, dataObject));
@@ -3036,6 +3109,14 @@ void Project::loadContextualObject(ChildLoaderStack &stack, const Data::DataObje
 
 					stack.contexts.push_back(loaderContext);
 				}
+
+				if (structuralDef.structuralFlags & Data::StructuralFlags::kHasModifiers) {
+					ChildLoaderContext loaderContext;
+					loaderContext.containerUnion.modifierContainer = element.get();
+					loaderContext.type = ChildLoaderContext::kTypeFlagTerminatedModifierList;
+
+					stack.contexts.push_back(loaderContext);
+				}
 			} else if (Data::DataObjectTypes::isModifier(dataObjectType)) {
 				ModifierLoaderContext loaderContext(&stack);
 				container->appendModifier(loadModifierObject(loaderContext, dataObject));
@@ -3116,13 +3197,44 @@ bool VisualElement::isVisual() const {
 	return true;
 }
 
+bool VisualElement::isVisible() const {
+	return _visible;
+}
+
+bool VisualElement::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "visible") {
+		result.setBool(_visible);
+		return true;
+	}
+
+	return Element::readAttribute(thread, result, attrib);
+}
+
+bool VisualElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
+	if (attrib == "visible") {
+		writeProxy = DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetVisibility>::create(this);
+		return true;
+	}
+
+	return Element::writeRefAttribute(thread, writeProxy, attrib);
+}
+
+bool VisualElement::scriptSetVisibility(const DynamicValue& result) {
+	if (result.getType() == DynamicValueTypes::kBoolean) {
+		_visible = result.getBool();
+		return true;
+	}
+
+	return false;
+}
+
 bool VisualElement::loadCommon(const Common::String &name, uint32 guid, const Data::Rect &rect, uint32 elementFlags, uint16 layer, uint32 streamLocator, uint16 sectionID) {
 	if (!_rect.load(rect))
 		return false;
 
 	_name = name;
 	_guid = guid;
-	_isHidden = ((elementFlags & Data::ElementFlags::kHidden) != 0);
+	_visible = ((elementFlags & Data::ElementFlags::kHidden) == 0);
 	_streamLocator = streamLocator;
 	_sectionID = sectionID;
 	_layer = layer;
@@ -3135,11 +3247,12 @@ bool NonVisualElement::isVisual() const {
 }
 
 
-ModifierFlags::ModifierFlags() : isLastModifier(false) {
+ModifierFlags::ModifierFlags() : isLastModifier(false), flagsWereLoaded(false) {
 }
 
 bool ModifierFlags::load(const uint32 dataModifierFlags) {
 	isLastModifier = ((dataModifierFlags & 0x2) != 0);
+	flagsWereLoaded = true;
 	return true;
 }
 
@@ -3221,12 +3334,17 @@ const Common::String& Modifier::getName() const {
 	return _name;
 }
 
+const ModifierFlags& Modifier::getModifierFlags() const {
+	return _modifierFlags;
+}
+
 void Modifier::visitInternalReferences(IStructuralReferenceVisitor *visitor) {
 }
 
 bool Modifier::loadPlugInHeader(const PlugInModifierLoaderContext &plugInContext) {
 	_guid = plugInContext.plugInModifierData.guid;
 	_name = plugInContext.plugInModifierData.name;
+	_modifierFlags.load(plugInContext.plugInModifierData.modifierFlags);
 
 	return true;
 }
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index cf8910c2f36..09b75f06d42 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -79,7 +79,9 @@ struct PlugInModifierLoaderContext;
 template<typename TElement, typename TElementData> class ElementFactory;
 
 
+char invariantToLower(char c);
 Common::String toCaseInsensitive(const Common::String &str);
+bool caseInsensitiveEqual(const Common::String &str1, const Common::String &str2);
 
 enum ColorDepthMode {
 	kColorDepthMode1Bit,
@@ -384,6 +386,33 @@ struct DynamicValueWriteProxy {
 	IDynamicValueWriteInterface *ifc;
 };
 
+template<class TClass, bool (TClass::*TWriteMethod)(const DynamicValue &dest)>
+struct DynamicValueWriteFuncHelper : public IDynamicValueWriteInterface {
+	bool write(const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const override {
+		return (static_cast<TClass *>(objectRef)->*TWriteMethod)(dest);
+	}
+	bool refAttrib(DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override {
+		return false;
+	}
+	bool refAttribIndexed(DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override {
+		return false;
+	}
+
+	static DynamicValueWriteProxy create(TClass *obj) {
+		DynamicValueWriteProxy proxy;
+		proxy.ptrOrOffset = 0;
+		proxy.objectRef = obj;
+		proxy.ifc = &_instance;
+		return proxy;
+	}
+
+private:
+	static DynamicValueWriteFuncHelper _instance;
+};
+
+template<class TClass, bool (TClass::*TWriteMethod)(const DynamicValue &dest)>
+DynamicValueWriteFuncHelper<TClass, TWriteMethod> DynamicValueWriteFuncHelper<TClass, TWriteMethod>::_instance;
+
 class DynamicListContainerBase {
 public:
 	virtual ~DynamicListContainerBase();
@@ -851,8 +880,8 @@ struct SceneTransitionEffect {
 
 class MessageDispatch {
 public:
-	MessageDispatch(const Event &evt, Structural *root, bool cascade, bool relay);
-	MessageDispatch(const Event &evt, Modifier *root, bool cascade, bool relay);
+	MessageDispatch(const Common::SharedPtr<MessageProperties> &msgProps, Structural *root, bool cascade, bool relay);
+	MessageDispatch(const Common::SharedPtr<MessageProperties> &msgProps, Modifier *root, bool cascade, bool relay);
 
 	bool isTerminated() const;
 	VThreadState continuePropagating(Runtime *runtime);
@@ -934,6 +963,10 @@ public:
 
 	VThread &getVThread() const;
 
+	// Sending a message on the VThread means "immediately"
+	void sendMessageOnVThread(const Common::SharedPtr<MessageDispatch> &dispatch);
+	void queueMessage(const Common::SharedPtr<MessageDispatch> &dispatch);
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	void debugSetEnabled(bool enabled);
 	void debugBreak();
@@ -980,11 +1013,9 @@ private:
 	void executeCompleteTransitionToScene(const Common::SharedPtr<Structural> &scene);
 	void executeSharedScenePostSceneChangeActions();
 
-	void loadScene(const Common::SharedPtr<Structural> &scene);
+	void queueEventAsLowLevelSceneStateTransitionAction(const Event &evt, Structural *root, bool cascade, bool relay);
 
-	// Sending a message on the VThread means "immediately"
-	void sendMessageOnVThread(const Common::SharedPtr<MessageDispatch> &dispatch);
-	void queueMessage(const Common::SharedPtr<MessageDispatch> &dispatch);
+	void loadScene(const Common::SharedPtr<Structural> &scene);
 
 	void ensureMainWindowExists();
 
@@ -1064,6 +1095,7 @@ public:
 	void setSelfReference(const Common::WeakPtr<RuntimeObject> &selfReference);
 	const Common::WeakPtr<RuntimeObject> &getSelfReference() const;
 
+	virtual bool isStructural() const;
 	virtual bool isProject() const;
 	virtual bool isSection() const;
 	virtual bool isSubsection() const;
@@ -1116,11 +1148,16 @@ public:
 	Structural();
 	virtual ~Structural();
 
+	bool isStructural() const override;
+
 	const Common::Array<Common::SharedPtr<Structural> > &getChildren() const;
 	void addChild(const Common::SharedPtr<Structural> &child);
 	void removeAllChildren();
 	void removeAllModifiers();
 	void removeChild(Structural *child);
+	void removeAllAssets();
+
+	void holdAssets(const Common::Array<Common::SharedPtr<Asset> > &assets);
 
 	Structural *getParent() const;
 	void setParent(Structural *parent);
@@ -1156,6 +1193,8 @@ protected:
 	Common::Array<Common::SharedPtr<Modifier> > _modifiers;
 	Common::String _name;
 
+	Common::Array<Common::SharedPtr<Asset> > _assets;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	Common::SharedPtr<DebugInspector> _debugInspector;
 	Debugger *_debugger;
@@ -1170,18 +1209,6 @@ struct ProjectPresentationSettings {
 	uint32 bitsPerPixel;
 };
 
-class AssetInfo {
-public:
-	AssetInfo();
-	virtual ~AssetInfo();
-
-	void setSegmentIndex(int segmentIndex);
-	int getSegmentIndex() const;
-
-private:
-	int _segmentIndex;
-};
-
 struct AssetDefLoaderContext {
 	Common::Array<Common::SharedPtr<Asset> > assets;
 };
@@ -1189,7 +1216,8 @@ struct AssetDefLoaderContext {
 struct ChildLoaderContext {
 	enum Type {
 		kTypeUnknown,
-		kTypeModifierList,
+		kTypeCountedModifierList,
+		kTypeFlagTerminatedModifierList,
 		kTypeProject,
 		kTypeSection,
 		kTypeFilteredElements,
@@ -1296,7 +1324,7 @@ private:
 		Common::String name;
 
 		// If the asset is live, this will be its asset info
-		Common::SharedPtr<AssetInfo> assetInfo;
+		Common::WeakPtr<Asset> asset;
 	};
 
 	void openSegmentStream(int segmentIndex);
@@ -1394,10 +1422,17 @@ class VisualElement : public Element {
 public:
 	bool isVisual() const override;
 
+	bool isVisible() const;
+
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
+	bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
+
+	bool scriptSetVisibility(const DynamicValue &result);
+
 protected:
 	bool loadCommon(const Common::String &name, uint32 guid, const Data::Rect &rect, uint32 elementFlags, uint16 layer, uint32 streamLocator, uint16 sectionID);
 
-	bool _isHidden;
+	bool _visible;
 	Rect16 _rect;
 	uint16 _layer;
 };
@@ -1414,6 +1449,7 @@ struct ModifierFlags {
 	bool load(const uint32 dataModifierFlags);
 
 	bool isLastModifier : 1;
+	bool flagsWereLoaded : 1;
 };
 
 class Modifier : public RuntimeObject, public IMessageConsumer, public IDebuggable {
@@ -1440,6 +1476,8 @@ public:
 	void setName(const Common::String &name);
 	const Common::String &getName() const;
 
+	const ModifierFlags &getModifierFlags() const;
+
 	// Shallow clones only need to copy the object.  Descendent copies are done using visitInternalReferences.
 	virtual Common::SharedPtr<Modifier> shallowClone() const = 0;
 


Commit: 18f0271bebf3788e768b0e7f0af99762c6c8bcc1
    https://github.com/scummvm/scummvm/commit/18f0271bebf3788e768b0e7f0af99762c6c8bcc1
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add messenger and timer messenger support

Changed paths:
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 76be424d9c4..e2509d33e9a 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -534,9 +534,9 @@ MiniscriptInstructionOutcome Send::execute(MiniscriptThread *thread) const {
 	Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(_evt, payloadValue, thread->getModifier()->getSelfReference()));
 	Common::SharedPtr<MessageDispatch> dispatch;
 	if (obj->isModifier())
-		dispatch.reset(new MessageDispatch(msgProps, static_cast<Modifier *>(obj.get()), _messageFlags.cascade, _messageFlags.relay));
+		dispatch.reset(new MessageDispatch(msgProps, static_cast<Modifier *>(obj.get()), _messageFlags.cascade, _messageFlags.relay, true));
 	else if (obj->isStructural())
-		dispatch.reset(new MessageDispatch(msgProps, static_cast<Structural *>(obj.get()), _messageFlags.cascade, _messageFlags.relay));
+		dispatch.reset(new MessageDispatch(msgProps, static_cast<Structural *>(obj.get()), _messageFlags.cascade, _messageFlags.relay, true));
 	else {
 		thread->error("Message destination is not a structural object or modifier");
 		return kMiniscriptInstructionOutcomeFailed;
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 6948e964a9e..4619fb38725 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -122,7 +122,7 @@ VThreadState BehaviorModifier::propagateTask(const PropagateTaskData &taskData)
 	}
 
 	Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(taskData.eventID, 0), DynamicValue(), this->getSelfReference()));
-	Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, _children[taskData.index].get(), true, true));
+	Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, _children[taskData.index].get(), true, true, false));
 	taskData.runtime->sendMessageOnVThread(dispatch);
 
 	return kVThreadReturn;
@@ -186,6 +186,26 @@ bool MessengerModifier::load(ModifierLoaderContext &context, const Data::Messeng
 	return true;
 }
 
+bool MessengerModifier::respondsToEvent(const Event &evt) const {
+	return _when.respondsTo(evt);
+}
+
+VThreadState MessengerModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	if (_when.respondsTo(msg->getEvent())) {
+		_sendSpec.sendFromMessenger(runtime, this);
+	}
+
+	return kVThreadReturn;
+}
+
+void MessengerModifier::linkInternalReferences(ObjectLinkingScope *outerScope) {
+	_sendSpec.linkInternalReferences(outerScope);
+}
+
+void MessengerModifier::visitInternalReferences(IStructuralReferenceVisitor *visitor) {
+	_sendSpec.visitInternalReferences(visitor);
+}
+
 Common::SharedPtr<Modifier> MessengerModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new MessengerModifier(*this));
 }
@@ -495,6 +515,11 @@ Common::SharedPtr<Modifier> IfMessengerModifier::shallowClone() const {
 	return clone;
 }
 
+TimerMessengerModifier::~TimerMessengerModifier() {
+	if (_scheduledEvent)
+		_scheduledEvent->cancel();
+}
+
 bool TimerMessengerModifier::load(ModifierLoaderContext &context, const Data::TimerMessengerModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -511,8 +536,46 @@ bool TimerMessengerModifier::load(ModifierLoaderContext &context, const Data::Ti
 	return true;
 }
 
+bool TimerMessengerModifier::respondsToEvent(const Event &evt) const {
+	return _executeWhen.respondsTo(evt) || _terminateWhen.respondsTo(evt);
+}
+
+VThreadState TimerMessengerModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	// If this terminates AND starts then just cancel out and terminate
+	if (_terminateWhen.respondsTo(msg->getEvent())) {
+		if (_scheduledEvent)
+			_scheduledEvent->cancel();
+	} else if (_executeWhen.respondsTo(msg->getEvent())) {
+		if (!_scheduledEvent) {
+			_scheduledEvent = runtime->getScheduler().scheduleMethod<TimerMessengerModifier, &TimerMessengerModifier::activate>(runtime->getPlayTime() + _milliseconds, this);
+		}
+	}
+
+	return kVThreadReturn;
+}
+
+void TimerMessengerModifier::linkInternalReferences(ObjectLinkingScope *outerScope) {
+	_sendSpec.linkInternalReferences(outerScope);
+}
+
+void TimerMessengerModifier::visitInternalReferences(IStructuralReferenceVisitor *visitor) {
+	_sendSpec.visitInternalReferences(visitor);
+}
+
 Common::SharedPtr<Modifier> TimerMessengerModifier::shallowClone() const {
-	return Common::SharedPtr<Modifier>(new TimerMessengerModifier(*this));
+	TimerMessengerModifier *clone = new TimerMessengerModifier(*this);
+	clone->_scheduledEvent.reset();
+
+	return Common::SharedPtr<Modifier>(clone);
+}
+
+void TimerMessengerModifier::activate(Runtime *runtime) {
+	if (_looping)
+		_scheduledEvent = runtime->getScheduler().scheduleMethod<TimerMessengerModifier, &TimerMessengerModifier::activate>(runtime->getPlayTime() + _milliseconds, this);
+	else
+		_scheduledEvent.reset();
+
+	_sendSpec.sendFromMessenger(runtime, this);
 }
 
 bool BoundaryDetectionMessengerModifier::load(ModifierLoaderContext &context, const Data::BoundaryDetectionMessengerModifier &data) {
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index a95f00ed9e2..3048eb3fcf3 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -103,8 +103,15 @@ class MessengerModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::MessengerModifier &data);
 
+	bool respondsToEvent(const Event &evt) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
+
+	void linkInternalReferences(ObjectLinkingScope *outerScope);
+	void visitInternalReferences(IStructuralReferenceVisitor *visitor);
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Messenger Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
@@ -316,20 +323,33 @@ private:
 
 class TimerMessengerModifier : public Modifier {
 public:
+	~TimerMessengerModifier();
+
 	bool load(ModifierLoaderContext &context, const Data::TimerMessengerModifier &data);
 
+	bool respondsToEvent(const Event &evt) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
+
+	void linkInternalReferences(ObjectLinkingScope *outerScope);
+	void visitInternalReferences(IStructuralReferenceVisitor *visitor);
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Timer Messenger Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
+	void activate(Runtime *runtime);
+
 	Event _executeWhen;
 	Event _terminateWhen;
 	MessengerSendSpec _sendSpec;
 	uint32 _milliseconds;
 	bool _looping;
+
+	Common::SharedPtr<ScheduledEvent> _scheduledEvent;
 };
 
 class BoundaryDetectionMessengerModifier : public Modifier {
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index a01db25bb86..2000363ebaf 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -245,7 +245,7 @@ Common::Error MTropolisEngine::run() {
 	uint expectedFrameRate = 60;
 	ColorDepthMode preferredColorDepthMode = kColorDepthMode8Bit;
 
-	_runtime.reset(new Runtime());
+	_runtime.reset(new Runtime(_system));
 
 	if (_gameDescription->gameID == GID_OBSIDIAN && _gameDescription->desc.platform == Common::kPlatformWindows) {
 		preferredWidth = 640;
@@ -415,15 +415,6 @@ Common::Error MTropolisEngine::run() {
 	}
 #endif
 
-	uint32 prevTimeStamp = _system->getMillis();
-
-	int32 earliness = 0;	// In thousandths of a frame
-	const int32 aheadLimit = 5000;
-	const int32 behindLimit = -5000;
-	const int32 jitterTolerance = 500;	// Half a frame
-	const int skippedFrameLimit = 10;
-	uint32 limitRollingAccumulatedThousandthsOfFrames = 0;
-	uint32 limitRollingAccumulatedMSec = 0;
 	bool paused = false;
 
 	int frameCounter = 0;
@@ -436,71 +427,8 @@ Common::Error MTropolisEngine::run() {
 			paused = _runtime->debugGetDebugger()->isPaused();
 #endif
 
-		// Scheduling is a bit tricky here because the project frame can suspend mid-execution.
-		bool skipDraw = false;
-		bool forceDraw = false;
-		bool frameWasRun = false;
-		if (!paused)
-			frameWasRun = _runtime->runProjectFrame();
-
-		uint32 frameTime = _system->getMillis();
-		const uint32 realElapsedThisFrame = frameTime - prevTimeStamp;
-		prevTimeStamp = frameTime;
-
-		if (frameWasRun) {
-			frameCounter++;
-			uint32 timeAdvance = realElapsedThisFrame;
-
-			if (enableFrameRateLimit) {
-				earliness += 1000;
-				uint32 durationInThousandsthsOfAFrame = realElapsedThisFrame * expectedFrameRate;
-				earliness -= static_cast<int32>(durationInThousandsthsOfAFrame);
-				if (earliness > aheadLimit) {
-					earliness = aheadLimit;
-				} else if (earliness < behindLimit) {
-					earliness = behindLimit;
-				}
-
-				if (earliness < -1000) {
-					if (numSkippedFrames < skippedFrameLimit)
-						skipDraw = true; // If we're lagging behind then don't draw
-				} else if (earliness > jitterTolerance) {
-					// If we're ahead by enough then pause.  Tolerate some slight addition to avoid
-					// degenerate cases if the timer wobbles near a frame edge.
-					uint millis = earliness / expectedFrameRate;
-					if (millis)
-						_system->delayMillis(millis);
-				}
-
-				limitRollingAccumulatedThousandthsOfFrames += 1000;
-
-				const uint32 prevRollingTime = limitRollingAccumulatedMSec;
-
-				// Resolve the actual msec to increment via rolling timers.
-				// Round up so that if time is divided by 60 then to derive tick count then it will always be in sync.
-				limitRollingAccumulatedMSec = (limitRollingAccumulatedThousandthsOfFrames + expectedFrameRate - 1) / expectedFrameRate;
-
-				timeAdvance = limitRollingAccumulatedMSec - prevRollingTime;
-
-				if (limitRollingAccumulatedThousandthsOfFrames >= expectedFrameRate * 1000) {
-					const int32 secondsToRemove = limitRollingAccumulatedThousandthsOfFrames / (expectedFrameRate * 1000);
-					limitRollingAccumulatedThousandthsOfFrames -= secondsToRemove * 1000 * expectedFrameRate;
-					limitRollingAccumulatedMSec -= secondsToRemove * 1000;
-				}
-			}
-
-			_runtime->advanceProjectTime(timeAdvance);
-		}
-
-		_runtime->runRealFrame(realElapsedThisFrame);
-
-		if (forceDraw || !skipDraw || !_runtime->mustDraw()) {
-			_runtime->drawFrame(_system);
-			numSkippedFrames = 0;
-		} else {
-			if (frameWasRun)
-				numSkippedFrames++;
-		}
+		_runtime->runFrame();
+		_runtime->drawFrame();
 	}
 
 	_runtime.release();
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 4833ba6cc75..0c2c2182326 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -47,8 +47,8 @@ public:
 
 	void visitChildStructuralRef(Common::SharedPtr<Structural> &structural) override;
 	void visitChildModifierRef(Common::SharedPtr<Modifier> &modifier) override;
-	void visitWeakStructuralRef(Common::SharedPtr<Structural> &structural) override;
-	void visitWeakModifierRef(Common::SharedPtr<Modifier> &modifier) override;
+	void visitWeakStructuralRef(Common::WeakPtr<Structural> &structural) override;
+	void visitWeakModifierRef(Common::WeakPtr<Modifier> &modifier) override;
 
 private:
 	ObjectLinkingScope *_scope;
@@ -65,11 +65,10 @@ void ModifierInnerScopeBuilder::visitChildModifierRef(Common::SharedPtr<Modifier
 	_scope->addObject(modifier->getStaticGUID(), modifier->getName(), modifier);
 }
 
-void ModifierInnerScopeBuilder::visitWeakStructuralRef(Common::SharedPtr<Structural> &structural) {
-	assert(false);
+void ModifierInnerScopeBuilder::visitWeakStructuralRef(Common::WeakPtr<Structural> &structural) {
 }
 
-void ModifierInnerScopeBuilder::visitWeakModifierRef(Common::SharedPtr<Modifier> &modifier) {
+void ModifierInnerScopeBuilder::visitWeakModifierRef(Common::WeakPtr<Modifier> &modifier) {
 	// Do nothing
 }
 
@@ -79,8 +78,8 @@ public:
 
 	void visitChildStructuralRef(Common::SharedPtr<Structural> &structural) override;
 	void visitChildModifierRef(Common::SharedPtr<Modifier> &modifier) override;
-	void visitWeakStructuralRef(Common::SharedPtr<Structural> &structural) override;
-	void visitWeakModifierRef(Common::SharedPtr<Modifier> &modifier) override;
+	void visitWeakStructuralRef(Common::WeakPtr<Structural> &structural) override;
+	void visitWeakModifierRef(Common::WeakPtr<Modifier> &modifier) override;
 
 private:
 	Runtime *_runtime;
@@ -99,11 +98,11 @@ void ModifierChildMaterializer::visitChildModifierRef(Common::SharedPtr<Modifier
 	modifier->materialize(_runtime, _outerScope);
 }
 
-void ModifierChildMaterializer::visitWeakStructuralRef(Common::SharedPtr<Structural> &structural) {
-	assert(false);
+void ModifierChildMaterializer::visitWeakStructuralRef(Common::WeakPtr<Structural> &structural) {
+	// Do nothing
 }
 
-void ModifierChildMaterializer::visitWeakModifierRef(Common::SharedPtr<Modifier> &modifier) {
+void ModifierChildMaterializer::visitWeakModifierRef(Common::WeakPtr<Modifier> &modifier) {
 	// Do nothing
 }
 
@@ -143,6 +142,41 @@ bool caseInsensitiveEqual(const Common::String& str1, const Common::String& str2
 	return true;
 }
 
+bool EventIDs::isCommand(EventID eventID) {
+	switch (eventID) {
+	case kElementShow:
+	case kElementHide:
+	case kElementSelect:
+	case kElementDeselect:
+	case kElementToggleSelect:
+	case kElementEnableEdit:
+	case kElementDisableEdit:
+	case kElementUpdatedCalculated:
+	case kElementScrollUp:
+	case kElementScrollDown:
+	case kElementScrollLeft:
+	case kElementScrollRight:
+	case kPreloadMedia:
+	case kFlushMedia:
+	case kPrerollMedia:
+	case kClone:
+	case kKill:
+	case kPlay:
+	case kStop:
+	case kPause:
+	case kUnpause:
+	case kTogglePause:
+	case kCloseProject:
+	case kFlushAllMedia:
+	case kAttribGet:
+	case kAttribSet:
+		return true;
+	default:
+		return false;
+	}
+}
+
+
 bool Point16::load(const Data::Point &point) {
 	x = point.x;
 	y = point.y;
@@ -1118,7 +1152,7 @@ void DynamicValue::initFromOther(const DynamicValue &other) {
 	_type = other._type;
 }
 
-MessengerSendSpec::MessengerSendSpec() : destination(0) {
+MessengerSendSpec::MessengerSendSpec() : destination(0), _linkType(kLinkTypeNotYetLinked) {
 }
 
 bool MessengerSendSpec::load(const Data::Event &dataEvent, uint32 dataMessageFlags, const Data::InternalTypeTaggedValue &dataLocator, const Common::String &dataWithSource, const Common::String &dataWithString, uint32 dataDestination) {
@@ -1152,6 +1186,160 @@ bool MessengerSendSpec::load(const Data::PlugInTypeTaggedValue &dataEvent, const
 	return true;
 }
 
+void MessengerSendSpec::linkInternalReferences(ObjectLinkingScope *outerScope) {
+	switch (destination) {
+	case kMessageDestNone:
+	case kMessageDestSharedScene:
+	case kMessageDestScene:
+	case kMessageDestSection:
+	case kMessageDestProject:
+	case kMessageDestActiveScene:
+	case kMessageDestElementsParent:
+	case kMessageDestChildren:
+	case kMessageDestModifiersParent:
+	case kMessageDestSubsection:
+	case kMessageDestElement:
+	case kMessageDestSourcesParent:
+	case kMessageDestBehavior:
+	case kMessageDestNextElement:
+	case kMessageDestPrevElement:
+	case kMessageDestBehaviorsParent:
+		_linkType = kLinkTypeCoded;
+		break;
+	default: {
+			Common::SharedPtr<RuntimeObject> resolution = outerScope->resolve(destination).lock();
+			if (resolution) {
+				if (resolution->isModifier()) {
+					resolvedModifierDest = resolution.staticCast<Modifier>();
+					_linkType = kLinkTypeModifier;
+				} else if (resolution->isStructural()) {
+					resolvedStructuralDest = resolution.staticCast<Structural>();
+					_linkType = kLinkTypeStructural;
+				} else {
+					_linkType = kLinkTypeUnresolved;
+				}
+			} else {
+				_linkType = kLinkTypeUnresolved;
+			}
+		} break;
+	}
+}
+
+void MessengerSendSpec::visitInternalReferences(IStructuralReferenceVisitor *visitor) {
+	visitor->visitWeakModifierRef(resolvedModifierDest);
+	visitor->visitWeakStructuralRef(resolvedStructuralDest);
+}
+
+void MessengerSendSpec::resolveDestination(Runtime *runtime, Modifier *sender, Common::WeakPtr<Structural> &outStructuralDest, Common::WeakPtr<Modifier> &outModifierDest) const {
+	outStructuralDest.reset();
+	outModifierDest.reset();
+
+	if (_linkType == kLinkTypeModifier) {
+		outModifierDest = resolvedModifierDest;
+	} else if (_linkType == kLinkTypeStructural) {
+		outStructuralDest = resolvedStructuralDest;
+	} else if (_linkType == kLinkTypeCoded) {
+		switch (destination) {
+		case kMessageDestNone:
+			break;
+		case kMessageDestSharedScene:
+			outStructuralDest = runtime->getActiveSharedScene();
+			break;
+		case kMessageDestScene:
+			resolveHierarchyStructuralDestination(runtime, sender, outStructuralDest, outModifierDest, isSceneFilter);
+			break;
+		case kMessageDestSection:
+			resolveHierarchyStructuralDestination(runtime, sender, outStructuralDest, outModifierDest, isSectionFilter);
+			break;
+		case kMessageDestProject:
+			outStructuralDest = runtime->getProject()->getSelfReference().staticCast<Project>();
+			break;
+		case kMessageDestActiveScene:
+			outStructuralDest = runtime->getActiveMainScene();
+			break;
+		case kMessageDestElement:
+			resolveHierarchyStructuralDestination(runtime, sender, outStructuralDest, outModifierDest, isElementFilter);
+			break;
+		case kMessageDestChildren:
+		case kMessageDestElementsParent:
+		case kMessageDestModifiersParent:
+		case kMessageDestSubsection:
+		case kMessageDestSourcesParent:
+		case kMessageDestBehavior:
+		case kMessageDestNextElement:
+		case kMessageDestPrevElement:
+		case kMessageDestBehaviorsParent:
+			warning("Not-yet-implemented message destination type");
+			break;
+		default:
+			break;
+		}
+	}
+}
+
+void MessengerSendSpec::sendFromMessenger(Runtime *runtime, Modifier *sender) const {
+
+	Common::SharedPtr<MessageProperties> props(new MessageProperties(this->send, this->with, sender->getSelfReference()));
+
+	Common::WeakPtr<Modifier> modifierDestRef;
+	Common::WeakPtr<Structural> structuralDestRef;
+
+	resolveDestination(runtime, sender, structuralDestRef, modifierDestRef);
+
+	Common::SharedPtr<Modifier> modifierDest = modifierDestRef.lock();
+	Common::SharedPtr<Structural> structuralDest = structuralDestRef.lock();
+
+	Common::SharedPtr<MessageDispatch> dispatch;
+	if (structuralDest)
+		dispatch.reset(new MessageDispatch(props, structuralDest.get(), messageFlags.cascade, messageFlags.relay, true));
+	else if (modifierDest)
+		dispatch.reset(new MessageDispatch(props, modifierDest.get(), messageFlags.cascade, messageFlags.relay, true));
+
+	if (dispatch) {
+		if (messageFlags.immediate)
+			runtime->sendMessageOnVThread(dispatch);
+		else
+			runtime->queueMessage(dispatch);
+	}
+}
+
+void MessengerSendSpec::resolveHierarchyStructuralDestination(Runtime *runtime, Modifier *sender, Common::WeakPtr<Structural> &outStructuralDest, Common::WeakPtr<Modifier> &outModifierDest, bool (*compareFunc)(Structural *structural)) const {
+	RuntimeObject *obj = sender->getParent().lock().get();
+	while (obj) {
+		if (obj->isStructural()) {
+			Structural *structural = static_cast<Structural *>(obj);
+			if (compareFunc(structural)) {
+				outStructuralDest = structural->getSelfReference().staticCast<Structural>();
+				return;
+			}
+
+			obj = structural->getParent();
+		} else if (obj->isModifier()) {
+			Modifier *modifier = static_cast<Modifier *>(obj);
+			obj = modifier->getParent().lock().get();
+		} else {
+			break;
+		}
+	}
+}
+
+bool MessengerSendSpec::isSceneFilter(Structural *structural) {
+	Structural *parent = structural->getParent();
+	return parent != nullptr && parent->isSubsection();
+}
+
+bool MessengerSendSpec::isSectionFilter(Structural *structural) {
+	return structural->isSection();
+}
+
+bool MessengerSendSpec::isSubsectionFilter(Structural *structural) {
+	return structural->isSubsection();
+}
+
+bool MessengerSendSpec::isElementFilter(Structural *structural) {
+	return structural->isElement();
+}
+
 Event Event::create() {
 	Event evt;
 	evt.eventInfo = 0;
@@ -1515,6 +1703,11 @@ void Structural::materializeDescendents(Runtime *runtime, ObjectLinkingScope *ou
 	}
 }
 
+VThreadState Structural::consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	warning("Command type %i was ignored", msg->getEvent().eventType);
+	return kVThreadReturn;
+}
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 SupportStatus Structural::debugGetSupportStatus() const {
 	return kSupportStatusNone;
@@ -1656,24 +1849,42 @@ HighLevelSceneTransition::HighLevelSceneTransition(const Common::SharedPtr<Struc
 	: scene(scene), type(type), addToDestinationScene(addToDestinationScene), addToReturnList(addToReturnList) {
 }
 
-MessageDispatch::MessageDispatch(const Common::SharedPtr<MessageProperties> &msgProps, Structural *root, bool cascade, bool relay)
-	: _cascade(cascade), _relay(relay), _terminated(false), _msg(msgProps) {
-	PropagationStack topEntry;
-	topEntry.index = 0;
-	topEntry.propagationStage = PropagationStack::kStageSendToStructuralSelf;
-	topEntry.ptr.structural = root;
+MessageDispatch::MessageDispatch(const Common::SharedPtr<MessageProperties> &msgProps, Structural *root, bool cascade, bool relay, bool couldBeCommand)
+	: _cascade(cascade), _relay(relay), _terminated(false), _msg(msgProps), _isCommand(false) {
+	if (couldBeCommand && EventIDs::isCommand(msgProps->getEvent().eventType)) {
+		_isCommand = true;
+
+		PropagationStack topEntry;
+		topEntry.index = 0;
+		topEntry.propagationStage = PropagationStack::kStageSendCommand;
+		topEntry.ptr.structural = root;
 
-	_propagationStack.push_back(topEntry);
+		_propagationStack.push_back(topEntry);
+	} else {
+		PropagationStack topEntry;
+		topEntry.index = 0;
+		topEntry.propagationStage = PropagationStack::kStageSendToStructuralSelf;
+		topEntry.ptr.structural = root;
+
+		_propagationStack.push_back(topEntry);
+	}
 }
 
-MessageDispatch::MessageDispatch(const Common::SharedPtr<MessageProperties> &msgProps, Modifier *root, bool cascade, bool relay)
-	: _cascade(cascade), _relay(relay), _terminated(false), _msg(msgProps) {
-	PropagationStack topEntry;
-	topEntry.index = 0;
-	topEntry.propagationStage = PropagationStack::kStageSendToModifier;
-	topEntry.ptr.modifier = root;
+MessageDispatch::MessageDispatch(const Common::SharedPtr<MessageProperties> &msgProps, Modifier *root, bool cascade, bool relay, bool couldBeCommand)
+	: _cascade(cascade), _relay(relay), _terminated(false), _msg(msgProps), _isCommand(false) {
+	if (couldBeCommand && EventIDs::isCommand(msgProps->getEvent().eventType)) {
+		_isCommand = true;
+		// Can't actually send this so don't even bother.  Modifiers are not allowed to respond to commands.
+	} else {
+		PropagationStack topEntry;
+		topEntry.index = 0;
+		topEntry.propagationStage = PropagationStack::kStageSendToModifier;
+		topEntry.ptr.modifier = root;
+
+		_isCommand = (couldBeCommand && EventIDs::isCommand(msgProps->getEvent().eventType));
 
-	_propagationStack.push_back(topEntry);
+		_propagationStack.push_back(topEntry);
+	}
 }
 
 bool MessageDispatch::isTerminated() const {
@@ -1759,6 +1970,15 @@ VThreadState MessageDispatch::continuePropagating(Runtime *runtime) {
 				if (responds)
 					runtime->postConsumeMessageTask(structural, _msg);
 
+				return kVThreadReturn;
+			} break;
+		case PropagationStack::kStageSendCommand: {
+				Structural *structural = stackTop.ptr.structural;
+				_propagationStack.pop_back();
+				_terminated = true;
+
+				runtime->postConsumeCommandTask(structural, _msg);
+
 				return kVThreadReturn;
 			} break;
 		case PropagationStack::kStageSendToStructuralModifiers: {
@@ -1791,12 +2011,75 @@ VThreadState MessageDispatch::continuePropagating(Runtime *runtime) {
 	return kVThreadReturn;
 }
 
-Runtime::SceneStackEntry::SceneStackEntry() {
+void ScheduledEvent::cancel() {
+	if (_scheduler)
+		_scheduler->removeEvent(this);
+
+	_scheduler = nullptr;
+}
+
+uint64 ScheduledEvent::getScheduledTime() const {
+	return _scheduledTime;
+}
+
+void ScheduledEvent::activate(Runtime *runtime) const {
+	_activateFunc(this->_obj, runtime);
+}
+
+
+ScheduledEvent::ScheduledEvent(void *obj, void (*activateFunc)(void *, Runtime *runtime), uint64 scheduledTime, Scheduler *scheduler)
+	: _obj(obj), _activateFunc(activateFunc), _scheduledTime(scheduledTime), _scheduler(scheduler) {
+}
+
+Scheduler::Scheduler() {
+}
+
+Scheduler::~Scheduler() {
+	for (Common::Array<Common::SharedPtr<ScheduledEvent>>::iterator it = _events.begin(), itEnd = _events.end(); it != itEnd; ++it) {
+		it->get()->_scheduler = nullptr;
+	}
+
+	_events.clear();
+}
+
+Common::SharedPtr<ScheduledEvent> Scheduler::getFirstEvent() const {
+	if (_events.size() > 0)
+		return _events[0];
+	return nullptr;
+}
+
+void Scheduler::descheduleFirstEvent() {
+	_events.remove_at(0);
+}
+
+void Scheduler::insertEvent(const Common::SharedPtr<ScheduledEvent> &evt) {
+	uint32 t = evt->getScheduledTime();
+	size_t insertionIndex = 0;
+	while (insertionIndex < _events.size()) {
+		if (_events[insertionIndex]->getScheduledTime() > t)
+			break;
+		insertionIndex++;
+	}
+
+	_events.insert_at(insertionIndex, evt);
 }
 
+void Scheduler::removeEvent(const ScheduledEvent *evt) {
+	for (size_t i = 0; i < _events.size(); i++) {
+		if (_events[i].get() == evt) {
+			_events[i].get()->_scheduler = nullptr;
+			_events.remove_at(i);
+			break;
+		}
+	}
+}
 
-Runtime::Runtime() : _nextRuntimeGUID(1), _realDisplayMode(kColorDepthModeInvalid), _fakeDisplayMode(kColorDepthModeInvalid),
-					 _displayWidth(1024), _displayHeight(768), _realTime(0), _playTime(0), _sceneTransitionState(kSceneTransitionStateNotTransitioning) {
+Runtime::SceneStackEntry::SceneStackEntry() {
+}
+
+Runtime::Runtime(OSystem *system) : _nextRuntimeGUID(1), _realDisplayMode(kColorDepthModeInvalid), _fakeDisplayMode(kColorDepthModeInvalid),
+									_displayWidth(1024), _displayHeight(768), _realTimeBase(0), _playTimeBase(0),
+									_sceneTransitionState(kSceneTransitionStateNotTransitioning), _system(system) {
 	_vthread.reset(new VThread());
 
 	for (int i = 0; i < kColorDepthModeCount; i++) {
@@ -1804,18 +2087,25 @@ Runtime::Runtime() : _nextRuntimeGUID(1), _realDisplayMode(kColorDepthModeInvali
 		_realDisplayMode = kColorDepthModeInvalid;
 		_fakeDisplayMode = kColorDepthModeInvalid;
 	}
+
+	_realTimeBase = system->getMillis();
+	_playTimeBase = system->getMillis();
 }
 
-void Runtime::runRealFrame(uint32 msec) {
-	_realTime += msec;
+bool Runtime::runFrame() {
+	uint32 timeMillis = _system->getMillis();
+
+	uint32 realMSec = timeMillis - _realTimeBase - _realTime;
+	uint32 playMSec = timeMillis - _playTimeBase - _playTime;
+
+	_realTime = timeMillis - _realTimeBase;
+	_playTime = timeMillis - _playTimeBase;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	if (_debugger)
-		_debugger->runFrame(msec);
+		_debugger->runFrame(realMSec);
 #endif
-}
 
-bool Runtime::runProjectFrame() {
 	for (;;) {
 		VThreadState state = _vthread->step();
 		if (state != kVThreadReturn) {
@@ -1911,10 +2201,20 @@ bool Runtime::runProjectFrame() {
 
 		if (_sceneTransitionState == kSceneTransitionStateTransitioning && _playTime >= _sceneTransitionEndTime) {
 			_sceneTransitionState = kSceneTransitionStateNotTransitioning;
-			queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneTransitionEnded, 0), _activeMainScene.get(), false, true);
+			queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneTransitionEnded, 0), _activeMainScene.get(), true, true);
 			continue;
 		}
 
+		{
+			Common::SharedPtr<ScheduledEvent> firstScheduledEvent = _scheduler.getFirstEvent();
+			if (firstScheduledEvent && firstScheduledEvent->getScheduledTime() <= _playTime) {
+				_scheduler.descheduleFirstEvent();
+
+				firstScheduledEvent->activate(this);
+				continue;
+			}
+		}
+
 		// Ran out of actions
 		break;
 	}
@@ -1923,15 +2223,11 @@ bool Runtime::runProjectFrame() {
 	return true;
 }
 
-void Runtime::advanceProjectTime(uint32 msec) {
-	_playTime += msec;
-}
-
-void Runtime::drawFrame(OSystem* system) {
-	int width = system->getWidth();
-	int height = system->getHeight();
+void Runtime::drawFrame() {
+	int width = _system->getWidth();
+	int height = _system->getHeight();
 
-	system->fillScreen(Render::resolveRGB(0, 0, 0, getRenderPixelFormat()));
+	_system->fillScreen(Render::resolveRGB(0, 0, 0, getRenderPixelFormat()));
 
 	for (Common::Array<Common::SharedPtr<Window> >::const_iterator it = _windows.begin(), itEnd = _windows.end(); it != itEnd; ++it) {
 		const Window &window = *it->get();
@@ -1975,10 +2271,10 @@ void Runtime::drawFrame(OSystem* system) {
 		if (destLeft >= destRight || destTop >= destBottom || destLeft >= width || destTop >= height || destRight <= 0 || destBottom <= 0)
 			continue;
 
-		system->copyRectToScreen(surface.getBasePtr(srcLeft, srcTop), surface.pitch, destLeft, destTop, destRight - destLeft, destBottom - destTop);
+		_system->copyRectToScreen(surface.getBasePtr(srcLeft, srcTop), surface.pitch, destLeft, destTop, destRight - destLeft, destBottom - destTop);
 	}
 
-	system->updateScreen();
+	_system->updateScreen();
 }
 
 Common::SharedPtr<Structural> Runtime::findDefaultSharedSceneForScene(Structural *scene) {
@@ -2239,7 +2535,7 @@ void Runtime::executeSharedScenePostSceneChangeActions() {
 
 void Runtime::queueEventAsLowLevelSceneStateTransitionAction(const Event &evt, Structural *root, bool cascade, bool relay) {
 	Common::SharedPtr<MessageProperties> props(new MessageProperties(evt, DynamicValue(), Common::WeakPtr<RuntimeObject>()));
-	Common::SharedPtr<MessageDispatch> msg(new MessageDispatch(props, root, cascade, relay));
+	Common::SharedPtr<MessageDispatch> msg(new MessageDispatch(props, root, cascade, relay, false));
 	_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(msg));
 }
 
@@ -2261,6 +2557,8 @@ void Runtime::loadScene(const Common::SharedPtr<Structural>& scene) {
 		_debugger->refreshSceneStatus();
 	}
 #endif
+
+	refreshPlayTime();
 }
 
 void Runtime::sendMessageOnVThread(const Common::SharedPtr<MessageDispatch> &dispatch) {
@@ -2289,9 +2587,18 @@ VThreadState Runtime::consumeMessageTask(const ConsumeMessageTaskData &data) {
 	return consumer->consumeMessage(this, data.message);
 }
 
+VThreadState Runtime::consumeCommandTask(const ConsumeCommandTaskData &data) {
+	Structural *structural = data.structural;
+	return structural->consumeCommand(this, data.message);
+}
+
 void Runtime::queueMessage(const Common::SharedPtr<MessageDispatch>& dispatch) {
 	_messageQueue.push_back(dispatch);
 }
+Scheduler &Runtime::getScheduler() {
+	return _scheduler;
+}
+
 
 void Runtime::ensureMainWindowExists() {
 	// Maybe there's a better spot for this
@@ -2326,6 +2633,11 @@ void Runtime::unloadProject() {
 	_rootLinkingScope.reset();
 }
 
+void Runtime::refreshPlayTime() {
+	_playTime = _system->getMillis() - _playTimeBase;
+}
+
+
 void Runtime::queueProject(const Common::SharedPtr<ProjectDescription> &desc) {
 	_queuedProjectDesc = desc;
 }
@@ -2353,6 +2665,12 @@ void Runtime::postConsumeMessageTask(IMessageConsumer *consumer, const Common::S
 	params->message = msg;
 }
 
+void Runtime::postConsumeCommandTask(Structural *structural, const Common::SharedPtr<MessageProperties> &msg) {
+	ConsumeCommandTaskData *params = _vthread->pushTask(this, &Runtime::consumeCommandTask);
+	params->structural = structural;
+	params->message = msg;
+}
+
 uint32 Runtime::allocateRuntimeGUID() {
 	return _nextRuntimeGUID++;
 }
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 09b75f06d42..629d20f9e28 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -60,6 +60,7 @@ class Element;
 class MessageDispatch;
 class MiniscriptThread;
 class Modifier;
+class ObjectLinkingScope;
 class PlugInModifier;
 class RuntimeObject;
 class PlugIn;
@@ -73,12 +74,12 @@ struct IModifierContainer;
 struct IModifierFactory;
 struct IPlugInModifierFactory;
 struct IPlugInModifierFactoryAndDataFactory;
+struct IStructuralReferenceVisitor;
 struct MessageProperties;
 struct ModifierLoaderContext;
 struct PlugInModifierLoaderContext;
 template<typename TElement, typename TElementData> class ElementFactory;
 
-
 char invariantToLower(char c);
 Common::String toCaseInsensitive(const Common::String &str);
 bool caseInsensitiveEqual(const Common::String &str1, const Common::String &str2);
@@ -214,6 +215,7 @@ enum EventID {
 	kUserTimeout = 1801,
 	kProjectStarted = 1802,
 	kProjectEnded = 1803,
+	kFlushAllMedia = 1804,
 
 	kAttribGet = 1300,
 	kAttribSet = 1200,
@@ -221,9 +223,20 @@ enum EventID {
 	kClone = 226,
 	kKill = 228,
 
+	kPlay = 201,
+	kStop = 202,
+	kPause = 801,
+	kUnpause = 802,
+	kTogglePause = 803,
+	kAtFirstCel = 804,
+	kAtLastCel = 805,
+
+
 	kAuthorMessage = 900,
 };
 
+bool isCommand(EventID eventID);
+
 } // End of namespace EventIDs
 
 struct Point16 {
@@ -710,10 +723,35 @@ struct MessengerSendSpec {
 	bool load(const Data::Event &dataEvent, uint32 dataMessageFlags, const Data::InternalTypeTaggedValue &dataLocator, const Common::String &dataWithSource, const Common::String &dataWithString, uint32 dataDestination);
 	bool load(const Data::PlugInTypeTaggedValue &dataEvent, const MessageFlags &dataMessageFlags, const Data::PlugInTypeTaggedValue &dataWith, uint32 dataDestination);
 
+	void linkInternalReferences(ObjectLinkingScope *outerScope);
+	void visitInternalReferences(IStructuralReferenceVisitor *visitor);
+	void resolveDestination(Runtime *runtime, Modifier *sender, Common::WeakPtr<Structural> &outStructuralDest, Common::WeakPtr<Modifier> &outModifierDest) const;
+
+	void sendFromMessenger(Runtime *runtime, Modifier *sender) const;
+
 	Event send;
 	MessageFlags messageFlags;
 	DynamicValue with;
 	uint32 destination; // May be a MessageDestination or GUID
+
+	enum LinkType {
+		kLinkTypeNotYetLinked,
+		kLinkTypeStructural,
+		kLinkTypeModifier,
+		kLinkTypeCoded,
+		kLinkTypeUnresolved,
+	};
+
+	LinkType _linkType;
+	Common::WeakPtr<Structural> resolvedStructuralDest;
+	Common::WeakPtr<Modifier> resolvedModifierDest;
+
+private:
+	void resolveHierarchyStructuralDestination(Runtime *runtime, Modifier *sender, Common::WeakPtr<Structural> &outStructuralDest, Common::WeakPtr<Modifier> &outModifierDest, bool (*compareFunc)(Structural *structural)) const;
+	static bool isSceneFilter(Structural *section);
+	static bool isSectionFilter(Structural *section);
+	static bool isSubsectionFilter(Structural *section);
+	static bool isElementFilter(Structural *section);
 };
 
 struct Message {
@@ -724,13 +762,15 @@ struct Message {
 };
 
 enum MessageDestination {
+	kMessageDestNone = 0,
+
 	kMessageDestSharedScene = 0x65,
 	kMessageDestScene = 0x66,
 	kMessageDestSection = 0x67,
 	kMessageDestProject = 0x68,
 	kMessageDestActiveScene = 0x69,
 	kMessageDestElementsParent = 0x6a,
-	kMessageDestChildren = 0x6b,
+	kMessageDestChildren = 0x6b,	// Saw this somewhere but can't find it any more?
 	kMessageDestModifiersParent = 0x6c,
 	kMessageDestSubsection = 0x6d,
 
@@ -880,8 +920,8 @@ struct SceneTransitionEffect {
 
 class MessageDispatch {
 public:
-	MessageDispatch(const Common::SharedPtr<MessageProperties> &msgProps, Structural *root, bool cascade, bool relay);
-	MessageDispatch(const Common::SharedPtr<MessageProperties> &msgProps, Modifier *root, bool cascade, bool relay);
+	MessageDispatch(const Common::SharedPtr<MessageProperties> &msgProps, Structural *root, bool cascade, bool relay, bool couldBeCommand);
+	MessageDispatch(const Common::SharedPtr<MessageProperties> &msgProps, Modifier *root, bool cascade, bool relay, bool couldBeCommand);
 
 	bool isTerminated() const;
 	VThreadState continuePropagating(Runtime *runtime);
@@ -901,6 +941,8 @@ private:
 			kStageSendToStructuralSelf,
 			kStageSendToStructuralModifiers,
 			kStageSendToStructuralChildren,
+
+			kStageSendCommand,
 		};
 
 		PropagationStage propagationStage;
@@ -913,16 +955,64 @@ private:
 	bool _cascade; // Traverses structure tree
 	bool _relay;   // Fire on multiple modifiers
 	bool _terminated;
+	bool _isCommand;
+};
+
+class Scheduler;
+
+class ScheduledEvent : Common::NonCopyable {
+	friend class Scheduler;
+
+public:
+	void cancel();
+	uint64 getScheduledTime() const;
+	void activate(Runtime *runtime) const;
+
+private:
+	ScheduledEvent(void *obj, void (*activateFunc)(void *, Runtime *), uint64 scheduledTime, Scheduler *scheduler);
+
+	void *_obj;
+	void (*_activateFunc)(void *obj, Runtime *runtime);
+
+	uint64 _scheduledTime;
+	Scheduler *_scheduler;
+};
+
+class Scheduler {
+	friend class ScheduledEvent;
+
+public:
+	Scheduler();
+	~Scheduler();
+
+	template<class T, void (T::*TMethodPtr)(Runtime *)>
+	Common::SharedPtr<ScheduledEvent> scheduleMethod(uint64 scheduledTime, T* obj) {
+		Common::SharedPtr<ScheduledEvent> evt(new ScheduledEvent(obj, Scheduler::methodActivateHelper<T, TMethodPtr>, scheduledTime, this));
+		insertEvent(evt);
+		return evt;
+	}
+
+	Common::SharedPtr<ScheduledEvent> getFirstEvent() const;
+	void descheduleFirstEvent();
+
+private:
+	template<class T, void (T::*TMethodPtr)(Runtime *)>
+	static void methodActivateHelper(void *obj, Runtime *runtime) {
+		(static_cast<T *>(obj)->*TMethodPtr)(runtime);
+	}
+
+	void insertEvent(const Common::SharedPtr<ScheduledEvent> &evt);
+	void removeEvent(const ScheduledEvent *evt);
+
+	Common::Array<Common::SharedPtr<ScheduledEvent>> _events;
 };
 
 class Runtime {
 public:
-	Runtime();
+	explicit Runtime(OSystem *system);
 
-	void runRealFrame(uint32 msec);
-	bool runProjectFrame();
-	void advanceProjectTime(uint32 msec);
-	void drawFrame(OSystem *osystem);
+	bool runFrame();
+	void drawFrame();
 	void queueProject(const Common::SharedPtr<ProjectDescription> &desc);
 
 	void addVolume(int volumeID, const char *name, bool isMounted);
@@ -931,6 +1021,7 @@ public:
 	Project *getProject() const;
 
 	void postConsumeMessageTask(IMessageConsumer *msgConsumer, const Common::SharedPtr<MessageProperties> &msg);
+	void postConsumeCommandTask(Structural *structural, const Common::SharedPtr<MessageProperties> &msg);
 
 	uint32 allocateRuntimeGUID();
 
@@ -967,6 +1058,8 @@ public:
 	void sendMessageOnVThread(const Common::SharedPtr<MessageDispatch> &dispatch);
 	void queueMessage(const Common::SharedPtr<MessageDispatch> &dispatch);
 
+	Scheduler &getScheduler();
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	void debugSetEnabled(bool enabled);
 	void debugBreak();
@@ -1006,6 +1099,11 @@ private:
 		Common::SharedPtr<MessageProperties> message;
 	};
 
+	struct ConsumeCommandTaskData {
+		Structural *structural;
+		Common::SharedPtr<MessageProperties> message;
+	};
+
 	static Common::SharedPtr<Structural> findDefaultSharedSceneForScene(Structural *scene);
 	void executeTeardown(const Teardown &teardown);
 	void executeLowLevelSceneStateTransition(const LowLevelSceneStateTransitionAction &transitionAction);
@@ -1020,9 +1118,11 @@ private:
 	void ensureMainWindowExists();
 
 	void unloadProject();
+	void refreshPlayTime();	// Updates play time to be in sync with the system clock.  Used so that events occurring after storage access don't skip.
 
 	VThreadState dispatchMessageTask(const DispatchMethodTaskData &data);
 	VThreadState consumeMessageTask(const ConsumeMessageTaskData &data);
+	VThreadState consumeCommandTask(const ConsumeCommandTaskData &data);
 
 	Common::Array<VolumeState> _volumes;
 	Common::SharedPtr<ProjectDescription> _queuedProjectDesc;
@@ -1057,8 +1157,14 @@ private:
 	uint16 _displayWidth;
 	uint16 _displayHeight;
 
-	uint64 _realTime;
-	uint64 _playTime;
+	uint64 _realTimeBase;
+	uint64 _playTimeBase;
+
+	uint32 _realTime;
+	uint32 _playTime;
+
+	Scheduler _scheduler;
+	OSystem *_system;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	Common::SharedPtr<Debugger> _debugger;
@@ -1133,8 +1239,8 @@ private:
 struct IStructuralReferenceVisitor {
 	virtual void visitChildStructuralRef(Common::SharedPtr<Structural> &structural) = 0;
 	virtual void visitChildModifierRef(Common::SharedPtr<Modifier> &modifier) = 0;
-	virtual void visitWeakStructuralRef(Common::SharedPtr<Structural> &structural) = 0;
-	virtual void visitWeakModifierRef(Common::SharedPtr<Modifier> &modifier) = 0;
+	virtual void visitWeakStructuralRef(Common::WeakPtr<Structural> &structural) = 0;
+	virtual void visitWeakModifierRef(Common::WeakPtr<Modifier> &modifier) = 0;
 };
 
 struct IMessageConsumer {
@@ -1173,6 +1279,8 @@ public:
 	void materializeSelfAndDescendents(Runtime *runtime, ObjectLinkingScope *outerScope);
 	void materializeDescendents(Runtime *runtime, ObjectLinkingScope *outerScope);
 
+	virtual VThreadState consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg);
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	SupportStatus debugGetSupportStatus() const override;
 	const Common::String &debugGetName() const override;
@@ -1186,6 +1294,7 @@ protected:
 	virtual ObjectLinkingScope *getPersistentStructuralScope();
 	virtual ObjectLinkingScope *getPersistentModifierScope();
 
+	// If you override this, you must override visitInternalReferences too.
 	virtual void linkInternalReferences(ObjectLinkingScope *outerScope);
 
 	Structural *_parent;
@@ -1500,7 +1609,8 @@ public:
 protected:
 	bool loadTypicalHeader(const Data::TypicalModifierHeader &typicalHeader);
 
-	// Links any references contained in the object, resolving static GUIDs to runtime object references
+	// Links any references contained in the object, resolving static GUIDs to runtime object references.
+	// If you override this, you must override visitInternalReferences too.
 	virtual void linkInternalReferences(ObjectLinkingScope *scope);
 
 	Common::String _name;


Commit: 6e53185f520c29da11adda7e33476fed2efac0fa
    https://github.com/scummvm/scummvm/commit/6e53185f520c29da11adda7e33476fed2efac0fa
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Initial movie element support

Changed paths:
    common/quicktime.h
    engines/mtropolis/asset_factory.cpp
    engines/mtropolis/asset_factory.h
    engines/mtropolis/assets.cpp
    engines/mtropolis/assets.h
    engines/mtropolis/debug.cpp
    engines/mtropolis/element_factory.cpp
    engines/mtropolis/element_factory.h
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/render.cpp
    engines/mtropolis/render.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/common/quicktime.h b/common/quicktime.h
index 99d1d6499ee..707bf81a15d 100644
--- a/common/quicktime.h
+++ b/common/quicktime.h
@@ -49,6 +49,7 @@ namespace Common {
  * @details File parser used in engines:
  *          - groovie
  *          - mohawk
+ *          - mtropolis
  *          - sci
  * @{
  */
@@ -78,7 +79,7 @@ public:
 
 	/**
 	 * Set the beginning offset of the video so we can modify the offsets in the stco
-	 * atom of videos inside the Mohawk archives
+	 * atom of videos inside the Mohawk/mTropolis archives
 	 * @param offset the beginning offset of the video
 	 */
 	void setChunkBeginOffset(uint32 offset) { _beginOffset = offset; }
diff --git a/engines/mtropolis/asset_factory.cpp b/engines/mtropolis/asset_factory.cpp
index e67080f720e..db796ac26b1 100644
--- a/engines/mtropolis/asset_factory.cpp
+++ b/engines/mtropolis/asset_factory.cpp
@@ -24,7 +24,7 @@
 
 namespace MTropolis {
 
-AssetLoaderContext::AssetLoaderContext() {
+AssetLoaderContext::AssetLoaderContext(size_t streamIndex) : streamIndex(streamIndex) {
 }
 
 template<typename TAsset, typename TAssetData>
diff --git a/engines/mtropolis/asset_factory.h b/engines/mtropolis/asset_factory.h
index 04ad2c38a50..9b3b0a96814 100644
--- a/engines/mtropolis/asset_factory.h
+++ b/engines/mtropolis/asset_factory.h
@@ -28,7 +28,9 @@
 namespace MTropolis {
 
 struct AssetLoaderContext {
-	AssetLoaderContext();
+	explicit AssetLoaderContext(size_t streamIndex);
+
+	size_t streamIndex;
 };
 
 struct IAssetFactory {
diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index e68f5e6018f..b8ffb0a6d6d 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -20,6 +20,7 @@
  */
 
 #include "mtropolis/assets.h"
+#include "mtropolis/asset_factory.h"
 
 namespace MTropolis {
 
@@ -29,6 +30,10 @@ Asset::Asset() : _assetID(0) {
 Asset::~Asset() {
 }
 
+uint32 Asset::getAssetID() const {
+	return _assetID;
+}
+
 bool ColorTableAsset::load(AssetLoaderContext &context, const Data::ColorTableAsset &data) {
 	_assetID = data.assetID;
 	for (int i = 0; i < 256; i++) {
@@ -39,6 +44,10 @@ bool ColorTableAsset::load(AssetLoaderContext &context, const Data::ColorTableAs
 	return true;
 }
 
+AssetType ColorTableAsset::getAssetType() const {
+	return kAssetTypeColorTable;
+}
+
 bool AudioAsset::load(AssetLoaderContext &context, const Data::AudioAsset &data) {
 	_sampleRate = data.sampleRate1;
 	_bitsPerSample = data.bitsPerSample;
@@ -73,14 +82,45 @@ bool AudioAsset::load(AssetLoaderContext &context, const Data::AudioAsset &data)
 	return true;
 }
 
+AssetType AudioAsset::getAssetType() const {
+	return kAssetTypeAudio;
+}
+
 bool MovieAsset::load(AssetLoaderContext &context, const Data::MovieAsset &data) {
+	_assetID = data.assetID;
 	_moovAtomPos = data.moovAtomPos;
 	_movieDataPos = data.movieDataPos;
 	_movieDataSize = data.movieDataSize;
 	_extFileName = data.extFileName;
+	_streamIndex = context.streamIndex;
 
 	return true;
 }
 
+AssetType MovieAsset::getAssetType() const {
+	return kAssetTypeMovie;
+}
+
+uint32 MovieAsset::getMovieDataPos() const {
+	return _movieDataPos;
+}
+
+uint32 MovieAsset::getMoovAtomPos() const {
+	return _moovAtomPos;
+}
+
+uint32 MovieAsset::getMovieDataSize() const {
+	return _movieDataSize;
+}
+
+
+const Common::String &MovieAsset::getExtFileName() const {
+	return _extFileName;
+}
+
+size_t MovieAsset::getStreamIndex() const {
+	return _streamIndex;
+}
+
 
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/assets.h b/engines/mtropolis/assets.h
index 7086e4381ce..ee34993334f 100644
--- a/engines/mtropolis/assets.h
+++ b/engines/mtropolis/assets.h
@@ -32,6 +32,7 @@ struct AssetLoaderContext;
 class ColorTableAsset : public Asset {
 public:
 	bool load(AssetLoaderContext &context, const Data::ColorTableAsset &data);
+	AssetType getAssetType() const override;
 
 private:
 	ColorRGB8 _colors[256];
@@ -51,6 +52,7 @@ public:
 	};
 
 	bool load(AssetLoaderContext &context, const Data::AudioAsset &data);
+	AssetType getAssetType() const override;
 
 private:
 	uint16 _sampleRate;
@@ -67,6 +69,14 @@ private:
 class MovieAsset : public Asset {
 public:
 	bool load(AssetLoaderContext &context, const Data::MovieAsset &data);
+	AssetType getAssetType() const override;
+
+	uint32 getMovieDataPos() const;
+	uint32 getMoovAtomPos() const;
+	uint32 getMovieDataSize() const;
+
+	const Common::String &getExtFileName() const;
+	size_t getStreamIndex() const;
 
 private:
 	uint32 _movieDataPos;
@@ -74,6 +84,7 @@ private:
 	uint32 _movieDataSize;
 
 	Common::String _extFileName;
+	size_t _streamIndex;
 };
 
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/debug.cpp b/engines/mtropolis/debug.cpp
index 57defec3bd8..b55ded7e1a1 100644
--- a/engines/mtropolis/debug.cpp
+++ b/engines/mtropolis/debug.cpp
@@ -95,7 +95,7 @@ void Debugger::notify(DebugSeverity severity, const Common::String& str) {
 	const Graphics::PixelFormat pixelFmt = _runtime->getRenderPixelFormat();
 
 	ToastNotification toastNotification;
-	toastNotification.window.reset(new Window(0, displayHeight, width, toastNotificationHeight, pixelFmt));
+	toastNotification.window.reset(new Window(_runtime, 0, displayHeight, width, toastNotificationHeight, pixelFmt));
 
 	byte fillColor[3] = {255, 255, 255};
 	if (severity == kDebugSeverityError) {
@@ -165,7 +165,7 @@ void Debugger::refreshSceneStatus() {
 
 	const Graphics::PixelFormat pixelFmt = _runtime->getRenderPixelFormat();
 
-	_sceneStatusWindow.reset(new Window(0, 0, horizPadding * 2 + width, vertSpacing * sceneStrs.size(), pixelFmt));
+	_sceneStatusWindow.reset(new Window(_runtime, 0, 0, horizPadding * 2 + width, vertSpacing * sceneStrs.size(), pixelFmt));
 	_runtime->addWindow(_sceneStatusWindow);
 
 	for (uint i = 0; i < sceneStrs.size(); i++) {
diff --git a/engines/mtropolis/element_factory.cpp b/engines/mtropolis/element_factory.cpp
index a86f25666df..217468ee3f3 100644
--- a/engines/mtropolis/element_factory.cpp
+++ b/engines/mtropolis/element_factory.cpp
@@ -25,7 +25,7 @@
 
 namespace MTropolis {
 
-ElementLoaderContext::ElementLoaderContext() {
+ElementLoaderContext::ElementLoaderContext(Runtime *runtime, size_t streamIndex) : runtime(runtime), streamIndex(streamIndex) {
 }
 
 template<typename TElement, typename TElementData>
diff --git a/engines/mtropolis/element_factory.h b/engines/mtropolis/element_factory.h
index 581b9a1c731..dae3329c978 100644
--- a/engines/mtropolis/element_factory.h
+++ b/engines/mtropolis/element_factory.h
@@ -28,7 +28,10 @@
 namespace MTropolis {
 
 struct ElementLoaderContext {
-	ElementLoaderContext();
+	ElementLoaderContext(Runtime *runtime, size_t streamIndex);
+
+	Runtime *runtime;
+	size_t streamIndex;
 };
 
 struct IElementFactory {
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 0e51d0eb66f..6913874221b 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -20,10 +20,19 @@
  */
 
 #include "mtropolis/elements.h"
+#include "mtropolis/assets.h"
+#include "mtropolis/element_factory.h"
+#include "mtropolis/render.h"
+
+#include "video/video_decoder.h"
+#include "video/qt_decoder.h"
+
+#include "common/substream.h"
+#include "graphics/managed_surface.h"
 
 namespace MTropolis {
 
-GraphicElement::GraphicElement() : _directToScreen(false), _cacheBitmap(false) {
+GraphicElement::GraphicElement() : _cacheBitmap(false) {
 }
 
 GraphicElement::~GraphicElement() {
@@ -33,25 +42,29 @@ bool GraphicElement::load(ElementLoaderContext &context, const Data::GraphicElem
 	if (!loadCommon(data.name, data.guid, data.rect1, data.elementFlags, data.layer, data.streamLocator, data.sectionID))
 		return false;
 
-	_directToScreen = ((data.elementFlags & Data::ElementFlags::kNotDirectToScreen) == 0);
 	_cacheBitmap = ((data.elementFlags & Data::ElementFlags::kCacheBitmap) != 0);
 
 	return true;
 }
 
+void GraphicElement::render(Window *window) {
+	// todo
+}
+
 MovieElement::MovieElement()
-	: _directToScreen(false), _cacheBitmap(false), _paused(false)
-	, _loop(false), _alternate(false), _playEveryFrame(false), _assetID(0) {
+	: _cacheBitmap(false), _paused(false)
+	, _loop(false), _alternate(false), _playEveryFrame(false), _assetID(0), _runtime(nullptr) {
 }
 
 MovieElement::~MovieElement() {
+	if (_unloadSignaller)
+		_unloadSignaller->removeReceiver(this);
 }
 
 bool MovieElement::load(ElementLoaderContext &context, const Data::MovieElement &data) {
 	if (!loadCommon(data.name, data.guid, data.rect1, data.elementFlags, data.layer, data.streamLocator, data.sectionID))
 		return false;
 
-	_directToScreen = ((data.elementFlags & Data::ElementFlags::kNotDirectToScreen) == 0);
 	_cacheBitmap = ((data.elementFlags & Data::ElementFlags::kCacheBitmap) != 0);
 	_paused = ((data.elementFlags & Data::ElementFlags::kPaused) != 0);
 	_loop = ((data.animationFlags & Data::AnimationFlags::kLoop) != 0);
@@ -59,15 +72,12 @@ bool MovieElement::load(ElementLoaderContext &context, const Data::MovieElement
 	_playEveryFrame = ((data.animationFlags & Data::AnimationFlags::kPlayEveryFrame) != 0);
 	_assetID = data.assetID;
 
+	_runtime = context.runtime;
+
 	return true;
 }
 
 bool MovieElement::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
-	if (attrib == "direct") {
-		result.setBool(_directToScreen);
-		return true;
-	}
-
 	if (attrib == "paused") {
 		result.setBool(_paused);
 		return true;
@@ -77,10 +87,6 @@ bool MovieElement::readAttribute(MiniscriptThread *thread, DynamicValue &result,
 }
 
 bool MovieElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
-	if (attrib == "direct") {
-		writeProxy = DynamicValueWriteFuncHelper<MovieElement, &MovieElement::scriptSetDirect>::create(this);
-		return true;
-	}
 
 	if (attrib == "paused") {
 		writeProxy = DynamicValueWriteFuncHelper<MovieElement, &MovieElement::scriptSetPaused>::create(this);
@@ -90,12 +96,82 @@ bool MovieElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWrite
 	return VisualElement::writeRefAttribute(thread, writeProxy, attrib);
 }
 
-bool MovieElement::scriptSetDirect(const DynamicValue &dest) {
-	if (dest.getType() == DynamicValueTypes::kBoolean) {
-		_directToScreen = dest.getBool();
-		return true;
+VThreadState MovieElement::consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	if (Event::create(EventIDs::kPlay, 0).respondsTo(msg->getEvent())) {
+		StartPlayingTaskData *startPlayingTaskData = runtime->getVThread().pushTask(this, &MovieElement::startPlayingTask);
+		startPlayingTaskData->runtime = runtime;
+
+		ChangeFlagTaskData *becomeVisibleTaskData = runtime->getVThread().pushTask(static_cast<VisualElement *>(this), &MovieElement::changeVisibilityTask);
+		becomeVisibleTaskData->desiredFlag = true;
+		becomeVisibleTaskData->runtime = runtime;
+	}
+
+	return kVThreadReturn;
+}
+
+
+void MovieElement::activate() {
+	Project *project = _runtime->getProject();
+	Common::SharedPtr<Asset> asset = project->getAssetByID(_assetID).lock();
+
+	if (!asset) {
+		warning("Movie element references asset %i but the asset isn't loaded!", _assetID);
+		return;
+	}
+
+	if (asset->getAssetType() != kAssetTypeMovie) {
+		warning("Movie element assigned an asset that isn't a movie");
+		return;
+	}
+
+	MovieAsset *movieAsset = static_cast<MovieAsset *>(asset.get());
+	size_t streamIndex = movieAsset->getStreamIndex();
+	int segmentIndex = project->getSegmentForStreamIndex(streamIndex);
+	project->openSegmentStream(segmentIndex);
+	Common::SeekableReadStream *stream = project->getStreamForSegment(segmentIndex);
+
+	if (!stream) {
+		warning("Movie element stream could not be opened");
+		return;
+	}
+
+
+	Video::QuickTimeDecoder *qtDecoder = new Video::QuickTimeDecoder();
+	qtDecoder->setChunkBeginOffset(movieAsset->getMovieDataPos());
+
+	_videoDecoder.reset(qtDecoder);
+
+	Common::SafeSeekableSubReadStream *movieDataStream = new Common::SafeSeekableSubReadStream(stream, movieAsset->getMovieDataPos(), movieAsset->getMovieDataPos() + movieAsset->getMovieDataSize(), DisposeAfterUse::NO);
+
+	if (!_videoDecoder->loadStream(movieDataStream))
+		_videoDecoder.reset();
+
+	_unloadSignaller = project->notifyOnSegmentUnload(segmentIndex, this);
+}
+
+void MovieElement::deactivate() {
+	if (_unloadSignaller) {
+		_unloadSignaller->removeReceiver(this);
+		_unloadSignaller.reset();
+	}
+
+	_videoDecoder.reset();
+}
+
+void MovieElement::render(Window *window) {
+	const Graphics::Surface *videoSurface = nullptr;
+	while (_videoDecoder->needsUpdate()) {
+		videoSurface = _videoDecoder->decodeNextFrame();
+		if (_playEveryFrame)
+			break;
+	}
+
+	if (videoSurface) {
+		Graphics::ManagedSurface *target = window->getSurface().get();
+		Common::Rect srcRect(0, 0, videoSurface->w, videoSurface->h);
+		Common::Rect destRect(_rect.left, _rect.top, _rect.right, _rect.bottom);
+		target->blitFrom(*videoSurface, srcRect, destRect);
 	}
-	return false;
 }
 
 bool MovieElement::scriptSetPaused(const DynamicValue& dest) {
@@ -106,4 +182,26 @@ bool MovieElement::scriptSetPaused(const DynamicValue& dest) {
 	return false;
 }
 
+void MovieElement::onSegmentUnloaded(int segmentIndex) {
+	_videoDecoder.reset();
+}
+
+VThreadState MovieElement::startPlayingTask(const StartPlayingTaskData &taskData) {
+	if (_videoDecoder && !_videoDecoder->isPlaying()) {
+		EventIDs::EventID eventToSend = EventIDs::kPlay;
+		if (_paused) {
+			_paused = false;
+			eventToSend = EventIDs::kUnpause;
+		}
+
+		_videoDecoder->start();
+
+		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(eventToSend, 0), DynamicValue(), getSelfReference()));
+		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+		taskData.runtime->sendMessageOnVThread(dispatch);
+	}
+
+	return kVThreadReturn;
+}
+
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index be482254e4d..d45aa122b24 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -25,6 +25,12 @@
 #include "mtropolis/data.h"
 #include "mtropolis/runtime.h"
 
+namespace Video {
+
+class VideoDecoder;
+
+} // End of namespace Video
+
 namespace MTropolis {
 
 struct ElementLoaderContext;
@@ -36,16 +42,18 @@ public:
 
 	bool load(ElementLoaderContext &context, const Data::GraphicElement &data);
 
+	void render(Window *window) override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Graphic Element"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusPartial; }
 #endif
 
 private:
-	bool _directToScreen;
 	bool _cacheBitmap;
 };
 
-class MovieElement : public VisualElement {
+class MovieElement : public VisualElement, public ISegmentUnloadSignalReceiver {
 public:
 	MovieElement();
 	~MovieElement();
@@ -54,22 +62,39 @@ public:
 
 	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
 	bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
+	VThreadState consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
+
+	void activate() override;
+	void deactivate() override;
+
+	void render(Window *window) override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Movie Element"; }
 #endif
 
 private:
-	bool scriptSetDirect(const DynamicValue &dest);
 	bool scriptSetPaused(const DynamicValue &dest);
 
-	bool _directToScreen;
+	void onSegmentUnloaded(int segmentIndex) override;
+
+	struct StartPlayingTaskData {
+		Runtime *runtime;
+	};
+
+	VThreadState startPlayingTask(const StartPlayingTaskData &taskData);
+
 	bool _cacheBitmap;
 	bool _paused;
 	bool _loop;
 	bool _alternate;
 	bool _playEveryFrame;
 	uint32 _assetID;
+
+	Common::SharedPtr<Video::VideoDecoder> _videoDecoder;
+	Common::SharedPtr<SegmentUnloadSignaller> _unloadSignaller;
+
+	Runtime *_runtime;
 };
 
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index 2000363ebaf..c7dbcd38893 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -241,8 +241,6 @@ void MTropolisEngine::handleEvents() {
 Common::Error MTropolisEngine::run() {
 	int preferredWidth = 1024;
 	int preferredHeight = 768;
-	bool enableFrameRateLimit = true;
-	uint expectedFrameRate = 60;
 	ColorDepthMode preferredColorDepthMode = kColorDepthMode8Bit;
 
 	_runtime.reset(new Runtime(_system));
@@ -417,8 +415,6 @@ Common::Error MTropolisEngine::run() {
 
 	bool paused = false;
 
-	int frameCounter = 0;
-	int numSkippedFrames = 0;
 	while (!shouldQuit()) {
 		handleEvents();
 
diff --git a/engines/mtropolis/render.cpp b/engines/mtropolis/render.cpp
index c149f7f3309..12cac6fe828 100644
--- a/engines/mtropolis/render.cpp
+++ b/engines/mtropolis/render.cpp
@@ -64,7 +64,7 @@ inline int quantize8To5(int value, byte orderedDither16x16) {
 	return (value * 249 + (orderedDither16x16 << 3)) >> 11;
 }
 
-Window::Window(int32 x, int32 y, int16 width, int16 height, const Graphics::PixelFormat &format) : _x(x), _y(y) {
+Window::Window(Runtime *runtime, int32 x, int32 y, int16 width, int16 height, const Graphics::PixelFormat &format) : _runtime(runtime), _x(x), _y(y) {
 	_surface.reset(new Graphics::ManagedSurface(width, height, format));
 }
 
@@ -93,6 +93,18 @@ const Graphics::PixelFormat& Window::getPixelFormat() const {
 	return _surface->format;
 }
 
+void Window::close() {
+	Runtime *runtime = _runtime;
+	_runtime = nullptr;
+
+	if (runtime)
+		runtime->removeWindow(this);
+}
+
+void Window::detachFromRuntime() {
+	_runtime = nullptr;
+}
+
 namespace Render {
 
 uint32 resolveRGB(uint8 r, uint8 g, uint8 b, const Graphics::PixelFormat &fmt) {
@@ -104,6 +116,66 @@ uint32 resolveRGB(uint8 r, uint8 g, uint8 b, const Graphics::PixelFormat &fmt) {
 	return rPlaced | gPlaced | bPlaced | aPlaced;
 }
 
+static void recursiveCollectDrawElements(Structural *structural, Common::Array<VisualElement *> &normalBucket, Common::Array<VisualElement *> &directBucket) {
+	if (structural->isElement()) {
+		Element *element = static_cast<Element *>(structural);
+		if (element->isVisual()) {
+			VisualElement *visualElement = static_cast<VisualElement *>(element);
+			if (visualElement->isVisible()) {
+				if (visualElement->isDirectToScreen())
+					directBucket.push_back(visualElement);
+				else
+					normalBucket.push_back(visualElement);
+			}
+		}
+	}
+
+	for (Common::Array<Common::SharedPtr<Structural> >::const_iterator it = structural->getChildren().begin(), itEnd = structural->getChildren().end(); it != itEnd; ++it) {
+		recursiveCollectDrawElements(it->get(), normalBucket, directBucket);
+	}
+}
+
+static bool visualElementLayerLess(VisualElement *a, VisualElement *b) {
+	return a->getLayer() < b->getLayer();
+}
+
+static void renderNormalElement(VisualElement *element, Window *mainWindow) {
+	element->render(mainWindow);
+}
+
+static void renderDirectElement(VisualElement *element, Window *mainWindow) {
+	renderNormalElement(element, mainWindow);	// Meh
+}
+
+void renderProject(Runtime *runtime, Window *mainWindow) {
+	Common::Array<Structural *> scenes;
+	runtime->getScenesInRenderOrder(scenes);
+
+	Common::Array<VisualElement *> normalBucket;
+	Common::Array<VisualElement *> directBucket;
+
+	for (Common::Array<Structural *>::const_iterator it = scenes.begin(), itEnd = scenes.end(); it != itEnd; ++it) {
+		size_t normalStart = normalBucket.size();
+		size_t directStart = directBucket.size();
+
+		recursiveCollectDrawElements(*it, normalBucket, directBucket);
+
+		size_t normalEnd = normalBucket.size();
+		size_t directEnd = directBucket.size();
+
+		if (normalEnd - normalStart > 1)
+			Common::sort(normalBucket.begin() + normalStart, normalBucket.end(), visualElementLayerLess);
+		if (directEnd - directStart > 1)
+			Common::sort(directBucket.begin() + directStart, directBucket.end(), visualElementLayerLess);
+	}
+
+	for (Common::Array<VisualElement *>::const_iterator it = normalBucket.begin(), itEnd = normalBucket.end(); it != itEnd; ++it)
+		renderNormalElement(*it, mainWindow);
+
+	for (Common::Array<VisualElement *>::const_iterator it = directBucket.begin(), itEnd = directBucket.end(); it != itEnd; ++it)
+		renderDirectElement(*it, mainWindow);
+}
+
 } // End of namespace Render
 
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/render.h b/engines/mtropolis/render.h
index a63b017c43a..782c3451fa1 100644
--- a/engines/mtropolis/render.h
+++ b/engines/mtropolis/render.h
@@ -35,9 +35,12 @@ class ManagedSurface;
 
 namespace MTropolis {
 
+class Runtime;
+class Project;
+
 class Window {
 public:
-	Window(int32 x, int32 y, int16 width, int16 height, const Graphics::PixelFormat &format);
+	Window(Runtime *runtime, int32 x, int32 y, int16 width, int16 height, const Graphics::PixelFormat &format);
 	~Window();
 
 	int32 getX() const;
@@ -47,15 +50,20 @@ public:
 	const Common::SharedPtr<Graphics::ManagedSurface> &getSurface() const;
 	const Graphics::PixelFormat &getPixelFormat() const;
 
+	void close();
+	void detachFromRuntime();
+
 private:
 	int32 _x;
 	int32 _y;
 	Common::SharedPtr<Graphics::ManagedSurface> _surface;
+	Runtime *_runtime;
 };
 
 namespace Render {
 
 uint32 resolveRGB(uint8 r, uint8 g, uint8 b, const Graphics::PixelFormat &fmt);
+void renderProject(Runtime *runtime, Window *mainWindow);
 
 } // End of namespace Render
 
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 0c2c2182326..6a75c6badf7 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -95,6 +95,20 @@ void ModifierChildMaterializer::visitChildStructuralRef(Common::SharedPtr<Struct
 }
 
 void ModifierChildMaterializer::visitChildModifierRef(Common::SharedPtr<Modifier> &modifier) {
+	if (modifier->isAlias()) {
+		Common::SharedPtr<Modifier> templateModifier = _runtime->getProject()->resolveAlias(static_cast<AliasModifier *>(modifier.get())->getAliasID());
+		if (!templateModifier) {
+			error("Failed to resolve alias");
+		}
+
+		if (!modifier->isVariable()) {
+			Common::SharedPtr<Modifier> clonedModifier = templateModifier->shallowClone();
+			clonedModifier->setName(modifier->getName());
+
+			modifier = clonedModifier;
+		}
+	}
+
 	modifier->materialize(_runtime, _outerScope);
 }
 
@@ -1708,12 +1722,18 @@ VThreadState Structural::consumeCommand(Runtime *runtime, const Common::SharedPt
 	return kVThreadReturn;
 }
 
+void Structural::activate() {
+}
+
+void Structural::deactivate() {
+}
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 SupportStatus Structural::debugGetSupportStatus() const {
 	return kSupportStatusNone;
 }
 
-const Common::String& Structural::debugGetName() const {
+const Common::String &Structural::debugGetName() const {
 	return _name;
 }
 
@@ -2121,11 +2141,13 @@ bool Runtime::runFrame() {
 
 			_macFontMan.reset(new Graphics::MacFontManager(0, desc->getLanguage()));
 
-			_project.reset(new Project());
+			_project.reset(new Project(this));
 			_project->setSelfReference(_project);
 
 			_project->loadFromDescription(*desc);
 
+			ensureMainWindowExists();
+
 			_rootLinkingScope.addObject(_project->getStaticGUID(), _project->getName(), _project);
 
 			// We have to materialize global variables because they are not cloned from aliases.
@@ -2229,6 +2251,12 @@ void Runtime::drawFrame() {
 
 	_system->fillScreen(Render::resolveRGB(0, 0, 0, getRenderPixelFormat()));
 
+	{
+		Common::SharedPtr<Window> mainWindow = _mainWindow.lock();
+		if (mainWindow)
+			Render::renderProject(this, mainWindow.get());
+	}
+
 	for (Common::Array<Common::SharedPtr<Window> >::const_iterator it = _windows.begin(), itEnd = _windows.end(); it != itEnd; ++it) {
 		const Window &window = *it->get();
 		const Graphics::ManagedSurface &surface = *window.getSurface();
@@ -2292,6 +2320,8 @@ void Runtime::executeTeardown(const Teardown &teardown) {
 	if (!structural)
 		return;	// Already gone
 
+	recursiveDeactivateStructural(structural.get());
+
 	if (teardown.onlyRemoveChildren) {
 		structural->removeAllChildren();
 		structural->removeAllModifiers();
@@ -2533,6 +2563,24 @@ void Runtime::executeSharedScenePostSceneChangeActions() {
 	}
 }
 
+void Runtime::recursiveDeactivateStructural(Structural *structural) {
+	const Common::Array<Common::SharedPtr<Structural> > &children = structural->getChildren();
+	for (Common::Array<Common::SharedPtr<Structural> >::const_iterator it = children.begin(), itEnd = children.end(); it != itEnd; ++it) {
+		recursiveDeactivateStructural(it->get());
+	}
+
+	structural->deactivate();
+}
+
+void Runtime::recursiveActivateStructural(Structural *structural) {
+	structural->activate();
+
+	const Common::Array<Common::SharedPtr<Structural> > &children = structural->getChildren();
+	for (Common::Array<Common::SharedPtr<Structural> >::const_iterator it = children.begin(), itEnd = children.end(); it != itEnd; ++it) {
+		recursiveActivateStructural(it->get());
+	}
+}
+
 void Runtime::queueEventAsLowLevelSceneStateTransitionAction(const Event &evt, Structural *root, bool cascade, bool relay) {
 	Common::SharedPtr<MessageProperties> props(new MessageProperties(evt, DynamicValue(), Common::WeakPtr<RuntimeObject>()));
 	Common::SharedPtr<MessageDispatch> msg(new MessageDispatch(props, root, cascade, relay, false));
@@ -2550,6 +2598,8 @@ void Runtime::loadScene(const Common::SharedPtr<Structural>& scene) {
 	debug(1, "Scene loaded OK, materializing objects...");
 	scene->materializeDescendents(this, subsection->getSceneLoadMaterializeScope());
 	debug(1, "Scene materialized OK");
+	recursiveActivateStructural(scene.get());
+	debug(1, "Scene added to renderer OK");
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	if (_debugger) {
@@ -2599,6 +2649,12 @@ Scheduler &Runtime::getScheduler() {
 	return _scheduler;
 }
 
+void Runtime::getScenesInRenderOrder(Common::Array<Structural*> &scenes) const {
+	for (Common::Array<SceneStackEntry>::const_iterator it = _sceneStack.begin(), itEnd = _sceneStack.end(); it != itEnd; ++it) {
+		scenes.push_back(it->scene.get());
+	}
+}
+
 
 void Runtime::ensureMainWindowExists() {
 	// Maybe there's a better spot for this
@@ -2607,7 +2663,7 @@ void Runtime::ensureMainWindowExists() {
 
 		int32 centeredX = (static_cast<int32>(_displayWidth) - static_cast<int32>(presentationSettings.width)) / 2;
 		int32 centeredY = (static_cast<int32>(_displayHeight) - static_cast<int32>(presentationSettings.height)) / 2;
-		Common::SharedPtr<Window> mainWindow(new Window(centeredX, centeredY, presentationSettings.width, presentationSettings.height, _displayModePixelFormats[_realDisplayMode]));
+		Common::SharedPtr<Window> mainWindow(new Window(this, centeredX, centeredY, presentationSettings.width, presentationSettings.height, _displayModePixelFormats[_realDisplayMode]));
 		addWindow(mainWindow);
 		_mainWindow.reset(mainWindow);
 	}
@@ -2682,6 +2738,7 @@ void Runtime::addWindow(const Common::SharedPtr<Window> &window) {
 void Runtime::removeWindow(Window *window) {
 	for (size_t i = 0; i < _windows.size(); i++) {
 		if (_windows[i].get() == window) {
+			window->detachFromRuntime();
 			_windows.remove_at(i);
 			break;
 		}
@@ -2806,11 +2863,46 @@ const IPlugInModifierFactory *ProjectPlugInRegistry::findPlugInModifierFactory(c
 	return it->_value;
 }
 
-Project::Project()
-	: _projectFormat(Data::kProjectFormatUnknown), _isBigEndian(false), _haveGlobalObjectInfo(false), _haveProjectStructuralDef(false) {
+
+SegmentUnloadSignaller::SegmentUnloadSignaller(Project *project, int segmentIndex) : _project(project), _segmentIndex(segmentIndex) {
+}
+
+SegmentUnloadSignaller::~SegmentUnloadSignaller() {
+}
+
+void SegmentUnloadSignaller::onSegmentUnloaded() {
+	_project = nullptr;
+
+	// Need to be careful here because a receiver may unload this object, causing _receivers.size() to be invalid
+	const size_t numReceivers = _receivers.size();
+	for (size_t i = 0; i < numReceivers; i++) {
+		_receivers[i]->onSegmentUnloaded(_segmentIndex);
+	}
+}
+
+void SegmentUnloadSignaller::addReceiver(ISegmentUnloadSignalReceiver *receiver) {
+	_receivers.push_back(receiver);
+}
+
+void SegmentUnloadSignaller::removeReceiver(ISegmentUnloadSignalReceiver *receiver) {
+	for (size_t i = 0; i < _receivers.size(); i++) {
+		if (_receivers[i] == receiver) {
+			_receivers.remove_at(i);
+			break;
+		}
+	}
+}
+
+Project::Segment::Segment() : weakStream(nullptr) {
+}
+
+Project::Project(Runtime *runtime)
+	: _runtime(runtime), _projectFormat(Data::kProjectFormatUnknown), _isBigEndian(false), _haveGlobalObjectInfo(false), _haveProjectStructuralDef(false) {
 }
 
 Project::~Project() {
+	for (size_t i = 0; i < _segments.size(); i++)
+		closeSegmentStream(i);
 }
 
 void Project::loadFromDescription(const ProjectDescription& desc) {
@@ -2927,7 +3019,9 @@ void Project::loadSceneFromStream(const Common::SharedPtr<Structural>& scene, ui
 		error("Invalid stream ID");
 	}
 
-	const StreamDesc &streamDesc = _streams[streamID - 1];
+	size_t streamIndex = streamID - 1;
+
+	const StreamDesc &streamDesc = _streams[streamIndex];
 	uint segmentIndex = streamDesc.segmentIndex;
 
 	openSegmentStream(segmentIndex);
@@ -2972,12 +3066,12 @@ void Project::loadSceneFromStream(const Common::SharedPtr<Structural>& scene, ui
 
 		if (Data::DataObjectTypes::isAsset(dataObjectType)) {
 			// Asset defs can appear anywhere
-			loadAssetDef(assetDefLoader, *dataObject.get());
+			loadAssetDef(streamIndex, assetDefLoader, *dataObject.get());
 		} else if (dataObjectType == Data::DataObjectTypes::kAssetDataChunk) {
 			// Ignore
 			continue;
 		} else if (loaderStack.contexts.size() > 0) {
-			loadContextualObject(loaderStack, *dataObject.get());
+			loadContextualObject(streamIndex, loaderStack, *dataObject.get());
 		} else {
 			error("Unexpectedly exited scene context in loader");
 		}
@@ -2990,6 +3084,7 @@ void Project::loadSceneFromStream(const Common::SharedPtr<Structural>& scene, ui
 	}
 
 	scene->holdAssets(assetDefLoader.assets);
+	assignAssets(assetDefLoader.assets);
 }
 
 Common::SharedPtr<Modifier> Project::resolveAlias(uint32 aliasID) const {
@@ -3018,6 +3113,21 @@ bool Project::isProject() const {
 	return true;
 }
 
+Common::WeakPtr<Asset> Project::getAssetByID(uint32 assetID) const {
+	if (assetID >= _assetsByID.size())
+		return Common::WeakPtr<Asset>();
+
+	const AssetDesc *desc = _assetsByID[assetID];
+	if (desc == nullptr)
+		return Common::WeakPtr<Asset>();
+
+	return desc->asset;
+}
+
+size_t Project::getSegmentForStreamIndex(size_t streamIndex) const {
+	return _streams[streamIndex].segmentIndex;
+}
+
 void Project::openSegmentStream(int segmentIndex) {
 	if (segmentIndex < 0 || static_cast<size_t>(segmentIndex) > _segments.size()) {
 		error("Invalid segment index %i", segmentIndex);
@@ -3040,6 +3150,31 @@ void Project::openSegmentStream(int segmentIndex) {
 			error("Failed to open segment file %s", segment.desc.filePath.c_str());
 		}
 	}
+
+	segment.unloadSignaller.reset(new SegmentUnloadSignaller(this, segmentIndex));
+}
+
+void Project::closeSegmentStream(int segmentIndex) {
+	Segment &segment = _segments[segmentIndex];
+
+	if (!segment.weakStream)
+		return;
+
+	segment.unloadSignaller->onSegmentUnloaded();
+	segment.unloadSignaller.reset();
+	segment.rcStream.reset();
+	segment.weakStream = nullptr;
+}
+
+Common::SeekableReadStream* Project::getStreamForSegment(int segmentIndex) {
+	return _segments[segmentIndex].weakStream;
+}
+
+Common::SharedPtr<SegmentUnloadSignaller> Project::notifyOnSegmentUnload(int segmentIndex, ISegmentUnloadSignalReceiver *receiver) {
+	Common::SharedPtr<SegmentUnloadSignaller> signaller = _segments[segmentIndex].unloadSignaller;
+	if (signaller)
+		signaller->addReceiver(receiver);
+	return signaller;
 }
 
 void Project::loadBootStream(size_t streamIndex) {
@@ -3069,12 +3204,12 @@ void Project::loadBootStream(size_t streamIndex) {
 
 		if (Data::DataObjectTypes::isAsset(dataObjectType)) {
 			// Asset defs can appear anywhere
-			loadAssetDef(assetDefLoader, *dataObject.get());
+			loadAssetDef(streamIndex, assetDefLoader, *dataObject.get());
 		} else if (dataObjectType == Data::DataObjectTypes::kAssetDataChunk) {
 			// Ignore
 			continue;
 		} else if (loaderStack.contexts.size() > 0) {
-			loadContextualObject(loaderStack, *dataObject.get());
+			loadContextualObject(streamIndex, loaderStack, *dataObject.get());
 		} else {
 			// Root-level objects
 			switch (dataObject->getType()) {
@@ -3120,6 +3255,7 @@ void Project::loadBootStream(size_t streamIndex) {
 	}
 
 	holdAssets(assetDefLoader.assets);
+	assignAssets(assetDefLoader.assets);
 }
 
 void Project::loadPresentationSettings(const Data::PresentationSettings &presentationSettings) {
@@ -3167,7 +3303,7 @@ void Project::loadAssetCatalog(const Data::AssetCatalog &assetCatalog) {
 	}
 }
 
-void Project::loadGlobalObjectInfo(ChildLoaderStack& loaderStack, const Data::GlobalObjectInfo& globalObjectInfo) {
+void Project::loadGlobalObjectInfo(ChildLoaderStack &loaderStack, const Data::GlobalObjectInfo& globalObjectInfo) {
 	if (_haveGlobalObjectInfo)
 		error("Multiple global object infos");
 
@@ -3280,7 +3416,28 @@ ObjectLinkingScope *Project::getPersistentModifierScope() {
 	return &_modifierScope;
 }
 
-void Project::loadContextualObject(ChildLoaderStack &stack, const Data::DataObject &dataObject) {
+void Project::assignAssets(const Common::Array<Common::SharedPtr<Asset> >& assets) {
+	for (Common::Array<Common::SharedPtr<Asset> >::const_iterator it = assets.begin(), itEnd = assets.end(); it != itEnd; ++it) {
+		Common::SharedPtr<Asset> asset = *it;
+		uint32 assetID = asset->getAssetID();
+
+		if (assetID >= _assetsByID.size()) {
+			warning("Bad asset ID %u", assetID);
+			continue;
+		}
+
+		AssetDesc *desc = _assetsByID[assetID];
+		if (desc == nullptr) {
+			warning("Asset attempting to use deleted asset slot %u", assetID);
+			continue;
+		}
+
+		if (desc->asset.expired())
+			desc->asset = asset;
+	}
+}
+
+void Project::loadContextualObject(size_t streamIndex, ChildLoaderStack &stack, const Data::DataObject &dataObject) {
 	ChildLoaderContext &topContext = stack.contexts.back();
 	const Data::DataObjectTypes::DataObjectType dataObjectType = dataObject.getType();
 
@@ -3408,7 +3565,7 @@ void Project::loadContextualObject(ChildLoaderStack &stack, const Data::DataObje
 					error("No element factory defined for structural object");
 				}
 
-				ElementLoaderContext elementLoaderContext;
+				ElementLoaderContext elementLoaderContext(_runtime, streamIndex);
 				Common::SharedPtr<Element> element = elementFactory->createElement(elementLoaderContext, dataObject);
 
 				container->addChild(element);
@@ -3448,7 +3605,7 @@ void Project::loadContextualObject(ChildLoaderStack &stack, const Data::DataObje
 	}
 }
 
-void Project::loadAssetDef(AssetDefLoaderContext& context, const Data::DataObject& dataObject) {
+void Project::loadAssetDef(size_t streamIndex, AssetDefLoaderContext& context, const Data::DataObject& dataObject) {
 	assert(Data::DataObjectTypes::isAsset(dataObject.getType()));
 
 	IAssetFactory *factory = getAssetFactoryForDataObjectType(dataObject.getType());
@@ -3457,7 +3614,7 @@ void Project::loadAssetDef(AssetDefLoaderContext& context, const Data::DataObjec
 		return;
 	}
 
-	AssetLoaderContext loaderContext;
+	AssetLoaderContext loaderContext(streamIndex);
 	context.assets.push_back(factory->createAsset(loaderContext, dataObject));
 }
 
@@ -3519,11 +3676,23 @@ bool VisualElement::isVisible() const {
 	return _visible;
 }
 
+bool VisualElement::isDirectToScreen() const {
+	return _directToScreen;
+}
+
+uint16 VisualElement::getLayer() const {
+	return _layer;
+}
+
 bool VisualElement::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
 	if (attrib == "visible") {
 		result.setBool(_visible);
 		return true;
 	}
+	if (attrib == "direct") {
+		result.setBool(_directToScreen);
+		return true;
+	}
 
 	return Element::readAttribute(thread, result, attrib);
 }
@@ -3533,11 +3702,16 @@ bool VisualElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWrit
 		writeProxy = DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetVisibility>::create(this);
 		return true;
 	}
+	if (attrib == "direct") {
+		writeProxy = DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetDirect>::create(this);
+		return true;
+	}
 
 	return Element::writeRefAttribute(thread, writeProxy, attrib);
 }
 
 bool VisualElement::scriptSetVisibility(const DynamicValue& result) {
+	// FIXME: Need to make this fire Show/Hide events!
 	if (result.getType() == DynamicValueTypes::kBoolean) {
 		_visible = result.getBool();
 		return true;
@@ -3553,6 +3727,7 @@ bool VisualElement::loadCommon(const Common::String &name, uint32 guid, const Da
 	_name = name;
 	_guid = guid;
 	_visible = ((elementFlags & Data::ElementFlags::kHidden) == 0);
+	_directToScreen = ((elementFlags & Data::ElementFlags::kNotDirectToScreen) == 0);
 	_streamLocator = streamLocator;
 	_sectionID = sectionID;
 	_layer = layer;
@@ -3560,6 +3735,26 @@ bool VisualElement::loadCommon(const Common::String &name, uint32 guid, const Da
 	return true;
 }
 
+bool VisualElement::scriptSetDirect(const DynamicValue &dest) {
+	if (dest.getType() == DynamicValueTypes::kBoolean) {
+		_directToScreen = dest.getBool();
+		return true;
+	}
+	return false;
+}
+
+VThreadState VisualElement::changeVisibilityTask(const ChangeFlagTaskData &taskData) {
+	if (_visible != taskData.desiredFlag) {
+		_visible = taskData.desiredFlag;
+
+		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(_visible ? EventIDs::kElementHide : EventIDs::kElementShow, 0), DynamicValue(), getSelfReference()));
+		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+		taskData.runtime->sendMessageOnVThread(dispatch);
+	}
+
+	return kVThreadReturn;
+}
+
 bool NonVisualElement::isVisual() const {
 	return false;
 }
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 629d20f9e28..63f28259a8b 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -1060,6 +1060,8 @@ public:
 
 	Scheduler &getScheduler();
 
+	void getScenesInRenderOrder(Common::Array<Structural *> &scenes) const;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	void debugSetEnabled(bool enabled);
 	void debugBreak();
@@ -1111,6 +1113,9 @@ private:
 	void executeCompleteTransitionToScene(const Common::SharedPtr<Structural> &scene);
 	void executeSharedScenePostSceneChangeActions();
 
+	void recursiveDeactivateStructural(Structural *structural);
+	void recursiveActivateStructural(Structural *structural);
+
 	void queueEventAsLowLevelSceneStateTransitionAction(const Event &evt, Structural *root, bool cascade, bool relay);
 
 	void loadScene(const Common::SharedPtr<Structural> &scene);
@@ -1281,6 +1286,9 @@ public:
 
 	virtual VThreadState consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg);
 
+	virtual void activate();
+	virtual void deactivate();
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	SupportStatus debugGetSupportStatus() const override;
 	const Common::String &debugGetName() const override;
@@ -1368,10 +1376,28 @@ private:
 	Common::HashMap<Common::String, const IPlugInModifierFactory *> _factoryRegistry;
 };
 
-class Project : public Structural {
+struct ISegmentUnloadSignalReceiver {
+	virtual void onSegmentUnloaded(int segmentIndex) = 0;
+};
+
+class SegmentUnloadSignaller {
+public:
+	explicit SegmentUnloadSignaller(Project *project, int segmentIndex);
+	~SegmentUnloadSignaller();
 
+	void onSegmentUnloaded();
+	void addReceiver(ISegmentUnloadSignalReceiver *receiver);
+	void removeReceiver(ISegmentUnloadSignalReceiver *receiver);
+
+private:
+	Project *_project;
+	int _segmentIndex;
+	Common::Array<ISegmentUnloadSignalReceiver *> _receivers;
+};
+
+class Project : public Structural {
 public:
-	Project();
+	explicit Project(Runtime *runtime);
 	~Project();
 
 	void loadFromDescription(const ProjectDescription &desc);
@@ -1384,6 +1410,13 @@ public:
 
 	bool isProject() const override;
 
+	Common::WeakPtr<Asset> getAssetByID(uint32 assetID) const;
+	size_t getSegmentForStreamIndex(size_t streamIndex) const;
+	void openSegmentStream(int segmentIndex);
+	void closeSegmentStream(int segmentIndex);
+	Common::SeekableReadStream *getStreamForSegment(int segmentIndex);
+	Common::SharedPtr<SegmentUnloadSignaller> notifyOnSegmentUnload(int segmentIndex, ISegmentUnloadSignalReceiver *receiver);
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Project"; }
 #endif
@@ -1407,9 +1440,12 @@ private:
 	};
 
 	struct Segment {
+		Segment();
+
 		SegmentDescription desc;
 		Common::SharedPtr<Common::SeekableReadStream> rcStream;
 		Common::SeekableReadStream *weakStream;
+		Common::SharedPtr<SegmentUnloadSignaller> unloadSignaller;
 	};
 
 	enum StreamType {
@@ -1436,14 +1472,13 @@ private:
 		Common::WeakPtr<Asset> asset;
 	};
 
-	void openSegmentStream(int segmentIndex);
 	void loadBootStream(size_t streamIndex);
 
 	void loadPresentationSettings(const Data::PresentationSettings &presentationSettings);
 	void loadAssetCatalog(const Data::AssetCatalog &assetCatalog);
 	void loadGlobalObjectInfo(ChildLoaderStack &loaderStack, const Data::GlobalObjectInfo &globalObjectInfo);
-	void loadAssetDef(AssetDefLoaderContext &context, const Data::DataObject &dataObject);
-	void loadContextualObject(ChildLoaderStack &stack, const Data::DataObject &dataObject);
+	void loadAssetDef(size_t streamIndex, AssetDefLoaderContext &context, const Data::DataObject &dataObject);
+	void loadContextualObject(size_t streamIndex, ChildLoaderStack &stack, const Data::DataObject &dataObject);
 	Common::SharedPtr<Modifier> loadModifierObject(ModifierLoaderContext &loaderContext, const Data::DataObject &dataObject);
 	void loadLabelMap(const Data::ProjectLabelMap &projectLabelMap);
 	static size_t recursiveCountLabels(const Data::ProjectLabelMap::LabelTree &tree);
@@ -1451,6 +1486,8 @@ private:
 	ObjectLinkingScope *getPersistentStructuralScope() override;
 	ObjectLinkingScope *getPersistentModifierScope() override;
 
+	void assignAssets(const Common::Array<Common::SharedPtr<Asset> > &assets);
+
 	Common::Array<Segment> _segments;
 	Common::Array<StreamDesc> _streams;
 	Common::Array<LabelTree> _labelTree;
@@ -1475,6 +1512,8 @@ private:
 	Common::SharedPtr<ProjectResources> _resources;
 	ObjectLinkingScope _structuralScope;
 	ObjectLinkingScope _modifierScope;
+
+	Runtime *_runtime;
 };
 
 class Section : public Structural {
@@ -1532,15 +1571,29 @@ public:
 	bool isVisual() const override;
 
 	bool isVisible() const;
+	bool isDirectToScreen() const;
+	uint16 getLayer() const;
 
 	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
 	bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
 
 	bool scriptSetVisibility(const DynamicValue &result);
 
+	virtual void render(Window *window) = 0;
+
 protected:
 	bool loadCommon(const Common::String &name, uint32 guid, const Data::Rect &rect, uint32 elementFlags, uint16 layer, uint32 streamLocator, uint16 sectionID);
 
+	bool scriptSetDirect(const DynamicValue &dest);
+
+	struct ChangeFlagTaskData {
+		bool desiredFlag;
+		Runtime *runtime;
+	};
+
+	VThreadState changeVisibilityTask(const ChangeFlagTaskData &taskData);
+
+	bool _directToScreen;
 	bool _visible;
 	Rect16 _rect;
 	uint16 _layer;
@@ -1630,11 +1683,23 @@ public:
 	virtual void getValue(DynamicValue &dest) const = 0;
 };
 
+enum AssetType {
+	kAssetTypeNone,
+
+	kAssetTypeMovie,
+	kAssetTypeAudio,
+	kAssetTypeColorTable,
+};
+
 class Asset {
 public:
 	Asset();
 	virtual ~Asset();
 
+	uint32 getAssetID() const;
+
+	virtual AssetType getAssetType() const = 0;
+
 protected:
 	uint32 _assetID;
 };


Commit: 27645653319a772f2832def4293785a82c9d29bf
    https://github.com/scummvm/scummvm/commit/27645653319a772f2832def4293785a82c9d29bf
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix up modifier child cloning not working correctly, fix crash on invalid Miniscript send destination

Changed paths:
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index e2509d33e9a..c1761d7c1fa 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -531,6 +531,11 @@ MiniscriptInstructionOutcome Send::execute(MiniscriptThread *thread) const {
 
 	Common::SharedPtr<RuntimeObject> obj = targetValue.getObject().lock();
 
+	if (!obj) {
+		thread->error("Invalid message destination");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
 	Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(_evt, payloadValue, thread->getModifier()->getSelfReference()));
 	Common::SharedPtr<MessageDispatch> dispatch;
 	if (obj->isModifier())
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 4619fb38725..a31d45ac9fc 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -783,6 +783,34 @@ void CompoundVariableModifier::visitInternalReferences(IStructuralReferenceVisit
 	}
 }
 
+bool CompoundVariableModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	VariableModifier *var = findChildByName(attrib);
+	if (!var)
+		return false;
+
+	var->getValue(result);
+	return true;
+}
+
+bool CompoundVariableModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
+	VariableModifier *var = findChildByName(attrib);
+	if (!var)
+		return false;
+
+	writeProxy = DynamicValueWriteFuncHelper<VariableModifier, &VariableModifier::setValue>::create(var);
+	return true;
+}
+
+VariableModifier *CompoundVariableModifier::findChildByName(const Common::String &name) const {
+	for (Common::Array<Common::SharedPtr<Modifier> >::const_iterator it = _children.begin(), itEnd = _children.end(); it != itEnd; ++it) {
+		Modifier *modifier = it->get();
+		if (modifier->isVariable() && caseInsensitiveEqual(name, modifier->getName()))
+			return static_cast<VariableModifier *>(modifier);
+	}
+
+	return nullptr;
+}
+
 Common::SharedPtr<Modifier> CompoundVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new CompoundVariableModifier(*this));
 }
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 3048eb3fcf3..4634e2302a7 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -569,6 +569,11 @@ private:
 	void appendModifier(const Common::SharedPtr<Modifier> &modifier) override;
 	void visitInternalReferences(IStructuralReferenceVisitor *visitor) override;
 
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
+	bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) override;
+
+	VariableModifier *findChildByName(const Common::String &name) const;
+
 	Common::Array<Common::SharedPtr<Modifier> > _children;
 };
 
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 6a75c6badf7..0ef89b6b151 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -74,7 +74,7 @@ void ModifierInnerScopeBuilder::visitWeakModifierRef(Common::WeakPtr<Modifier> &
 
 class ModifierChildMaterializer : public IStructuralReferenceVisitor {
 public:
-	ModifierChildMaterializer(Runtime *runtime, ObjectLinkingScope *outerScope);
+	ModifierChildMaterializer(Runtime *runtime, Modifier *modifier, ObjectLinkingScope *outerScope);
 
 	void visitChildStructuralRef(Common::SharedPtr<Structural> &structural) override;
 	void visitChildModifierRef(Common::SharedPtr<Modifier> &modifier) override;
@@ -83,11 +83,26 @@ public:
 
 private:
 	Runtime *_runtime;
+	Modifier *_modifier;
 	ObjectLinkingScope *_outerScope;
 };
 
-ModifierChildMaterializer::ModifierChildMaterializer(Runtime *runtime, ObjectLinkingScope *outerScope)
-	: _runtime(runtime), _outerScope(outerScope) {
+class ModifierChildCloner : public IStructuralReferenceVisitor {
+public:
+	ModifierChildCloner(Runtime *runtime, const Common::WeakPtr<RuntimeObject> &relinkParent);
+
+	void visitChildStructuralRef(Common::SharedPtr<Structural> &structural) override;
+	void visitChildModifierRef(Common::SharedPtr<Modifier> &modifier) override;
+	void visitWeakStructuralRef(Common::WeakPtr<Structural> &structural) override;
+	void visitWeakModifierRef(Common::WeakPtr<Modifier> &modifier) override;
+
+private:
+	Runtime *_runtime;
+	Common::WeakPtr<RuntimeObject> _relinkParent;
+};
+
+ModifierChildMaterializer::ModifierChildMaterializer(Runtime *runtime, Modifier *modifier, ObjectLinkingScope *outerScope)
+	: _runtime(runtime), _modifier(modifier), _outerScope(outerScope) {
 }
 
 void ModifierChildMaterializer::visitChildStructuralRef(Common::SharedPtr<Structural> &structural) {
@@ -95,20 +110,7 @@ void ModifierChildMaterializer::visitChildStructuralRef(Common::SharedPtr<Struct
 }
 
 void ModifierChildMaterializer::visitChildModifierRef(Common::SharedPtr<Modifier> &modifier) {
-	if (modifier->isAlias()) {
-		Common::SharedPtr<Modifier> templateModifier = _runtime->getProject()->resolveAlias(static_cast<AliasModifier *>(modifier.get())->getAliasID());
-		if (!templateModifier) {
-			error("Failed to resolve alias");
-		}
-
-		if (!modifier->isVariable()) {
-			Common::SharedPtr<Modifier> clonedModifier = templateModifier->shallowClone();
-			clonedModifier->setName(modifier->getName());
-
-			modifier = clonedModifier;
-		}
-	}
-
+	_runtime->instantiateIfAlias(modifier, _modifier->getSelfReference());
 	modifier->materialize(_runtime, _outerScope);
 }
 
@@ -120,6 +122,31 @@ void ModifierChildMaterializer::visitWeakModifierRef(Common::WeakPtr<Modifier> &
 	// Do nothing
 }
 
+ModifierChildCloner::ModifierChildCloner(Runtime *runtime, const Common::WeakPtr<RuntimeObject> &relinkParent)
+	: _runtime(runtime), _relinkParent(relinkParent) {
+}
+
+void ModifierChildCloner::visitChildStructuralRef(Common::SharedPtr<Structural> &structural) {
+	assert(false);
+}
+
+void ModifierChildCloner::visitChildModifierRef(Common::SharedPtr<Modifier> &modifier) {
+	modifier = modifier->shallowClone();
+	modifier->setSelfReference(modifier);
+	modifier->setParent(_relinkParent);
+
+	ModifierChildCloner recursiveCloner(_runtime, modifier);
+	modifier->visitInternalReferences(&recursiveCloner);
+}
+
+void ModifierChildCloner::visitWeakStructuralRef(Common::WeakPtr<Structural> &structural) {
+	// Do nothing
+}
+
+void ModifierChildCloner::visitWeakModifierRef(Common::WeakPtr<Modifier> &modifier) {
+	// Do nothing
+}
+
 char invariantToLower(char c) {
 	if (c >= 'A' && c <= 'Z')
 		return static_cast<char>(c - 'A' + 'a');
@@ -1679,23 +1706,10 @@ void Structural::materializeDescendents(Runtime *runtime, ObjectLinkingScope *ou
 	modifierScope->setParent(outerScope);
 
 	for (Common::Array<Common::SharedPtr<Modifier> >::iterator it = _modifiers.begin(), itEnd = _modifiers.end(); it != itEnd; ++it) {
-		Modifier *modifier = it->get();
-		uint32 modifierGUID = modifier->getStaticGUID();
-		if (modifier->isAlias()) {
-			Common::SharedPtr<Modifier> templateModifier = runtime->getProject()->resolveAlias(static_cast<AliasModifier *>(modifier)->getAliasID());
-			if (!templateModifier) {
-				error("Failed to resolve alias");
-			}
+		Common::SharedPtr<Modifier> &modifierRef = *it;
 
-			if (!modifier->isVariable()) {
-				Common::SharedPtr<Modifier> clonedModifier = templateModifier->shallowClone();
-				clonedModifier->setName(modifier->getName());
-
-				(*it) = clonedModifier;
-				modifier = clonedModifier.get();
-			}
-		}
-		modifierScope->addObject(modifierGUID, modifier->getName(), *it);
+		runtime->instantiateIfAlias(modifierRef, getSelfReference());
+		modifierScope->addObject(modifierRef->getStaticGUID(), modifierRef->getName(), modifierRef);
 	}
 
 	for (Common::Array<Common::SharedPtr<Modifier> >::const_iterator it = _modifiers.begin(), itEnd = _modifiers.end(); it != itEnd; ++it) {
@@ -2655,6 +2669,32 @@ void Runtime::getScenesInRenderOrder(Common::Array<Structural*> &scenes) const {
 	}
 }
 
+void Runtime::instantiateIfAlias(Common::SharedPtr<Modifier> &modifier, const Common::WeakPtr<RuntimeObject> &relinkParent) {
+	if (modifier->isAlias()) {
+		Common::SharedPtr<Modifier> templateModifier = _project->resolveAlias(static_cast<AliasModifier *>(modifier.get())->getAliasID());
+		if (templateModifier->getStaticGUID() == 0x34130c) {
+			int n = 0;
+		}
+		if (!templateModifier) {
+			error("Failed to resolve alias");
+		}
+
+		if (!modifier->isVariable()) {
+			Common::SharedPtr<Modifier> clonedModifier = templateModifier->shallowClone();
+			clonedModifier->setSelfReference(clonedModifier);
+			clonedModifier->setRuntimeGUID(allocateRuntimeGUID());
+
+			clonedModifier->setName(modifier->getName());
+
+			modifier = clonedModifier;
+			clonedModifier->setParent(relinkParent);
+
+			ModifierChildCloner cloner(this, clonedModifier);
+			clonedModifier->visitInternalReferences(&cloner);
+		}
+	}
+}
+
 
 void Runtime::ensureMainWindowExists() {
 	// Maybe there's a better spot for this
@@ -3789,7 +3829,7 @@ void Modifier::materialize(Runtime *runtime, ObjectLinkingScope *outerScope) {
 	ModifierInnerScopeBuilder innerScopeBuilder(&innerScope);
 	this->visitInternalReferences(&innerScopeBuilder);
 
-	ModifierChildMaterializer childMaterializer(runtime, &innerScope);
+	ModifierChildMaterializer childMaterializer(runtime, this, &innerScope);
 	this->visitInternalReferences(&childMaterializer);
 
 	linkInternalReferences(outerScope);
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 63f28259a8b..0e12d1ea5e2 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -1062,6 +1062,8 @@ public:
 
 	void getScenesInRenderOrder(Common::Array<Structural *> &scenes) const;
 
+	void instantiateIfAlias(Common::SharedPtr<Modifier> &modifier, const Common::WeakPtr<RuntimeObject> &relinkParent);
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	void debugSetEnabled(bool enabled);
 	void debugBreak();


Commit: a1940c352a65c692f7b6eb5615531cbcff322766
    https://github.com/scummvm/scummvm/commit/a1940c352a65c692f7b6eb5615531cbcff322766
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add at last cel signal

Changed paths:
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/modifiers.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 6913874221b..da4a62a969e 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -52,13 +52,15 @@ void GraphicElement::render(Window *window) {
 }
 
 MovieElement::MovieElement()
-	: _cacheBitmap(false), _paused(false)
+	: _cacheBitmap(false), _paused(false), _reversed(false), _haveFiredAtFirstCel(false), _haveFiredAtLastCel(false)
 	, _loop(false), _alternate(false), _playEveryFrame(false), _assetID(0), _runtime(nullptr) {
 }
 
 MovieElement::~MovieElement() {
 	if (_unloadSignaller)
 		_unloadSignaller->removeReceiver(this);
+	if (_postRenderSignaller)
+		_postRenderSignaller->removeReceiver(this);
 }
 
 bool MovieElement::load(ElementLoaderContext &context, const Data::MovieElement &data) {
@@ -147,6 +149,7 @@ void MovieElement::activate() {
 		_videoDecoder.reset();
 
 	_unloadSignaller = project->notifyOnSegmentUnload(segmentIndex, this);
+	_postRenderSignaller = project->notifyOnPostRender(this);
 }
 
 void MovieElement::deactivate() {
@@ -154,6 +157,10 @@ void MovieElement::deactivate() {
 		_unloadSignaller->removeReceiver(this);
 		_unloadSignaller.reset();
 	}
+	if (_postRenderSignaller) {
+		_postRenderSignaller->removeReceiver(this);
+		_postRenderSignaller.reset();
+	}
 
 	_videoDecoder.reset();
 }
@@ -174,6 +181,26 @@ void MovieElement::render(Window *window) {
 	}
 }
 
+void MovieElement::onPostRender(Runtime *runtime, Project *project) {
+	if (_videoDecoder) {
+		if (_videoDecoder->isPlaying() && _videoDecoder->endOfVideo()) {
+			if (_alternate) {
+				_reversed = !_reversed;
+				if (!_videoDecoder->setReverse(_reversed)) {
+					warning("Failed to reverse video decoder, disabling it");
+					_videoDecoder.reset();
+				}
+			} else {
+				_videoDecoder->stop();
+			}
+
+			Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(_reversed ? EventIDs::kAtFirstCel : EventIDs::kAtLastCel, 0), DynamicValue(), getSelfReference()));
+			Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+			runtime->queueMessage(dispatch);
+		}
+	}
+}
+
 bool MovieElement::scriptSetPaused(const DynamicValue& dest) {
 	if (dest.getType() == DynamicValueTypes::kBoolean) {
 		_paused = dest.getBool();
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index d45aa122b24..537c489b418 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -53,7 +53,7 @@ private:
 	bool _cacheBitmap;
 };
 
-class MovieElement : public VisualElement, public ISegmentUnloadSignalReceiver {
+class MovieElement : public VisualElement, public ISegmentUnloadSignalReceiver, public IPostRenderSignalReceiver {
 public:
 	MovieElement();
 	~MovieElement();
@@ -68,9 +68,11 @@ public:
 	void deactivate() override;
 
 	void render(Window *window) override;
+	void onPostRender(Runtime *runtime, Project *project) override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Movie Element"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusPartial; }
 #endif
 
 private:
@@ -89,10 +91,14 @@ private:
 	bool _loop;
 	bool _alternate;
 	bool _playEveryFrame;
+	bool _reversed;
+	bool _haveFiredAtLastCel;
+	bool _haveFiredAtFirstCel;
 	uint32 _assetID;
 
 	Common::SharedPtr<Video::VideoDecoder> _videoDecoder;
 	Common::SharedPtr<SegmentUnloadSignaller> _unloadSignaller;
+	Common::SharedPtr<PostRenderSignaller> _postRenderSignaller;
 
 	Runtime *_runtime;
 };
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 4634e2302a7..ce1d6aa457d 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -560,6 +560,7 @@ public:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Compound Variable Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
@@ -586,6 +587,7 @@ public:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Boolean Variable Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
@@ -603,6 +605,7 @@ public:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Integer Variable Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
@@ -620,6 +623,7 @@ public:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Integer Range Variable Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
@@ -637,6 +641,7 @@ public:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Vector Variable Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
@@ -654,6 +659,7 @@ public:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Point Variable Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
@@ -671,6 +677,7 @@ public:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Floating Point Variable Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
@@ -688,6 +695,7 @@ public:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "String Variable Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 0ef89b6b151..1e26df57ede 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -2317,6 +2317,8 @@ void Runtime::drawFrame() {
 	}
 
 	_system->updateScreen();
+
+	_project->onPostRender();
 }
 
 Common::SharedPtr<Structural> Runtime::findDefaultSharedSceneForScene(Structural *scene) {
@@ -2903,6 +2905,32 @@ const IPlugInModifierFactory *ProjectPlugInRegistry::findPlugInModifierFactory(c
 	return it->_value;
 }
 
+PostRenderSignaller::PostRenderSignaller() {
+}
+
+PostRenderSignaller::~PostRenderSignaller() {
+}
+
+void PostRenderSignaller::onPostRender(Runtime *runtime, Project *project) {
+	const size_t numReceivers = _receivers.size();
+	for (size_t i = 0; i < numReceivers; i++) {
+		_receivers[i]->onPostRender(runtime, project);
+	}
+}
+
+void PostRenderSignaller::addReceiver(IPostRenderSignalReceiver *receiver) {
+	_receivers.push_back(receiver);
+}
+
+void PostRenderSignaller::removeReceiver(IPostRenderSignalReceiver* receiver) {
+	for (size_t i = 0; i < _receivers.size(); i++) {
+		if (_receivers[i] == receiver) {
+			_receivers.remove_at(i);
+			break;
+		}
+	}
+}
+
 
 SegmentUnloadSignaller::SegmentUnloadSignaller(Project *project, int segmentIndex) : _project(project), _segmentIndex(segmentIndex) {
 }
@@ -2937,7 +2965,8 @@ Project::Segment::Segment() : weakStream(nullptr) {
 }
 
 Project::Project(Runtime *runtime)
-	: _runtime(runtime), _projectFormat(Data::kProjectFormatUnknown), _isBigEndian(false), _haveGlobalObjectInfo(false), _haveProjectStructuralDef(false) {
+	: _runtime(runtime), _projectFormat(Data::kProjectFormatUnknown), _isBigEndian(false),
+	  _haveGlobalObjectInfo(false), _haveProjectStructuralDef(false), _postRenderSignaller(new PostRenderSignaller()) {
 }
 
 Project::~Project() {
@@ -3217,6 +3246,15 @@ Common::SharedPtr<SegmentUnloadSignaller> Project::notifyOnSegmentUnload(int seg
 	return signaller;
 }
 
+void Project::onPostRender() {
+	_postRenderSignaller->onPostRender(_runtime, this);
+}
+
+Common::SharedPtr<PostRenderSignaller> Project::notifyOnPostRender(IPostRenderSignalReceiver *receiver) {
+	_postRenderSignaller->addReceiver(receiver);
+	return _postRenderSignaller;
+}
+
 void Project::loadBootStream(size_t streamIndex) {
 	const StreamDesc &streamDesc = _streams[streamIndex];
 
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 0e12d1ea5e2..4662eb75347 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -1378,6 +1378,23 @@ private:
 	Common::HashMap<Common::String, const IPlugInModifierFactory *> _factoryRegistry;
 };
 
+struct IPostRenderSignalReceiver {
+	virtual void onPostRender(Runtime *runtime, Project *project) = 0;
+};
+
+class PostRenderSignaller {
+public:
+	PostRenderSignaller();
+	~PostRenderSignaller();
+
+	void onPostRender(Runtime *runtime, Project *project);
+	void addReceiver(IPostRenderSignalReceiver *receiver);
+	void removeReceiver(IPostRenderSignalReceiver *receiver);
+
+private:
+	Common::Array<IPostRenderSignalReceiver *> _receivers;
+};
+
 struct ISegmentUnloadSignalReceiver {
 	virtual void onSegmentUnloaded(int segmentIndex) = 0;
 };
@@ -1419,6 +1436,9 @@ public:
 	Common::SeekableReadStream *getStreamForSegment(int segmentIndex);
 	Common::SharedPtr<SegmentUnloadSignaller> notifyOnSegmentUnload(int segmentIndex, ISegmentUnloadSignalReceiver *receiver);
 
+	void onPostRender();
+	Common::SharedPtr<PostRenderSignaller> notifyOnPostRender(IPostRenderSignalReceiver *receiver);
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Project"; }
 #endif
@@ -1515,6 +1535,8 @@ private:
 	ObjectLinkingScope _structuralScope;
 	ObjectLinkingScope _modifierScope;
 
+	Common::SharedPtr<PostRenderSignaller> _postRenderSignaller;
+
 	Runtime *_runtime;
 };
 


Commit: c5434a80dcc37e9cf48192db3e8a4d7fef5133f6
    https://github.com/scummvm/scummvm/commit/c5434a80dcc37e9cf48192db3e8a4d7fef5133f6
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add image assets and elements

Changed paths:
    engines/mtropolis/asset_factory.cpp
    engines/mtropolis/assets.cpp
    engines/mtropolis/assets.h
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/element_factory.cpp
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/asset_factory.cpp b/engines/mtropolis/asset_factory.cpp
index db796ac26b1..3b771a57103 100644
--- a/engines/mtropolis/asset_factory.cpp
+++ b/engines/mtropolis/asset_factory.cpp
@@ -63,6 +63,8 @@ IAssetFactory *getAssetFactoryForDataObjectType(const Data::DataObjectTypes::Dat
 		return AssetFactory<AudioAsset, Data::AudioAsset>::getInstance();
 	case Data::DataObjectTypes::kMovieAsset:
 		return AssetFactory<MovieAsset, Data::MovieAsset>::getInstance();
+	case Data::DataObjectTypes::kImageAsset:
+		return AssetFactory<ImageAsset, Data::ImageAsset>::getInstance();
 
 	default:
 		return nullptr;
diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index b8ffb0a6d6d..f56f3344634 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -122,5 +122,74 @@ size_t MovieAsset::getStreamIndex() const {
 	return _streamIndex;
 }
 
+bool ImageAsset::load(AssetLoaderContext &context, const Data::ImageAsset &data) {
+	_assetID = data.assetID;
+	if (!_rect.load(data.rect1))
+		return false;
+	_filePosition = data.filePosition;
+	_size = data.size;
+	_streamIndex = context.streamIndex;
+
+	switch (data.bitsPerPixel) {
+	case 1:
+		_colorDepth = kColorDepthMode1Bit;
+		break;
+	case 2:
+		_colorDepth = kColorDepthMode2Bit;
+		break;
+	case 4:
+		_colorDepth = kColorDepthMode4Bit;
+		break;
+	case 8:
+		_colorDepth = kColorDepthMode8Bit;
+		break;
+	case 16:
+		_colorDepth = kColorDepthMode16Bit;
+		break;
+	case 32:
+		_colorDepth = kColorDepthMode32Bit;
+		break;
+	default:
+		return false;
+	}
+
+	if (data.haveMacPart)
+		_imageFormat = kImageFormatMac;
+	else if (data.haveWinPart)
+		_imageFormat = kImageFormatWindows;
+	else
+		return false;
+
+	return true;
+}
+
+AssetType ImageAsset::getAssetType() const {
+	return kAssetTypeImage;
+}
+
+const Rect16& ImageAsset::getRect() const {
+	return _rect;
+}
+
+ColorDepthMode ImageAsset::getColorDepth() const {
+	return _colorDepth;
+}
+
+uint32 ImageAsset::getFilePosition() const {
+	return _filePosition;
+}
+
+uint32 ImageAsset::getSize() const {
+	return _size;
+}
+
+size_t ImageAsset::getStreamIndex() const {
+	return _streamIndex;
+}
+
+ImageAsset::ImageFormat ImageAsset::getImageFormat() const {
+	return _imageFormat;
+}
+
 
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/assets.h b/engines/mtropolis/assets.h
index ee34993334f..ebfe952b874 100644
--- a/engines/mtropolis/assets.h
+++ b/engines/mtropolis/assets.h
@@ -87,6 +87,32 @@ private:
 	size_t _streamIndex;
 };
 
+class ImageAsset : public Asset {
+public:
+	bool load(AssetLoaderContext &context, const Data::ImageAsset &data);
+	AssetType getAssetType() const override;
+
+	enum ImageFormat {
+		kImageFormatMac,
+		kImageFormatWindows,
+	};
+
+	const Rect16 &getRect() const;
+	ColorDepthMode getColorDepth() const;
+	uint32 getFilePosition() const;
+	uint32 getSize() const;
+	size_t getStreamIndex() const;
+	ImageFormat getImageFormat() const;
+
+private:
+	Rect16 _rect;
+	ColorDepthMode _colorDepth;
+	uint32 _filePosition;
+	uint32 _size;
+	size_t _streamIndex;
+	ImageFormat _imageFormat;
+};
+
 } // End of namespace MTropolis
 
 #endif
diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index abdf2d24dd6..bf780e095b3 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -1544,6 +1544,36 @@ DataReadErrorCode AudioAsset::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode ImageAsset::load(DataReader &reader) {
+	if (_revision != 1)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(persistFlags) || !reader.readU32(unknown1) || !reader.readBytes(unknown2)
+		|| !reader.readU32(assetID) || !reader.readU32(unknown3))
+		return kDataReadErrorReadFailed;
+
+	haveWinPart = false;
+	haveMacPart = false;
+
+	if (reader.getProjectFormat() == kProjectFormatMacintosh) {
+		haveMacPart = true;
+		if (!reader.readBytes(platform.mac.unknown7))
+			return kDataReadErrorReadFailed;
+	} else if (reader.getProjectFormat() == kProjectFormatWindows) {
+		haveWinPart = true;
+		if (!reader.readBytes(platform.win.unknown8))
+			return kDataReadErrorReadFailed;
+	} else
+		return kDataReadErrorUnrecognized;
+
+	if (!rect1.load(reader) || !reader.readU32(hdpiFixed) || !reader.readU32(vdpiFixed) || !reader.readU16(bitsPerPixel)
+		|| !reader.readBytes(unknown4) || !reader.readBytes(unknown5) || !reader.readBytes(unknown6)
+		|| !rect2.load(reader) || !reader.readU32(filePosition) || !reader.readU32(size))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode AssetDataChunk::load(DataReader &reader) {
 	if (_revision != 0)
 		return kDataReadErrorUnsupportedRevision;
@@ -1728,7 +1758,7 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 		break;
 
 	case DataObjectTypes::kImageAsset:
-		//dataObject = new ImageAsset();
+		dataObject = new ImageAsset();
 		break;
 
 	case DataObjectTypes::kMToonAsset:
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index 1e4db88e8ca..47f5621b57f 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -1412,6 +1412,45 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct ImageAsset : public DataObject {
+	struct MacPart {
+		uint8 unknown7[44];
+	};
+
+	struct WinPart {
+		uint8 unknown8[10];
+	};
+
+	union PlatformPart {
+		WinPart win;
+		MacPart mac;
+	};
+
+	uint32 persistFlags;
+	uint32 unknown1;
+	uint8_t unknown2[4];
+	uint32 assetID;
+	uint32 unknown3;
+
+	Rect rect1;
+	uint32 hdpiFixed;
+	uint32 vdpiFixed;
+	uint16 bitsPerPixel;
+	uint8 unknown4[2];
+	uint8 unknown5[4];
+	uint8 unknown6[8];
+	Rect rect2;
+	uint32 filePosition;
+	uint32 size;
+
+	bool haveMacPart;
+	bool haveWinPart;
+	PlatformPart platform;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 struct AssetDataChunk : public DataObject {
 	uint32 unknown1;
 	uint32 sizeIncludingTag;
diff --git a/engines/mtropolis/element_factory.cpp b/engines/mtropolis/element_factory.cpp
index 217468ee3f3..6257e859103 100644
--- a/engines/mtropolis/element_factory.cpp
+++ b/engines/mtropolis/element_factory.cpp
@@ -64,6 +64,8 @@ IElementFactory *getElementFactoryForDataObjectType(const Data::DataObjectTypes:
 		return ElementFactory<GraphicElement, Data::GraphicElement>::getInstance();
 	case Data::DataObjectTypes::kMovieElement:
 		return ElementFactory<MovieElement, Data::MovieElement>::getInstance();
+	case Data::DataObjectTypes::kImageElement:
+		return ElementFactory<ImageElement, Data::ImageElement>::getInstance();
 
 	default:
 		return nullptr;
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index da4a62a969e..9ec0e17c820 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -137,7 +137,6 @@ void MovieElement::activate() {
 		return;
 	}
 
-
 	Video::QuickTimeDecoder *qtDecoder = new Video::QuickTimeDecoder();
 	qtDecoder->setChunkBeginOffset(movieAsset->getMovieDataPos());
 
@@ -231,4 +230,197 @@ VThreadState MovieElement::startPlayingTask(const StartPlayingTaskData &taskData
 	return kVThreadReturn;
 }
 
+
+ImageElement::ImageElement() : _cacheBitmap(false), _runtime(nullptr) {
+}
+
+ImageElement::~ImageElement() {
+}
+
+bool ImageElement::load(ElementLoaderContext &context, const Data::ImageElement &data) {
+	if (!VisualElement::loadCommon(data.name, data.guid, data.rect1, data.elementFlags, data.layer, data.streamLocator, data.sectionID))
+		return false;
+
+	_cacheBitmap = ((data.elementFlags & Data::ElementFlags::kCacheBitmap) != 0);
+	_runtime = context.runtime;
+	_assetID = data.imageAssetID;
+
+	return true;
+}
+
+bool ImageElement::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	return VisualElement::readAttribute(thread, result, attrib);
+}
+
+bool ImageElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
+	return VisualElement::writeRefAttribute(thread, writeProxy, attrib);
+}
+
+void ImageElement::activate() {
+	Project *project = _runtime->getProject();
+	Common::SharedPtr<Asset> asset = project->getAssetByID(_assetID).lock();
+
+	if (!asset) {
+		warning("Image element references asset %i but the asset isn't loaded!", _assetID);
+		return;
+	}
+
+	if (asset->getAssetType() != kAssetTypeImage) {
+		warning("Image element assigned an asset that isn't an image");
+		return;
+	}
+
+	ImageAsset *imageAsset = static_cast<ImageAsset *>(asset.get());
+	size_t streamIndex = imageAsset->getStreamIndex();
+	int segmentIndex = project->getSegmentForStreamIndex(streamIndex);
+	project->openSegmentStream(segmentIndex);
+	Common::SeekableReadStream *stream = project->getStreamForSegment(segmentIndex);
+
+	if (!stream->seek(imageAsset->getFilePosition())) {
+		warning("Image element failed to load");
+		return;
+	}
+
+	size_t bytesPerRow = 0;
+
+	Rect16 imageRect = imageAsset->getRect();
+	int width = imageRect.right - imageRect.left;
+	int height = imageRect.bottom - imageRect.top;
+
+	if (width <= 0 || height < 0) {
+		warning("Image asset has invalid size");
+		return;
+	}
+
+	Graphics::PixelFormat pixelFmt;
+	switch (imageAsset->getColorDepth()) {
+	case kColorDepthMode1Bit:
+		bytesPerRow = (width + 7) / 8;
+		pixelFmt = Graphics::PixelFormat::createFormatCLUT8();
+		break;
+	case kColorDepthMode2Bit:
+		bytesPerRow = (width + 3) / 4;
+		pixelFmt = Graphics::PixelFormat::createFormatCLUT8();
+		break;
+	case kColorDepthMode4Bit:
+		bytesPerRow = (width + 1) / 2;
+		pixelFmt = Graphics::PixelFormat::createFormatCLUT8();
+		break;
+	case kColorDepthMode8Bit:
+		bytesPerRow = width;
+		pixelFmt = Graphics::PixelFormat::createFormatCLUT8();
+		break;
+	case kColorDepthMode16Bit:
+		bytesPerRow = width * 2;
+		pixelFmt = Graphics::createPixelFormat<1555>();
+		break;
+	case kColorDepthMode32Bit:
+		bytesPerRow = width * 4;
+		pixelFmt = Graphics::createPixelFormat<8888>();
+		break;
+	default:
+		warning("Image asset has an unrecognizable pixel format");
+		return;
+	}
+
+	Common::Array<uint8> rowBuffer;
+	rowBuffer.resize(bytesPerRow);
+
+	ImageAsset::ImageFormat imageFormat = imageAsset->getImageFormat();
+	bool bottomUp = (imageFormat == ImageAsset::kImageFormatWindows);
+	bool isBigEndian = (imageFormat == ImageAsset::kImageFormatMac);
+
+	_imageSurface.reset(new Graphics::Surface());
+	_imageSurface->create(width, height, pixelFmt);
+
+	for (int inRow = 0; inRow < height; inRow++) {
+		int outRow = bottomUp ? (height - 1 - inRow) : inRow;
+
+		stream->read(&rowBuffer[0], bytesPerRow);
+		const uint8 *inRowBytes = &rowBuffer[0];
+
+		void *outBase = _imageSurface->getBasePtr(0, outRow);
+
+		switch (imageAsset->getColorDepth()) {
+		case kColorDepthMode1Bit: {
+				for (int x = 0; x < width; x++) {
+					int bit = (inRowBytes[x / 8] >> (7 - (x % 8))) & 1;
+					static_cast<uint8 *>(outBase)[x] = bit;
+				}
+			} break;
+		case kColorDepthMode2Bit: {
+				for (int x = 0; x < width; x++) {
+					int bit = (inRowBytes[x / 4] >> (3 - (x % 4))) & 3;
+					static_cast<uint8 *>(outBase)[x] = bit;
+				}
+			} break;
+		case kColorDepthMode4Bit: {
+				for (int x = 0; x < width; x++) {
+					int bit = (inRowBytes[x / 2] >> (1 - (x % 2))) & 15;
+					static_cast<uint8 *>(outBase)[x] = bit;
+				}
+			} break;
+		case kColorDepthMode8Bit:
+			memcpy(outBase, inRowBytes, width);
+			break;
+		case kColorDepthMode16Bit: {
+				if (isBigEndian) {
+					for (int x = 0; x < width; x++) {
+						uint16 packedPixel = inRowBytes[x * 2 + 1] + (inRowBytes[x * 2 + 0] << 8);
+						int r = ((packedPixel >> 10) & 0x1f);
+						int g = ((packedPixel >> 5) & 0x1f);
+						int b = (packedPixel & 0x1f);
+
+						uint16 repacked = (1 << pixelFmt.aShift) | (r << pixelFmt.rShift) | (g << pixelFmt.gShift) | (b << pixelFmt.bShift);
+						static_cast<uint16 *>(outBase)[x] = repacked;
+					}
+				} else {
+					for (int x = 0; x < width; x++) {
+						uint16 packedPixel = inRowBytes[x * 2 + 0] + (inRowBytes[x * 2 + 1] << 8);
+						int r = ((packedPixel >> 10) & 0x1f);
+						int g = ((packedPixel >> 5) & 0x1f);
+						int b = (packedPixel & 0x1f);
+
+						uint16 repacked = (1 << pixelFmt.aShift) | (r << pixelFmt.rShift) | (g << pixelFmt.gShift) | (b << pixelFmt.bShift);
+						static_cast<uint16 *>(outBase)[x] = repacked;
+					}
+				}
+			} break;
+		case kColorDepthMode32Bit: {
+				if (imageFormat == ImageAsset::kImageFormatMac) {
+					for (int x = 0; x < width; x++) {
+						uint8 r = inRowBytes[x * 4 + 0];
+						uint8 g = inRowBytes[x * 4 + 1];
+						uint8 b = inRowBytes[x * 4 + 2];
+						uint32 repacked = (255 << pixelFmt.aShift) | (r << pixelFmt.rShift) | (g << pixelFmt.gShift) | (b << pixelFmt.bShift);
+						static_cast<uint32 *>(outBase)[x] = repacked;
+					}
+				} else if (imageFormat == ImageAsset::kImageFormatWindows) {
+					for (int x = 0; x < width; x++) {
+						uint8 r = inRowBytes[x * 4 + 2];
+						uint8 g = inRowBytes[x * 4 + 1];
+						uint8 b = inRowBytes[x * 4 + 0];
+						uint32 repacked = (255 << pixelFmt.aShift) | (r << pixelFmt.rShift) | (g << pixelFmt.gShift) | (b << pixelFmt.bShift);
+						static_cast<uint32 *>(outBase)[x] = repacked;
+					}
+				}
+			} break;
+		default:
+			break;
+		}
+	}
+}
+
+void ImageElement::deactivate() {
+	_imageSurface.reset();
+}
+
+void ImageElement::render(Window *window) {
+	if (_imageSurface) {
+		Common::Rect srcRect(_imageSurface->w, _imageSurface->h);
+		Common::Rect destRect(_rect.left, _rect.top, _rect.right, _rect.bottom);
+		window->getSurface()->blitFrom(*_imageSurface, srcRect, destRect);
+	}
+}
+
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index 537c489b418..5de7aa8045e 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -60,8 +60,8 @@ public:
 
 	bool load(ElementLoaderContext &context, const Data::MovieElement &data);
 
-	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
-	bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
+	bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) override;
 	VThreadState consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
 
 	void activate() override;
@@ -103,6 +103,35 @@ private:
 	Runtime *_runtime;
 };
 
+class ImageElement : public VisualElement {
+public:
+	ImageElement();
+	~ImageElement();
+
+	bool load(ElementLoaderContext &context, const Data::ImageElement &data);
+
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
+	bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
+
+	void activate() override;
+	void deactivate() override;
+
+	void render(Window *window) override;
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Image Element"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusPartial; }
+#endif
+
+private:
+	bool _cacheBitmap;
+	uint32 _assetID;
+
+	Common::SharedPtr<Graphics::Surface> _imageSurface;
+
+	Runtime *_runtime;
+};
+
 } // End of namespace MTropolis
 
 #endif
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 1e26df57ede..cbafe46988c 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -2787,7 +2787,7 @@ void Runtime::removeWindow(Window *window) {
 	}
 }
 
-void Runtime::setupDisplayMode(ColorDepthMode displayMode, const Graphics::PixelFormat& pixelFormat) {
+void Runtime::setupDisplayMode(ColorDepthMode displayMode, const Graphics::PixelFormat &pixelFormat) {
 	_displayModeSupported[displayMode] = true;
 	_displayModePixelFormats[displayMode] = pixelFormat;
 }
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 4662eb75347..a13dc5f928d 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -1713,6 +1713,7 @@ enum AssetType {
 	kAssetTypeMovie,
 	kAssetTypeAudio,
 	kAssetTypeColorTable,
+	kAssetTypeImage,
 };
 
 class Asset {


Commit: f5887475a9d4f432503aa2e3b2aaeb80c3c6fed3
    https://github.com/scummvm/scummvm/commit/f5887475a9d4f432503aa2e3b2aaeb80c3c6fed3
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add text and sound skeletal defs to get to first scene

Changed paths:
  R engines/mtropolis/alignment_helper.h
    engines/mtropolis/asset_factory.cpp
    engines/mtropolis/assets.cpp
    engines/mtropolis/assets.h
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/debug.h
    engines/mtropolis/element_factory.cpp
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/notes.txt
    engines/mtropolis/render.cpp
    engines/mtropolis/render.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h
    engines/mtropolis/vthread.cpp
    engines/mtropolis/vthread.h


diff --git a/engines/mtropolis/alignment_helper.h b/engines/mtropolis/alignment_helper.h
deleted file mode 100644
index bc46ac872e6..00000000000
--- a/engines/mtropolis/alignment_helper.h
+++ /dev/null
@@ -1,44 +0,0 @@
-/* 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 <http://www.gnu.org/licenses/>.
- *
- */
-
-#ifndef MTROPOLIS_ALIGNMENT_HELPER_H
-#define MTROPOLIS_ALIGNMENT_HELPER_H
-
-#include <cstddef>
-
-namespace MTropolis {
-
-template<typename TClass>
-struct AlignmentHelper {
-	char prefix;
-	TClass item;
-
-	static size_t getAlignment();
-};
-
-template<typename TClass>
-inline size_t AlignmentHelper<TClass>::getAlignment() {
-	return offsetof(AlignmentHelper<TClass>, item);
-}
-
-} // End of namespace MTropolis
-
-#endif
diff --git a/engines/mtropolis/asset_factory.cpp b/engines/mtropolis/asset_factory.cpp
index 3b771a57103..95b3e9c8655 100644
--- a/engines/mtropolis/asset_factory.cpp
+++ b/engines/mtropolis/asset_factory.cpp
@@ -65,6 +65,8 @@ IAssetFactory *getAssetFactoryForDataObjectType(const Data::DataObjectTypes::Dat
 		return AssetFactory<MovieAsset, Data::MovieAsset>::getInstance();
 	case Data::DataObjectTypes::kImageAsset:
 		return AssetFactory<ImageAsset, Data::ImageAsset>::getInstance();
+	case Data::DataObjectTypes::kTextAsset:
+		return AssetFactory<TextAsset, Data::TextAsset>::getInstance();
 
 	default:
 		return nullptr;
diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index f56f3344634..4fbb4e02695 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -22,6 +22,8 @@
 #include "mtropolis/assets.h"
 #include "mtropolis/asset_factory.h"
 
+#include "graphics/surface.h"
+
 namespace MTropolis {
 
 Asset::Asset() : _assetID(0) {
@@ -49,6 +51,7 @@ AssetType ColorTableAsset::getAssetType() const {
 }
 
 bool AudioAsset::load(AssetLoaderContext &context, const Data::AudioAsset &data) {
+	_assetID = data.assetID;
 	_sampleRate = data.sampleRate1;
 	_bitsPerSample = data.bitsPerSample;
 
@@ -191,5 +194,90 @@ ImageAsset::ImageFormat ImageAsset::getImageFormat() const {
 	return _imageFormat;
 }
 
+bool TextAsset::load(AssetLoaderContext &context, const Data::TextAsset &data) {
+	_assetID = data.assetID;
+
+	switch (data.alignment) {
+	case Data::kTextAlignmentCodeLeft:
+		_alignment = kTextAlignmentLeft;
+		break;
+	case Data::kTextAlignmentCodeRight:
+		_alignment = kTextAlignmentRight;
+		break;
+	case Data::kTextAlignmentCodeCenter:
+		_alignment = kTextAlignmentCenter;
+		break;
+	default:
+		return false;
+	};
+
+	_isBitmap = ((data.isBitmap & 1) != 0);
+
+	if (_isBitmap) {
+		if (!_bitmapRect.load(data.bitmapRect))
+			return false;
+
+		_bitmapData.reset(new Graphics::Surface());
+		uint16 pitch = (data.pitchBigEndian[0] << 8) + data.pitchBigEndian[1];
+		uint16 width = _bitmapRect.getWidth();
+		uint16 height = _bitmapRect.getHeight();
+		if (static_cast<uint32>(pitch * width) != data.bitmapSize) {
+			// Pitch is normally aligned to 4 bytes, so if this fails, maybe compute it that way?
+			warning("Pre-rendered text bitmap pitch didn't compute to bitmap size correctly, maybe it's wrong?");
+			return false;
+		}
+
+		if (pitch * 8 < width) {
+			warning("Pre-rendered text pitch is too small");
+			return false;
+		}
+
+		_bitmapData->create(width, height, Graphics::PixelFormat::createFormatCLUT8());
+
+		for (int row = 0; row < height; row++) {
+			uint8 *outRow = static_cast<uint8 *>(_bitmapData->getBasePtr(0, row));
+			const uint8 *inRow = &data.bitmapData[row * pitch];
+			for (int col = 0; col < width; col++) {
+				int bit = ((inRow[col / 8] >> (7 - (col & 7))) & 1);
+				outRow[col] = bit;
+			}
+		}
+	} else {
+		_bitmapRect = Rect16::create(0, 0, 0, 0);
+
+		_stringData = data.text;
+
+		for (size_t i = 0; i < data.macFormattingSpans.size(); i++) {
+			const Data::TextAsset::MacFormattingSpan &inSpan = data.macFormattingSpans[i];
+			MacFormattingSpan fmtSpan;
+			fmtSpan.formatting = MacFontFormatting(inSpan.fontID, inSpan.fontFlags, inSpan.size);
+			fmtSpan.spanStart = inSpan.spanStart;
+
+			_macFormattingSpans.push_back(fmtSpan);
+		}
+	}
+
+	return true;
+}
+
+AssetType TextAsset::getAssetType() const {
+	return kAssetTypeText;
+}
+
+bool TextAsset::isBitmap() const {
+	return _isBitmap;
+}
+
+const Common::SharedPtr<Graphics::Surface>& TextAsset::getBitmapSurface() const {
+	return _bitmapData;
+}
+
+const Common::String& TextAsset::getString() const {
+	return _stringData;
+}
+
+const Common::Array<MacFormattingSpan> &TextAsset::getMacFormattingSpans() const {
+	return _macFormattingSpans;
+}
 
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/assets.h b/engines/mtropolis/assets.h
index ebfe952b874..0ed30d2aff5 100644
--- a/engines/mtropolis/assets.h
+++ b/engines/mtropolis/assets.h
@@ -24,6 +24,7 @@
 
 #include "mtropolis/data.h"
 #include "mtropolis/runtime.h"
+#include "mtropolis/render.h"
 
 namespace MTropolis {
 
@@ -113,6 +114,27 @@ private:
 	ImageFormat _imageFormat;
 };
 
+class TextAsset : public Asset {
+public:
+	bool load(AssetLoaderContext &context, const Data::TextAsset &data);
+	AssetType getAssetType() const override;
+
+	bool isBitmap() const;
+	const Common::SharedPtr<Graphics::Surface> &getBitmapSurface() const;
+	const Common::String &getString() const;
+	const Common::Array<MacFormattingSpan> &getMacFormattingSpans() const;
+
+private:
+	Rect16 _bitmapRect;
+	TextAlignment _alignment;
+	bool _isBitmap;
+
+	Common::SharedPtr<Graphics::Surface> _bitmapData;
+	Common::String _stringData;
+
+	Common::Array<MacFormattingSpan> _macFormattingSpans;
+};
+
 } // End of namespace MTropolis
 
 #endif
diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index bf780e095b3..8fd33eb4c2f 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -23,8 +23,6 @@
 #include "common/debug.h"
 #include "common/memstream.h"
 
-#include <float.h>
-
 namespace MTropolis {
 
 namespace Data {
@@ -142,6 +140,7 @@ bool isAsset(DataObjectType type) {
 	case kColorTableAsset:
 	case kImageAsset:
 	case kMToonAsset:
+	case kTextAsset:
 		return true;
 	default:
 		return false;
@@ -775,18 +774,74 @@ DataReadErrorCode ImageElement::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode TextLabelElement::load(DataReader &reader) {
+	if (reader.getProjectFormat() == kProjectFormatMacintosh) {
+		if (_revision != 2)
+			return kDataReadErrorUnsupportedRevision;
+	} else if (reader.getProjectFormat() == kProjectFormatWindows) {
+		if (_revision != 0)
+			return kDataReadErrorUnsupportedRevision;
+	} else
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(structuralFlags) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid)
+		|| !reader.readU16(lengthOfName) || !reader.readU32(elementFlags) || !reader.readU16(layer)
+		|| !reader.readU16(sectionID))
+		return kDataReadErrorReadFailed;
+
+	haveMacPart = false;
+	haveWinPart = false;
+	if (reader.getProjectFormat() == kProjectFormatWindows) {
+		haveWinPart = true;
+		if (!reader.readBytes(platform.win.unknown3))
+			return kDataReadErrorReadFailed;
+	}
+
+	if (!rect1.load(reader) || !rect2.load(reader) || !reader.readU32(assetID))
+		return kDataReadErrorReadFailed;
+
+	if (reader.getProjectFormat() == kProjectFormatWindows) {
+		if (!reader.readBytes(platform.win.unknown4))
+			return kDataReadErrorReadFailed;
+	} else if (reader.getProjectFormat() == kProjectFormatMacintosh) {
+		haveMacPart = true;
+		if (!reader.readBytes(platform.mac.unknown2))
+			return kDataReadErrorReadFailed;
+	} else
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readTerminatedStr(name, lengthOfName))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode SoundElement::load(DataReader& reader) {
+	if (_revision != 3)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(structuralFlags) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid)
+		|| !reader.readU16(lengthOfName) || !reader.readU32(elementFlags) || !reader.readU32(soundFlags)
+		|| !reader.readU16(unknown2) || !reader.readBytes(unknown3) || !reader.readU16(rightVolume)
+		|| !reader.readU16(leftVolume) || !reader.readS16(balance) || !reader.readU32(assetID)
+		|| !reader.readBytes(unknown5) || !reader.readTerminatedStr(name, lengthOfName))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode MovieElement::load(DataReader &reader) {
 	if (_revision != 2)
 		return kDataReadErrorUnsupportedRevision;
 	
 	if (!reader.readU32(structuralFlags) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid)
 		|| !reader.readU16(lengthOfName) || !reader.readU32(elementFlags) || !reader.readU16(layer)
-			|| !reader.readBytes(unknown3) || !reader.readU16(sectionID) || !reader.readBytes(unknown5)
-			|| !rect1.load(reader) || !rect2.load(reader) || !reader.readU32(assetID)
-			|| !reader.readU32(unknown7) || !reader.readU16(volume) || !reader.readU32(animationFlags)
-			|| !reader.readBytes(unknown10) || !reader.readBytes(unknown11) || !reader.readU32(streamLocator)
-			|| !reader.readBytes(unknown13) || !reader.readTerminatedStr(name, lengthOfName))
-			return kDataReadErrorReadFailed;
+		|| !reader.readBytes(unknown3) || !reader.readU16(sectionID) || !reader.readBytes(unknown5)
+		|| !rect1.load(reader) || !rect2.load(reader) || !reader.readU32(assetID)
+		|| !reader.readU32(unknown7) || !reader.readU16(volume) || !reader.readU32(animationFlags)
+		|| !reader.readBytes(unknown10) || !reader.readBytes(unknown11) || !reader.readU32(streamLocator)
+		|| !reader.readBytes(unknown13) || !reader.readTerminatedStr(name, lengthOfName))
+		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
 }
@@ -1574,6 +1629,60 @@ DataReadErrorCode ImageAsset::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode TextAsset::load(DataReader &reader) {
+	if (_revision != 3)
+		return kDataReadErrorReadFailed;
+
+	if (!reader.readU32(persistFlags) || !reader.readU32(sizeIncludingTag) || !reader.readU32(unknown1)
+		|| !reader.readU32(assetID) || !reader.readU32(unknown2))
+		return kDataReadErrorReadFailed;
+
+	haveMacPart = false;
+	haveWinPart = false;
+	if (reader.getProjectFormat() == kProjectFormatMacintosh) {
+		haveMacPart = true;
+		if (!reader.readBytes(platform.mac.unknown3))
+			return kDataReadErrorReadFailed;
+	} else if (reader.getProjectFormat() == kProjectFormatWindows) {
+		haveWinPart = true;
+		if (!reader.readBytes(platform.win.unknown4))
+			return kDataReadErrorReadFailed;
+	} else
+		return kDataReadErrorUnrecognized;
+
+	if (!bitmapRect.load(reader) || !reader.readU32(hdpi) || !reader.readU32(vdpi) || !reader.readU16(unknown5)
+		|| !reader.readBytes(pitchBigEndian) || !reader.readU32(unknown6) || !reader.readU32(bitmapSize)
+		|| !reader.readBytes(unknown7) || !reader.readU32(textSize) || !reader.readBytes(unknown8)
+		|| !reader.readU16(alignment) || !reader.readU16(isBitmap))
+		return kDataReadErrorReadFailed;
+
+	if ((isBitmap & 1) == 0) {
+		if (!reader.readNonTerminatedStr(text, textSize))
+			return kDataReadErrorReadFailed;
+
+		if (reader.getProjectFormat() == kProjectFormatMacintosh) {
+			uint16_t numFormattingSpans;
+			if (!reader.readU16(numFormattingSpans))
+				return kDataReadErrorReadFailed;
+
+			macFormattingSpans.resize(numFormattingSpans);
+			for (size_t i = 0; i < numFormattingSpans; i++) {
+				MacFormattingSpan &span = macFormattingSpans[i];
+				if (!reader.readBytes(span.unknown9) || !reader.readU16(span.spanStart) || !reader.readBytes(span.unknown10)
+					|| !reader.readU16(span.fontID) || !reader.readU8(span.fontFlags) || !reader.readBytes(span.unknown11)
+					|| !reader.readU16(span.size) || !reader.readBytes(span.unknown12))
+					return kDataReadErrorReadFailed;
+			}
+		}
+	} else {
+		bitmapData.resize(bitmapSize);
+		if (bitmapSize > 0 && !reader.read(&bitmapData[0], bitmapSize))
+			return kDataReadErrorReadFailed;
+	}
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode AssetDataChunk::load(DataReader &reader) {
 	if (_revision != 0)
 		return kDataReadErrorUnsupportedRevision;
@@ -1651,10 +1760,10 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 		dataObject = new ImageElement();
 		break;
 	case DataObjectTypes::kSoundElement:
-		//dataObject = new SoundElement();
+		dataObject = new SoundElement();
 		break;
 	case DataObjectTypes::kTextLabelElement:
-		//dataObject = new TextLabelElement();
+		dataObject = new TextLabelElement();
 		break;
 
 	case DataObjectTypes::kGlobalObjectInfo:
@@ -1765,6 +1874,10 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 		//dataObject = new MToonAsset();
 		break;
 
+	case DataObjectTypes::kTextAsset:
+		dataObject = new TextAsset();
+		break;
+
 	case DataObjectTypes::kAssetDataChunk:
 		dataObject = new AssetDataChunk();
 		break;
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index 47f5621b57f..a42f1e145c8 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -63,6 +63,12 @@ enum ModifierFlags {
 	kModifierFlagLast = 0x2,
 };
 
+enum TextAlignmentCode {
+	kTextAlignmentCodeLeft = 0,
+	kTextAlignmentCodeCenter = 1,
+	kTextAlignmentCodeRight = 0xffff,
+};
+
 
 namespace DataObjectTypes {
 
@@ -83,12 +89,12 @@ enum DataObjectType {
 	kSectionStructuralDef                = 0x3,
 	kSubsectionStructuralDef             = 0x21,
 
-	kGraphicElement                      = 0x8,		// NYI
-	kMovieElement                        = 0x5,		// NYI
+	kGraphicElement                      = 0x8,
+	kMovieElement                        = 0x5,
 	kMToonElement                        = 0x6,		// NYI
-	kImageElement                        = 0x7,		// NYI
-	kSoundElement                        = 0xa,		// NYI
-	kTextLabelElement                    = 0x15,	// NYI
+	kImageElement                        = 0x7,
+	kSoundElement                        = 0xa,
+	kTextLabelElement                    = 0x15,
 
 	kAliasModifier                       = 0x27,
 	kChangeSceneModifier                 = 0x136,
@@ -134,6 +140,7 @@ enum DataObjectType {
 	kColorTableAsset                     = 0x1e,
 	kImageAsset                          = 0xe,		// NYI
 	kMToonAsset                          = 0xf,		// NYI
+	kTextAsset                           = 0x1f,	// NYI
 
 	kAssetDataChunk                      = 0xffff,
 };
@@ -570,6 +577,68 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct TextLabelElement : public StructuralDef {
+	// Possible element flags: NotDirectToScreen, CacheBitmap, Hidden
+	uint32 sizeIncludingTag;
+	uint32 guid;
+	uint16 lengthOfName;
+	uint32 elementFlags;
+	uint16 layer;
+	uint16 sectionID;
+	Rect rect1;
+	Rect rect2;
+	uint32 assetID;
+
+	struct MacPart {
+		uint8 unknown2[30];
+	};
+
+	struct WinPart {
+		uint8 unknown3[2];
+		uint8 unknown4[8];
+	};
+
+	union PlatformPart {
+		MacPart mac;
+		WinPart win;
+	};
+
+	bool haveMacPart;
+	bool haveWinPart;
+	PlatformPart platform;
+
+	Common::String name;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
+struct SoundElement : public StructuralDef {
+	enum SoundFlags {
+		kPaused = 0x40000000,
+		kLoop = 0x80000000,
+	};
+
+	// Possible element flags: Loop, Paused
+	uint32 sizeIncludingTag;
+	uint32 guid;
+	uint16 lengthOfName;
+	uint32 elementFlags;
+	uint32 soundFlags;
+	uint16 unknown2;
+	uint8 unknown3[2];
+	uint16 rightVolume;
+	uint16 leftVolume;
+	int16 balance;
+	uint32 assetID;
+	uint8 unknown5[8];
+
+	Common::String name;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 struct MovieElement : public StructuralDef {
 	// Possible flags: NotDirectToScreen, CacheBitmap, Hidden, Loop, Loop + Alternate, Paused
 	uint32 sizeIncludingTag;
@@ -1451,6 +1520,63 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct TextAsset : public DataObject {
+	struct MacFormattingSpan {
+		uint8 unknown9[2];
+		uint16 spanStart;
+		uint8 unknown10[4];
+		uint16 fontID;
+		uint8 fontFlags;
+		uint8 unknown11[1];
+		uint16 size;
+		uint8 unknown12[6];
+	};
+
+	struct MacPart {
+		uint8 unknown3[44];
+	};
+
+	struct WinPart {
+		uint8 unknown4[10];
+	};
+
+	union PlatformPart {
+		MacPart mac;
+		WinPart win;
+	};
+
+	uint32 persistFlags;
+	uint32 sizeIncludingTag;
+	uint32 unknown1;
+	uint32 assetID;
+	uint32 unknown2;
+	Rect bitmapRect;
+	uint32 hdpi;
+	uint32 vdpi;
+	uint16 unknown5;
+	uint8 pitchBigEndian[2];
+	uint32 unknown6;
+
+	uint32 bitmapSize;
+	uint8 unknown7[20];
+	uint32 textSize;
+	uint8 unknown8[8];
+	uint16 alignment;
+	uint16 isBitmap;
+
+	bool haveMacPart;
+	bool haveWinPart;
+	PlatformPart platform;
+
+	Common::String text;
+	Common::Array<uint8> bitmapData;
+
+	Common::Array<MacFormattingSpan> macFormattingSpans;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 struct AssetDataChunk : public DataObject {
 	uint32 unknown1;
 	uint32 sizeIncludingTag;
diff --git a/engines/mtropolis/debug.h b/engines/mtropolis/debug.h
index e73f9bd4880..d602cc9f909 100644
--- a/engines/mtropolis/debug.h
+++ b/engines/mtropolis/debug.h
@@ -26,8 +26,6 @@
 #include "common/ptr.h"
 #include "common/hashmap.h"
 
-#include <cstdarg>
-
 #define MTROPOLIS_DEBUG_VTHREAD_STACKS
 #define MTROPOLIS_DEBUG_ENABLE
 
diff --git a/engines/mtropolis/element_factory.cpp b/engines/mtropolis/element_factory.cpp
index 6257e859103..c829cbc965d 100644
--- a/engines/mtropolis/element_factory.cpp
+++ b/engines/mtropolis/element_factory.cpp
@@ -66,6 +66,10 @@ IElementFactory *getElementFactoryForDataObjectType(const Data::DataObjectTypes:
 		return ElementFactory<MovieElement, Data::MovieElement>::getInstance();
 	case Data::DataObjectTypes::kImageElement:
 		return ElementFactory<ImageElement, Data::ImageElement>::getInstance();
+	case Data::DataObjectTypes::kTextLabelElement:
+		return ElementFactory<TextLabelElement, Data::TextLabelElement>::getInstance();
+	case Data::DataObjectTypes::kSoundElement:
+		return ElementFactory<SoundElement, Data::SoundElement>::getInstance();
 
 	default:
 		return nullptr;
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 9ec0e17c820..5cfd35c4685 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -423,4 +423,96 @@ void ImageElement::render(Window *window) {
 	}
 }
 
+TextLabelElement::TextLabelElement() : _needsRender(false), _isBitmap(false) {
+}
+
+TextLabelElement::~TextLabelElement() {
+}
+
+bool TextLabelElement::load(ElementLoaderContext &context, const Data::TextLabelElement &data) {
+	if (!loadCommon(data.name, data.guid, data.rect1, data.elementFlags, data.layer, 0, data.sectionID))
+		return false;
+
+	_cacheBitmap = ((data.elementFlags & Data::ElementFlags::kCacheBitmap) != 0);
+	_assetID = data.assetID;
+	_runtime = context.runtime;
+
+	return true;
+}
+
+bool TextLabelElement::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	return VisualElement::readAttribute(thread, result, attrib);
+}
+
+bool TextLabelElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
+	return VisualElement::writeRefAttribute(thread, writeProxy, attrib);
+}
+
+void TextLabelElement::activate() {
+	Project *project = _runtime->getProject();
+	Common::SharedPtr<Asset> asset = project->getAssetByID(_assetID).lock();
+
+	if (!asset) {
+		warning("Text element references asset %i but the asset isn't loaded!", _assetID);
+		return;
+	}
+
+	if (asset->getAssetType() != kAssetTypeText) {
+		warning("Text element assigned an asset that isn't text");
+		return;
+	}
+
+	TextAsset *textAsset = static_cast<TextAsset *>(asset.get());
+
+	if (textAsset->isBitmap()) {
+		_renderedText = textAsset->getBitmapSurface();
+		_needsRender = false;
+	} else {
+		_needsRender = true;
+		_text = textAsset->getString();
+		_macFormattingSpans = textAsset->getMacFormattingSpans();
+	}
+}
+
+void TextLabelElement::deactivate() {
+}
+
+
+void TextLabelElement::render(Window *window) {
+}
+
+SoundElement::SoundElement() {
+}
+
+SoundElement::~SoundElement() {
+}
+
+bool SoundElement::load(ElementLoaderContext &context, const Data::SoundElement &data) {
+	if (!NonVisualElement::loadCommon(data.name, data.guid, data.elementFlags))
+		return false;
+
+	_paused = ((data.soundFlags & Data::SoundElement::kPaused) != 0);
+	_leftVolume = data.leftVolume;
+	_rightVolume = data.rightVolume;
+	_balance = data.balance;
+	_assetID = data.assetID;
+	_runtime = context.runtime;
+
+	return true;
+}
+
+bool SoundElement::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	return NonVisualElement::readAttribute(thread, result, attrib);
+}
+
+bool SoundElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
+	return NonVisualElement::writeRefAttribute(thread, writeProxy, attrib);
+}
+
+void SoundElement::activate() {
+}
+
+void SoundElement::deactivate() {
+}
+
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index 5de7aa8045e..56bd82422db 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -24,6 +24,7 @@
 
 #include "mtropolis/data.h"
 #include "mtropolis/runtime.h"
+#include "mtropolis/render.h"
 
 namespace Video {
 
@@ -132,6 +133,68 @@ private:
 	Runtime *_runtime;
 };
 
+class TextLabelElement : public VisualElement {
+public:
+	TextLabelElement();
+	~TextLabelElement();
+
+	bool load(ElementLoaderContext &context, const Data::TextLabelElement &data);
+
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
+	bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
+
+	void activate() override;
+	void deactivate() override;
+
+	void render(Window *window) override;
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Text Label Element"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusPartial; }
+#endif
+
+private:
+	bool _cacheBitmap;
+	bool _needsRender;
+
+	bool _isBitmap;
+	uint32 _assetID;
+
+	Common::String _text;
+	Common::Array<MacFormattingSpan> _macFormattingSpans;
+	Common::SharedPtr<Graphics::Surface> _renderedText;	// NOTE: This may be a pre-rendered instance that is read-only.  Rendering must create a new surface!
+
+	Runtime *_runtime;
+};
+
+class SoundElement : public NonVisualElement {
+public:
+	SoundElement();
+	~SoundElement();
+
+	bool load(ElementLoaderContext &context, const Data::SoundElement &data);
+
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
+	bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
+
+	void activate() override;
+	void deactivate() override;
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Sound Element"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusPartial; }
+#endif
+
+private:
+	bool _paused;
+	uint16 _leftVolume;
+	uint16 _rightVolume;
+	int16 _balance;
+	uint32 _assetID;
+
+	Runtime *_runtime;
+};
+
 } // End of namespace MTropolis
 
 #endif
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index c1761d7c1fa..01954cdbc00 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -20,13 +20,10 @@
  */
 
 #include "mtropolis/miniscript.h"
-#include "mtropolis/alignment_helper.h"
 #include "common/config-manager.h"
 
 #include "common/memstream.h"
 
-#include <new>
-
 namespace MTropolis {
 
 static bool miniscriptEvaluateTruth(const DynamicValue& value) {
@@ -254,7 +251,7 @@ bool MiniscriptInstructionFactory<T>::create(void *dest, uint32 instrFlags, Data
 template<class T>
 void MiniscriptInstructionFactory<T>::getSizeAndAlignment(size_t &outSize, size_t &outAlignment) const {
 	outSize = sizeof(T);
-	outAlignment = AlignmentHelper<T>::getAlignment();
+	outAlignment = alignof(T);
 }
 
 template<class T>
diff --git a/engines/mtropolis/notes.txt b/engines/mtropolis/notes.txt
index 86f42ebca3b..332d5e5d293 100644
--- a/engines/mtropolis/notes.txt
+++ b/engines/mtropolis/notes.txt
@@ -16,7 +16,9 @@ copy of the original, and assigning new GUIDs to the new objects.
 
 Objects are only ever materialized once.  Aliasable variables in the global
 modifier table are materialized when the project is loaded, while everything
-else is materialized when it's imported into the project.
+else is materialized when it's imported into the project.  (Important note:
+This doesn't apply to aliased compound variables because they are not
+actually considered variable modifiers!)
 
 Objects cloned from already-materialized objects should NOT be materialized
 again, instead they should be fixed up using an object reference remap table,
diff --git a/engines/mtropolis/render.cpp b/engines/mtropolis/render.cpp
index 12cac6fe828..7034bdce565 100644
--- a/engines/mtropolis/render.cpp
+++ b/engines/mtropolis/render.cpp
@@ -64,6 +64,12 @@ inline int quantize8To5(int value, byte orderedDither16x16) {
 	return (value * 249 + (orderedDither16x16 << 3)) >> 11;
 }
 
+MacFontFormatting::MacFontFormatting() : fontID(0), fontFlags(0), size(12) {
+}
+
+MacFontFormatting::MacFontFormatting(uint16 fontID, uint8 fontFlags, uint16 size) : fontID(fontID), fontFlags(fontFlags), size(size) {
+}
+
 Window::Window(Runtime *runtime, int32 x, int32 y, int16 width, int16 height, const Graphics::PixelFormat &format) : _runtime(runtime), _x(x), _y(y) {
 	_surface.reset(new Graphics::ManagedSurface(width, height, format));
 }
diff --git a/engines/mtropolis/render.h b/engines/mtropolis/render.h
index 782c3451fa1..f2ab278a84c 100644
--- a/engines/mtropolis/render.h
+++ b/engines/mtropolis/render.h
@@ -38,6 +38,26 @@ namespace MTropolis {
 class Runtime;
 class Project;
 
+enum TextAlignment {
+	kTextAlignmentLeft,
+	kTextAlignmentCenter,
+	kTextAlignmentRight,
+};
+
+struct MacFontFormatting {
+	MacFontFormatting();
+	MacFontFormatting(uint16 fontID, uint8 fontFlags, uint16 size);
+
+	uint16 fontID;
+	uint8 fontFlags;
+	uint16 size;
+};
+
+struct MacFormattingSpan {
+	uint16 spanStart;
+	MacFontFormatting formatting;
+};
+
 class Window {
 public:
 	Window(Runtime *runtime, int32 x, int32 y, int16 width, int16 height, const Graphics::PixelFormat &format);
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index cbafe46988c..3521a2d4b84 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -231,6 +231,9 @@ bool Rect16::load(const Data::Rect &rect) {
 	bottom = rect.bottom;
 	right = rect.right;
 
+	if (bottom < top || right < left)
+		return false;
+
 	return true;
 }
 
@@ -2674,9 +2677,6 @@ void Runtime::getScenesInRenderOrder(Common::Array<Structural*> &scenes) const {
 void Runtime::instantiateIfAlias(Common::SharedPtr<Modifier> &modifier, const Common::WeakPtr<RuntimeObject> &relinkParent) {
 	if (modifier->isAlias()) {
 		Common::SharedPtr<Modifier> templateModifier = _project->resolveAlias(static_cast<AliasModifier *>(modifier.get())->getAliasID());
-		if (templateModifier->getStaticGUID() == 0x34130c) {
-			int n = 0;
-		}
 		if (!templateModifier) {
 			error("Failed to resolve alias");
 		}
@@ -3693,7 +3693,12 @@ void Project::loadAssetDef(size_t streamIndex, AssetDefLoaderContext& context, c
 	}
 
 	AssetLoaderContext loaderContext(streamIndex);
-	context.assets.push_back(factory->createAsset(loaderContext, dataObject));
+	Common::SharedPtr<Asset> asset = factory->createAsset(loaderContext, dataObject);
+	if (!asset) {
+		warning("An asset failed to load");
+		return;
+	}
+	context.assets.push_back(asset);
 }
 
 bool Section::load(const Data::SectionStructuralDef &data) {
@@ -3837,6 +3842,15 @@ bool NonVisualElement::isVisual() const {
 	return false;
 }
 
+bool NonVisualElement::loadCommon(const Common::String &name, uint32 guid, uint32 elementFlags) {
+	_name = name;
+	_guid = guid;
+	_streamLocator = 0;
+	_sectionID = 0;
+
+	return true;
+}
+
 
 ModifierFlags::ModifierFlags() : isLastModifier(false), flagsWereLoaded(false) {
 }
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index a13dc5f928d..0fa596ec842 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -276,6 +276,17 @@ struct Rect16 {
 	inline bool operator!=(const Rect16 &other) const {
 		return !((*this) == other);
 	}
+
+	inline uint16 getWidth() const { return static_cast<uint16>(right - left); }
+	inline uint16 getHeight() const { return static_cast<uint16>(bottom - top); }
+	inline static Rect16 create(int16 left, int16 top, int16 right, int16 bottom) {
+		Rect16 result;
+		result.left = left;
+		result.top = top;
+		result.right = right;
+		result.bottom = bottom;
+		return result;
+	}
 };
 
 struct IntRange {
@@ -1627,7 +1638,7 @@ class NonVisualElement : public Element {
 public:
 	bool isVisual() const override;
 
-	bool loadCommon(const Data::Rect &rect, const Common::String &str, uint32 elementFlags);
+	bool loadCommon(const Common::String &name, uint32 guid, uint32 elementFlags);
 };
 
 struct ModifierFlags {
@@ -1714,6 +1725,7 @@ enum AssetType {
 	kAssetTypeAudio,
 	kAssetTypeColorTable,
 	kAssetTypeImage,
+	kAssetTypeText,
 };
 
 class Asset {
diff --git a/engines/mtropolis/vthread.cpp b/engines/mtropolis/vthread.cpp
index 3fd2f13b5b8..83b68139966 100644
--- a/engines/mtropolis/vthread.cpp
+++ b/engines/mtropolis/vthread.cpp
@@ -64,7 +64,7 @@ VThreadState VThread::step() {
 }
 
 void VThread::reserveFrame(size_t size, size_t alignment, void *&outFramePtr, void *&outUnadjustedDataPtr, size_t &outPrevFrameOffset) {
-	const size_t frameAlignment = AlignmentHelper<VThreadStackFrame>::getAlignment();
+	const size_t frameAlignment = alignof(VThreadStackFrame);
 	const size_t frameAlignmentMask = frameAlignment - 1;
 
 	size_t dataAlignmentMask = alignment - 1;
diff --git a/engines/mtropolis/vthread.h b/engines/mtropolis/vthread.h
index f571ed30b3d..993dbab1a3a 100644
--- a/engines/mtropolis/vthread.h
+++ b/engines/mtropolis/vthread.h
@@ -22,8 +22,6 @@
 #ifndef MTROPOLIS_TASKSTACK_H
 #define MTROPOLIS_TASKSTACK_H
 
-#include <new>
-#include "mtropolis/alignment_helper.h"
 #include "mtropolis/debug.h"
 
 namespace MTropolis {
@@ -287,8 +285,8 @@ template<typename TClass, typename TData>
 TData *VThread::pushTaskWithFaultHandler(const VThreadFaultIdentifier *faultID, TClass *obj, VThreadState (TClass::*method)(const TData &data)) {
 	typedef VThreadMethodData<TClass, TData> FrameData_t; 
 
-	const size_t frameAlignment = AlignmentHelper<VThreadStackFrame>::getAlignment();
-	const size_t dataAlignment = AlignmentHelper<FrameData_t>::getAlignment();
+	const size_t frameAlignment = alignof(VThreadStackFrame);
+	const size_t dataAlignment = alignof(FrameData_t);
 	const size_t maxAlignment = (frameAlignment < dataAlignment) ? dataAlignment : frameAlignment;
 
 	void *framePtr;
@@ -314,8 +312,8 @@ template<typename TData>
 TData *VThread::pushTaskWithFaultHandler(const VThreadFaultIdentifier *faultID, VThreadState (*func)(const TData &data)) {
 	typedef VThreadFunctionData<TData> FrameData_t;
 
-	const size_t frameAlignment = AlignmentHelper<VThreadStackFrame>::getAlignment();
-	const size_t dataAlignment = AlignmentHelper<FrameData_t>::getAlignment();
+	const size_t frameAlignment = alignof(VThreadStackFrame);
+	const size_t dataAlignment = alignof(FrameData_t);
 	const size_t maxAlignment = (frameAlignment < dataAlignment) ? dataAlignment : frameAlignment;
 
 	void *framePtr;


Commit: 17458248c28000fba73e789e8a14c68ce6e13f0d
    https://github.com/scummvm/scummvm/commit/17458248c28000fba73e789e8a14c68ce6e13f0d
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Preliminary MIDI stuff

Changed paths:
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/plugins.h


diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index ea0d5750241..b87f8dcfc0a 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -24,11 +24,77 @@
 
 #include "mtropolis/miniscript.h"
 
+#include "audio/mididrv.h"
+#include "audio/midiplayer.h"
+#include "audio/midiparser.h"
+
 
 namespace MTropolis {
 
 namespace Standard {
 
+class MidiPlayer : public Audio::MidiPlayer {
+public:
+	explicit MidiPlayer(int8 source);
+	~MidiPlayer();
+
+	void playFile(const void *data, size_t size);
+	int8 getSource() const;
+
+private:
+	int8 _source;
+	bool _isInitialized;
+};
+
+MidiPlayer::MidiPlayer(int8 source) : _isInitialized(false), _source(source) {
+	MidiDriver::DeviceHandle deviceHdl = MidiDriver::detectDevice(MDT_MIDI);
+	if (!deviceHdl)
+		return;
+
+	_driver = MidiDriver::createMidi(deviceHdl);
+	if (!_driver)
+		return;
+
+	if (_driver->open() != 0) {
+		_driver->close();
+		delete _driver;
+		_driver = nullptr;
+		return;
+	}
+
+	_driver->setTimerCallback(static_cast<Audio::MidiPlayer *>(this), &timerCallback);
+
+	_isInitialized = true;
+}
+
+MidiPlayer::~MidiPlayer() {
+	stop();
+
+	if (_parser)
+		delete _parser;
+}
+
+void MidiPlayer::playFile(const void *data, size_t size) {
+	Common::StackLock lock(_mutex);
+
+	stop();
+
+	_parser = MidiParser::createParser_SMF();
+	_parser->setMidiDriver(this);
+	_parser->setTimerRate(_driver->getBaseTempo());
+
+	// FIXME: MIDI API shouldn't need mutable void input...
+	_parser->loadMusic(static_cast<byte *>(const_cast<void *>(data)), size);
+	_parser->setTrack(0);
+	_parser->startPlaying();
+
+	resume();
+}
+
+int8 MidiPlayer::getSource() const {
+	return _source;
+}
+
 bool CursorModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::CursorModifier &data) {
 	if (!_applyWhen.load(data.applyWhen) || !_removeWhen.load(data.removeWhen))
 		return false;
@@ -134,7 +200,15 @@ Common::SharedPtr<Modifier> ObjectReferenceVariableModifier::shallowClone() cons
 	return Common::SharedPtr<Modifier>(new ObjectReferenceVariableModifier(*this));
 }
 
+MidiModifier::MidiModifier() : _plugIn(nullptr) {
+}
+
+MidiModifier::~MidiModifier() {
+}
+
 bool MidiModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::MidiModifier &data) {
+	_plugIn = static_cast<StandardPlugIn *>(context.plugIn);
+
 	if (data.executeWhen.type != Data::PlugInTypeTaggedValue::kEvent)
 		return false;
 
@@ -176,8 +250,29 @@ bool MidiModifier::load(const PlugInModifierLoaderContext &context, const Data::
 	return true;
 }
 
+bool MidiModifier::respondsToEvent(const Event &evt) const {
+	return _executeWhen.respondsTo(evt) || _terminateWhen.respondsTo(evt);
+}
+
+VThreadState MidiModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	if (_executeWhen.respondsTo(msg->getEvent())) {
+		if (_mode == kModeFile) {
+			if (_embeddedFile) {
+				_plugIn->getMidi()->playFile(&_embeddedFile->contents[0], _embeddedFile->contents.size());
+			}
+		}
+
+	}
+
+	return kVThreadReturn;
+}
+
 Common::SharedPtr<Modifier> MidiModifier::shallowClone() const {
-	return Common::SharedPtr<Modifier>(new MidiModifier(*this));
+	Common::SharedPtr<MidiModifier> clone(new MidiModifier(*this));
+
+	clone->_isActive = false;
+
+	return clone;
 }
 
 ListVariableModifier::ListVariableModifier() : _list(new DynamicList()) {
@@ -322,7 +417,12 @@ StandardPlugIn::StandardPlugIn()
 	, _objRefVarModifierFactory(this)
 	, _midiModifierFactory(this)
 	, _listVarModifierFactory(this)
-	, _sysInfoModifierFactory(this) {
+	, _sysInfoModifierFactory(this)
+	, _lastAllocatedSourceID(0) {
+	_midi.reset(new MidiPlayer(0));
+}
+
+StandardPlugIn::~StandardPlugIn() {
 }
 
 void StandardPlugIn::registerModifiers(IPlugInModifierRegistrar *registrar) const {
@@ -343,6 +443,28 @@ StandardPlugInHacks& StandardPlugIn::getHacks() {
 	return _hacks;
 }
 
+MidiPlayer *StandardPlugIn::getMidi() const {
+	return _midi.get();
+}
+
+int8 StandardPlugIn::allocateMidiSource() {
+	if (_deallocatedSources.size() > 0) {
+		int8 src = _deallocatedSources.back();
+		_deallocatedSources.pop_back();
+		return src;
+	}
+
+	if (_lastAllocatedSourceID == INT8_MAX)
+		error("Ran out of MIDI sources");
+
+	_lastAllocatedSourceID++;
+	return _lastAllocatedSourceID;
+}
+
+void StandardPlugIn::deallocateMidiSource(int8 source) {
+	_deallocatedSources.push_back(source);
+}
+
 } // End of namespace Standard
 
 namespace PlugIns {
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index ea1cb4055a0..85e8445eaf2 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -27,6 +27,8 @@
 #include "mtropolis/runtime.h"
 #include "mtropolis/plugin/standard_data.h"
 
+class MidiDriver;
+
 namespace MTropolis {
 
 class Runtime;
@@ -34,6 +36,7 @@ class Runtime;
 namespace Standard {
 
 class StandardPlugIn;
+class MidiPlayer;
 
 class CursorModifier : public Modifier {
 public:
@@ -112,8 +115,14 @@ private:
 
 class MidiModifier : public Modifier {
 public:
+	MidiModifier();
+	~MidiModifier();
+
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::MidiModifier &data);
 
+	bool respondsToEvent(const Event &evt) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "MIDI Modifier"; }
 #endif
@@ -155,6 +164,10 @@ private:
 	ModeSpecificUnion _modeSpecific;
 
 	Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> _embeddedFile;
+
+	bool _isActive;
+
+	StandardPlugIn *_plugIn;
 };
 
 class ListVariableModifier : public VariableModifier {
@@ -206,12 +219,18 @@ struct StandardPlugInHacks {
 class StandardPlugIn : public MTropolis::PlugIn {
 public:
 	StandardPlugIn();
+	~StandardPlugIn();
 
 	void registerModifiers(IPlugInModifierRegistrar *registrar) const override;
 
 	const StandardPlugInHacks &getHacks() const;
 	StandardPlugInHacks &getHacks();
 
+	MidiPlayer *getMidi() const;
+
+	int8 allocateMidiSource();
+	void deallocateMidiSource(int8 source);
+
 private:
 	PlugInModifierFactory<CursorModifier, Data::Standard::CursorModifier> _cursorModifierFactory;
 	PlugInModifierFactory<STransCtModifier, Data::Standard::STransCtModifier> _sTransCtModifierFactory;
@@ -221,7 +240,11 @@ private:
 	PlugInModifierFactory<ListVariableModifier, Data::Standard::ListVariableModifier> _listVarModifierFactory;
 	PlugInModifierFactory<SysInfoModifier, Data::Standard::SysInfoModifier> _sysInfoModifierFactory;
 
+	Common::SharedPtr<MidiPlayer> _midi;
 	StandardPlugInHacks _hacks;
+
+	Common::Array<int8> _deallocatedSources;
+	int8 _lastAllocatedSourceID;
 };
 
 } // End of namespace Standard
diff --git a/engines/mtropolis/plugins.h b/engines/mtropolis/plugins.h
index fc08a508135..3c8ab00f903 100644
--- a/engines/mtropolis/plugins.h
+++ b/engines/mtropolis/plugins.h
@@ -24,6 +24,8 @@
 
 #include "common/ptr.h"
 
+class MidiDriver;
+
 namespace MTropolis {
 
 class PlugIn;


Commit: dc333593f181238b25241d8930bf1b5976cdd56f
    https://github.com/scummvm/scummvm/commit/dc333593f181238b25241d8930bf1b5976cdd56f
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Debug overlay base work

Changed paths:
  A engines/mtropolis/actions.h
    engines/mtropolis/debug.cpp
    engines/mtropolis/debug.h
    engines/mtropolis/elements.cpp
    engines/mtropolis/metaengine.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/mtropolis.h
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/render.cpp
    engines/mtropolis/render.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/actions.h b/engines/mtropolis/actions.h
new file mode 100644
index 00000000000..0af00a8df1a
--- /dev/null
+++ b/engines/mtropolis/actions.h
@@ -0,0 +1,47 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_ACTIONS_H
+#define MTROPOLIS_ACTIONS_H
+
+namespace MTropolis {
+
+namespace Actions {
+
+enum Action {
+	kNone = 0,
+
+	kDebugToggleOverlay,
+};
+
+enum MouseButton {
+	kMouseButtonLeft,
+	kMouseButtonRight,
+	kMouseButtonMiddle,
+
+	kMouseButtonCount,
+};
+
+} // End of namespace Actions
+
+} // End of namespace MTropolis
+
+#endif
diff --git a/engines/mtropolis/debug.cpp b/engines/mtropolis/debug.cpp
index b55ded7e1a1..2881a669dfd 100644
--- a/engines/mtropolis/debug.cpp
+++ b/engines/mtropolis/debug.cpp
@@ -29,6 +29,274 @@
 
 namespace MTropolis {
 
+static const byte g_sceneTreeGraphic[] = {
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
+	0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
+	0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
+	0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
+	0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
+	0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
+	0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0,
+	0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
+	0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0,
+	0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0,
+	0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0,
+	0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
+	0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
+	0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+static const byte g_inspectorGraphic[] = {
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
+	0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0,
+	0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0,
+	0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0,
+	0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0,
+	0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0,
+	0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0,
+	0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0,
+	0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0,
+	0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
+	0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0,
+	0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0,
+	0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0,
+	0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+static const byte g_stepThroughGraphic[] = {
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
+	0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0,
+	0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0,
+	0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0,
+	0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0,
+	0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0,
+	0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0,
+	0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0,
+	0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0,
+	0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0,
+	0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0,
+	0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0,
+	0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0,
+	0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+static const byte g_resizeGraphic[] = {
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
+	0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
+	0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0,
+	0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0,
+	0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0,
+	0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0,
+	0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0,
+	0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0,
+	0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
+	0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+class DebugToolWindowBase : public Window {
+public:
+	DebugToolWindowBase(DebuggerTool tool, const Common::String &title, Debugger *debugger, const WindowParameters &windowParams);
+
+protected:
+	const int kTopBarHeight = 12;
+	const int kScrollBarWidth = 12;
+	const int kCloseWidth = 12;
+	const int kResizeHeight = 12;
+
+	void onMouseDown(int32 x, int32 y, int mouseButton) override;
+	void onMouseMove(int32 x, int32 y) override;
+	void onMouseUp(int32 x, int32 y, int mouseButton) override;
+
+	virtual void toolOnMouseDown(int32 x, int32 y, int mouseButton) {}
+	virtual void toolOnMouseMove(int32 x, int32 y) {}
+	virtual void toolOnMouseUp(int32 x, int32 y, int mouseButton) {}
+
+	void refreshChrome();
+
+	Debugger *_debugger;
+	Common::SharedPtr<Graphics::ManagedSurface> _toolSurface;
+
+private:
+	enum ToolWindowWidget {
+		kToolWindowWidgetNone,
+
+		kToolWindowWidgetClose,
+		kToolWindowWidgetScroll,
+		kToolWindowWidgetResize,
+		kToolWindowWidgetMove,
+	};
+	ToolWindowWidget _activeWidget;
+	bool _isMouseCaptured;
+	int32 _dragStartX;
+	int32 _dragStartY;
+	int32 _resizeStartWidth;
+	int32 _resizeStartHeight;
+	DebuggerTool _tool;
+
+	Common::String _title;
+};
+
+DebugToolWindowBase::DebugToolWindowBase(DebuggerTool tool, const Common::String &title, Debugger *debugger, const WindowParameters &windowParams)
+	: Window(windowParams), _debugger(debugger), _tool(tool), _title(title), _activeWidget(kToolWindowWidgetNone), _isMouseCaptured(false) {
+
+	refreshChrome();
+}
+
+void DebugToolWindowBase::onMouseDown(int32 x, int32 y, int mouseButton) {
+	if (mouseButton != Actions::kMouseButtonLeft)
+		return;
+
+	if (_isMouseCaptured)
+		return;
+
+	_isMouseCaptured = true;
+	_dragStartX = x;
+	_dragStartY = y;
+
+	if (y < kTopBarHeight) {
+		if (x < kCloseWidth)
+			_activeWidget = kToolWindowWidgetClose;
+		else
+			_activeWidget = kToolWindowWidgetMove;
+
+		_dragStartX = x;
+		_dragStartY = y;
+	} else if (x >= getWidth() - kScrollBarWidth) {
+		if (y >= getHeight() - kResizeHeight) {
+			_activeWidget = kToolWindowWidgetResize;
+			_resizeStartWidth = getWidth();
+			_resizeStartHeight = getHeight();
+		}
+		else
+			_activeWidget = kToolWindowWidgetScroll;
+	} else {
+		_activeWidget = kToolWindowWidgetNone;
+		toolOnMouseDown(x, y - kTopBarHeight, mouseButton);
+	}
+}
+
+void DebugToolWindowBase::onMouseMove(int32 x, int32 y) {
+	if (_activeWidget == kToolWindowWidgetNone)
+		toolOnMouseMove(x, y - kTopBarHeight);
+	else {
+		if (_activeWidget == kToolWindowWidgetMove) {
+			int32 relX = x - _dragStartX;
+			int32 relY = y - _dragStartY;
+			setPosition(getX() + relX, getY() + relY);
+		} else if (_activeWidget == kToolWindowWidgetResize) {
+			int32 relX = x - _dragStartX;
+			int32 relY = y - _dragStartY;
+			int32 newWidth = _resizeStartWidth + relX;
+			int32 newHeight = _resizeStartHeight + relY;
+
+			if (newWidth < 100)
+				newWidth = 100;
+			if (newHeight < 100)
+				newHeight = 100;
+
+			if (newWidth != getWidth() || newHeight != getHeight()) {
+				this->resizeWindow(newWidth, newHeight);
+				refreshChrome();
+			}
+		}
+	}
+}
+
+void DebugToolWindowBase::onMouseUp(int32 x, int32 y, int mouseButton) {
+	if (mouseButton != Actions::kMouseButtonLeft)
+		return;
+
+	if (!_isMouseCaptured)
+		return;
+
+	_isMouseCaptured = false;
+
+	if (_activeWidget == kToolWindowWidgetNone)
+		toolOnMouseUp(x, y - kTopBarHeight, mouseButton);
+	else {
+		if (_activeWidget == kToolWindowWidgetClose) {
+			if (x < kCloseWidth && y < kTopBarHeight) {
+				_debugger->closeToolWindow(_tool);
+				return;
+			}
+		}
+
+		_activeWidget = kToolWindowWidgetNone;
+	}
+}
+
+void DebugToolWindowBase::refreshChrome() {
+	Graphics::ManagedSurface *surface = getSurface().get();
+
+	const Graphics::PixelFormat &fmt = surface->rawSurface().format;
+
+	uint32 blackColor = fmt.RGBToColor(0, 0, 0);
+	uint32 whiteColor = fmt.RGBToColor(255, 255, 255);
+	uint32 closeColor = fmt.RGBToColor(255, 0, 0);
+
+	uint32 topBarColor = fmt.RGBToColor(192, 192, 192);
+	uint32 topTextColor = blackColor;
+
+	uint32 inactiveScrollColor = fmt.RGBToColor(225, 225, 225);
+
+	int width = surface->w;
+	int height = surface->h;
+
+	for (int y = 0; y < 12; y++) {
+		for (int x = 0; x < 12; x++) {
+			uint32 pixelColor = (g_resizeGraphic[y * 12 + x] == 0) ? blackColor : whiteColor;
+			surface->setPixel(width - 12 + x, height - 12 + y, pixelColor);
+		}
+	}
+
+	surface->fillRect(Common::Rect(0, 0, width, kTopBarHeight), topBarColor);
+
+	const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
+	int titleWidth = font->getStringWidth(_title);
+	int titleAvailableWidth = width - kCloseWidth;
+	if (titleWidth < titleAvailableWidth)
+		titleWidth = titleAvailableWidth;
+
+	int titleY = (kTopBarHeight - font->getFontAscent()) / 2;
+
+	font->drawString(surface, _title, kCloseWidth, titleY, titleAvailableWidth, topTextColor, Graphics::kTextAlignCenter, 0, true);
+
+	surface->fillRect(Common::Rect(width - kScrollBarWidth, kTopBarHeight, width, height - kResizeHeight), inactiveScrollColor);
+	surface->fillRect(Common::Rect(0, 0, kCloseWidth, kTopBarHeight), closeColor);
+	surface->drawThickLine(2, 2, kCloseWidth - 4, kTopBarHeight - 4, 2, 2, whiteColor);
+	surface->drawThickLine(kCloseWidth - 4, 2, 2, kTopBarHeight - 4, 2, 2, whiteColor);
+}
+
+class DebugToolsWindow : public Window {
+public:
+	DebugToolsWindow(Debugger *debugger, const WindowParameters &windowParams);
+
+	void onMouseDown(int32 x, int32 y, int mouseButton) override;
+
+private:
+	Debugger *_debugger;
+};
+
+DebugToolsWindow::DebugToolsWindow(Debugger *debugger, const WindowParameters &windowParams)
+	: Window(windowParams), _debugger(debugger) {
+}
+
+void DebugToolsWindow::onMouseDown(int32 x, int32 y, int mouseButton) {
+	int tool = 0;
+	if (y > 1)
+		tool = (y - 1) / 17;
+	_debugger->openToolWindow(static_cast<DebuggerTool>(tool));
+}
+
+
 DebugInspector::DebugInspector(IDebuggable *debuggable) {
 }
 
@@ -41,11 +309,50 @@ void DebugInspector::onDestroyed() {
 
 Debugger::Debugger(Runtime *runtime) : _paused(false), _runtime(runtime) {
 	refreshSceneStatus();
+
+	const Graphics::PixelFormat renderFmt = runtime->getRenderPixelFormat();
+
+	const byte *toolGraphics[kDebuggerToolCount] = {
+		g_sceneTreeGraphic,
+		g_inspectorGraphic,
+		g_stepThroughGraphic,
+	};
+
+	_toolsWindow.reset(new DebugToolsWindow(this, WindowParameters(runtime, 0, 0, 18, 1 + kDebuggerToolCount * 17, renderFmt)));
+	Graphics::ManagedSurface *toolWindowSurface = _toolsWindow->getSurface().get();
+
+	uint32 whiteColor = renderFmt.RGBToColor(255, 255, 255);
+	uint32 blackColor = renderFmt.RGBToColor(0, 0, 0);
+
+	const uint32 toolGraphicPalette[] = {blackColor, whiteColor};
+
+	for (int y = 0; y < 1 + kDebuggerToolCount * 17; y++) {
+		for (int x = 0; x < 18; x++) {
+			toolWindowSurface->setPixel(x, y, whiteColor);
+		}
+	}
+
+	for (int tool = 0; tool < kDebuggerToolCount; tool++) {
+		const byte *toolGraphic = toolGraphics[tool];
+
+		for (int y = 0; y < 16; y++) {
+			for (int x = 0; x < 16; x++) {
+				toolWindowSurface->setPixel(x + 1, tool * 17 + 1 + y, toolGraphicPalette[toolGraphic[y * 16 + x]]);
+			}
+		}
+	}
+
+	_toolsWindow->setStrata(1);
+	runtime->addWindow(_toolsWindow);
 }
 
 Debugger::~Debugger() {
-	if (_runtime)
+	if (_runtime) {
 		_runtime->removeWindow(_sceneStatusWindow.get());
+		_runtime->removeWindow(_toolsWindow.get());
+		for (int i = 0; i < kDebuggerToolCount; i++)
+			_runtime->removeWindow(_toolWindows[i].get());
+	}
 }
 
 void Debugger::runFrame(uint32 msec) {
@@ -95,7 +402,9 @@ void Debugger::notify(DebugSeverity severity, const Common::String& str) {
 	const Graphics::PixelFormat pixelFmt = _runtime->getRenderPixelFormat();
 
 	ToastNotification toastNotification;
-	toastNotification.window.reset(new Window(_runtime, 0, displayHeight, width, toastNotificationHeight, pixelFmt));
+	toastNotification.window.reset(new Window(WindowParameters(_runtime, 0, displayHeight, width, toastNotificationHeight, pixelFmt)));
+	toastNotification.window->setStrata(3);
+	toastNotification.window->setMouseTransparent(true);
 
 	byte fillColor[3] = {255, 255, 255};
 	if (severity == kDebugSeverityError) {
@@ -165,12 +474,18 @@ void Debugger::refreshSceneStatus() {
 
 	const Graphics::PixelFormat pixelFmt = _runtime->getRenderPixelFormat();
 
-	_sceneStatusWindow.reset(new Window(_runtime, 0, 0, horizPadding * 2 + width, vertSpacing * sceneStrs.size(), pixelFmt));
+	_sceneStatusWindow.reset(new Window(WindowParameters(_runtime, 0, 0, horizPadding * 2 + width, vertSpacing * sceneStrs.size(), pixelFmt)));
+	_sceneStatusWindow->setMouseTransparent(true);
+	_sceneStatusWindow->setStrata(1);
+
 	_runtime->addWindow(_sceneStatusWindow);
 
 	for (uint i = 0; i < sceneStrs.size(); i++) {
 		font->drawString(_sceneStatusWindow->getSurface().get(), sceneStrs[i], horizPadding, vertSpacing * i + (vertSpacing - font->getFontAscent()) / 2, width, Render::resolveRGB(255, 255, 255, pixelFmt));
 	}
+
+	if (_toolsWindow)
+		_toolsWindow->setPosition(0, _sceneStatusWindow->getHeight());
 }
 
 void Debugger::complainAboutUnfinished(Structural *structural) {
@@ -204,6 +519,37 @@ void Debugger::complainAboutUnfinished(Structural *structural) {
 	}
 }
 
+void Debugger::openToolWindow(DebuggerTool tool) {
+	if (tool < 0 || tool >= kDebuggerToolCount)
+		return;	// This should never happen
+
+	Common::SharedPtr<Window> &windowRef = _toolWindows[tool];
+	if (windowRef)
+		return;
+
+	switch (tool) {
+	case kDebuggerToolSceneTree:
+		windowRef.reset(new DebugToolWindowBase(kDebuggerToolSceneTree, "SceneTree", this, WindowParameters(_runtime, 32, 32, 100, 320, _runtime->getRenderPixelFormat())));
+		break;
+	case kDebuggerToolInspector:
+		windowRef.reset(new DebugToolWindowBase(kDebuggerToolInspector, "Inspector", this, WindowParameters(_runtime, 32, 32, 100, 320, _runtime->getRenderPixelFormat())));
+		break;
+	case kDebuggerToolStepThrough:
+		windowRef.reset(new DebugToolWindowBase(kDebuggerToolStepThrough, "Debugger", this, WindowParameters(_runtime, 32, 32, 100, 320, _runtime->getRenderPixelFormat())));
+		break;
+	default:
+		assert(false);
+		return;
+	}
+
+	_runtime->addWindow(windowRef);
+}
+
+void Debugger::closeToolWindow(DebuggerTool tool) {
+	_runtime->removeWindow(_toolWindows[tool].get());
+	_toolWindows[tool].reset();
+}
+
 void Debugger::scanStructuralStatus(Structural *structural, Common::HashMap<Common::String, SupportStatus> &unfinishedModifiers, Common::HashMap<Common::String, SupportStatus> &unfinishedElements) {
 	for (Common::Array<Common::SharedPtr<Structural>>::const_iterator it = structural->getChildren().begin(), itEnd = structural->getChildren().end(); it != itEnd; ++it) {
 		scanStructuralStatus(it->get(), unfinishedModifiers, unfinishedElements);
diff --git a/engines/mtropolis/debug.h b/engines/mtropolis/debug.h
index d602cc9f909..8869d0a651b 100644
--- a/engines/mtropolis/debug.h
+++ b/engines/mtropolis/debug.h
@@ -70,6 +70,14 @@ private:
 	Runtime *_runtime;
 };
 
+enum DebuggerTool {
+	kDebuggerToolSceneTree,
+	kDebuggerToolInspector,
+	kDebuggerToolStepThrough,
+
+	kDebuggerToolCount,
+};
+
 class Debugger {
 public:
 	explicit Debugger(Runtime *runtime);
@@ -87,6 +95,9 @@ public:
 	void refreshSceneStatus();
 	void complainAboutUnfinished(Structural *structural);
 
+	void openToolWindow(DebuggerTool tool);
+	void closeToolWindow(DebuggerTool tool);
+
 private:
 	Debugger();
 
@@ -102,6 +113,8 @@ private:
 	bool _paused;
 	Runtime *_runtime;
 	Common::SharedPtr<Window> _sceneStatusWindow;
+	Common::SharedPtr<Window> _toolsWindow;
+	Common::SharedPtr<Window> _toolWindows[kDebuggerToolCount];
 	Common::Array<ToastNotification> _toastNotifications;
 };
 
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 5cfd35c4685..e827dece2e5 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -323,6 +323,12 @@ void ImageElement::activate() {
 		return;
 	}
 
+	// If this is the same mode as the render target, then copy the exact mode
+	// so blits go faster
+	if (imageAsset->getColorDepth() == _runtime->getRealColorDepth()) {
+		pixelFmt = _runtime->getRenderPixelFormat();
+	}
+
 	Common::Array<uint8> rowBuffer;
 	rowBuffer.resize(bytesPerRow);
 
diff --git a/engines/mtropolis/metaengine.cpp b/engines/mtropolis/metaengine.cpp
index d80b93f72ef..bfbd24791e4 100644
--- a/engines/mtropolis/metaengine.cpp
+++ b/engines/mtropolis/metaengine.cpp
@@ -20,9 +20,16 @@
  */
 
 #include "engines/advancedDetector.h"
+
+#include "mtropolis/actions.h"
+#include "mtropolis/debug.h"
 #include "mtropolis/detection.h"
+
 #include "mtropolis/mtropolis.h"
 
+#include "backends/keymapper/action.h"
+#include "backends/keymapper/keymap.h"
+
 namespace MTropolis {
 
 uint32 MTropolisEngine::getGameID() const {
@@ -47,6 +54,8 @@ public:
 
 	bool hasFeature(MetaEngineFeature f) const override;
 	Common::Error createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const override;
+
+	Common::Array<Common::Keymap *> initKeymaps(const char *target) const override;
 };
 
 bool MTropolisMetaEngine::hasFeature(MetaEngineFeature f) const {
@@ -54,7 +63,12 @@ bool MTropolisMetaEngine::hasFeature(MetaEngineFeature f) const {
 }
 
 bool MTropolis::MTropolisEngine::hasFeature(EngineFeature f) const {
-	return (f == kSupportsReturnToLauncher);
+	switch (f) {
+	case kSupportsReturnToLauncher:
+		return true;
+	default:
+		return false;
+	};
 }
 
 Common::Error MTropolisMetaEngine::createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const {
@@ -62,6 +76,19 @@ Common::Error MTropolisMetaEngine::createInstance(OSystem *syst, Engine **engine
 	return Common::kNoError;
 }
 
+
+Common::Array<Common::Keymap *> MTropolisMetaEngine::initKeymaps(const char *target) const {
+	Common::Keymap *keymap = new Common::Keymap(Common::Keymap::kKeymapTypeGame, "mtropolis", "mTropolis");
+
+	Common::Action *act;
+	act = new Common::Action("DEBUG_TOGGLE_OVERLAY", Common::convertUtf8ToUtf32("Toggle debug overlay"));
+	act->setCustomEngineActionEvent(MTropolis::Actions::kDebugToggleOverlay);
+	act->addDefaultInputMapping("F10");
+	keymap->addAction(act);
+
+	return Common::Keymap::arrayOf(keymap);
+}
+
 #if PLUGIN_ENABLED_DYNAMIC(MTROPOLIS)
 REGISTER_PLUGIN_DYNAMIC(MTROPOLIS, PLUGIN_TYPE_ENGINE, MTropolisMetaEngine);
 #else
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index a31d45ac9fc..0a91b4be78d 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -784,28 +784,44 @@ void CompoundVariableModifier::visitInternalReferences(IStructuralReferenceVisit
 }
 
 bool CompoundVariableModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
-	VariableModifier *var = findChildByName(attrib);
-	if (!var)
+	Modifier *var = findChildByName(attrib);
+	if (!var || !var->isModifier())
 		return false;
 
-	var->getValue(result);
+	static_cast<VariableModifier *>(var)->getValue(result);
 	return true;
 }
 
+bool CompoundVariableModifier::readAttributeIndexed(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib, const DynamicValue &index) {
+	Modifier *var = findChildByName(attrib);
+	if (!var || !var->isModifier())
+		return false;
+
+	return var->readAttributeIndexed(thread, result, "value", index);
+}
+
 bool CompoundVariableModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
-	VariableModifier *var = findChildByName(attrib);
-	if (!var)
+	Modifier *var = findChildByName(attrib);
+	if (!var || !var->isModifier())
 		return false;
 
-	writeProxy = DynamicValueWriteFuncHelper<VariableModifier, &VariableModifier::setValue>::create(var);
+	writeProxy = DynamicValueWriteFuncHelper<VariableModifier, &VariableModifier::setValue>::create(static_cast<VariableModifier *>(var));
 	return true;
 }
 
-VariableModifier *CompoundVariableModifier::findChildByName(const Common::String &name) const {
+bool CompoundVariableModifier::writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib, const DynamicValue &index) {
+	Modifier *var = findChildByName(attrib);
+	if (!var || !var->isModifier())
+		return false;
+
+	return var->writeRefAttributeIndexed(thread, writeProxy, "value", index);
+}
+
+Modifier *CompoundVariableModifier::findChildByName(const Common::String &name) const {
 	for (Common::Array<Common::SharedPtr<Modifier> >::const_iterator it = _children.begin(), itEnd = _children.end(); it != itEnd; ++it) {
 		Modifier *modifier = it->get();
-		if (modifier->isVariable() && caseInsensitiveEqual(name, modifier->getName()))
-			return static_cast<VariableModifier *>(modifier);
+		if (caseInsensitiveEqual(name, modifier->getName()))
+			return modifier;
 	}
 
 	return nullptr;
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index ce1d6aa457d..a55ef0ef460 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -571,9 +571,11 @@ private:
 	void visitInternalReferences(IStructuralReferenceVisitor *visitor) override;
 
 	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
+	bool readAttributeIndexed(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib, const DynamicValue &index) override;
 	bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) override;
+	bool writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib, const DynamicValue &index) override;
 
-	VariableModifier *findChildByName(const Common::String &name) const;
+	Modifier *findChildByName(const Common::String &name) const;
 
 	Common::Array<Common::SharedPtr<Modifier> > _children;
 };
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index c7dbcd38893..ca0db128350 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -20,6 +20,8 @@
  */
 
 #include "mtropolis/mtropolis.h"
+
+#include "mtropolis/actions.h"
 #include "mtropolis/console.h"
 #include "mtropolis/debug.h"
 #include "mtropolis/runtime.h"
@@ -232,6 +234,28 @@ void MTropolisEngine::handleEvents() {
 
 	while (eventMan->pollEvent(evt)) {
 		switch (evt.type) {
+		case Common::EVENT_LBUTTONDOWN:
+			_runtime->onMouseDown(evt.mouse.x, evt.mouse.y, MTropolis::Actions::kMouseButtonLeft);
+			break;
+		case Common::EVENT_MBUTTONDOWN:
+			_runtime->onMouseDown(evt.mouse.x, evt.mouse.y, MTropolis::Actions::kMouseButtonMiddle);
+			break;
+		case Common::EVENT_RBUTTONDOWN:
+			_runtime->onMouseDown(evt.mouse.x, evt.mouse.y, MTropolis::Actions::kMouseButtonRight);
+			break;
+		case Common::EVENT_LBUTTONUP:
+			_runtime->onMouseUp(evt.mouse.x, evt.mouse.y, MTropolis::Actions::kMouseButtonLeft);
+			break;
+		case Common::EVENT_MBUTTONUP:
+			_runtime->onMouseUp(evt.mouse.x, evt.mouse.y, MTropolis::Actions::kMouseButtonMiddle);
+			break;
+		case Common::EVENT_RBUTTONUP:
+			_runtime->onMouseUp(evt.mouse.x, evt.mouse.y, MTropolis::Actions::kMouseButtonRight);
+			break;
+		case Common::EVENT_MOUSEMOVE:
+			_runtime->onMouseMove(evt.mouse.x, evt.mouse.y);
+			break;
+
 		default:
 			break;
 		}
diff --git a/engines/mtropolis/mtropolis.h b/engines/mtropolis/mtropolis.h
index c7bb4757ae9..2677cf6de56 100644
--- a/engines/mtropolis/mtropolis.h
+++ b/engines/mtropolis/mtropolis.h
@@ -38,6 +38,8 @@
  */
 namespace MTropolis {
 
+
+
 class Runtime;
 
 class MTropolisEngine : public ::Engine {
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index b87f8dcfc0a..8008cfeb4e6 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -47,7 +47,7 @@ private:
 };
 
 MidiPlayer::MidiPlayer(int8 source) : _isInitialized(false), _source(source) {
-	MidiDriver::DeviceHandle deviceHdl = MidiDriver::detectDevice(MDT_MIDI);
+	MidiDriver::DeviceHandle deviceHdl = MidiDriver::detectDevice(MDT_MIDI | MDT_PREFER_GM);
 	if (!deviceHdl)
 		return;
 
diff --git a/engines/mtropolis/render.cpp b/engines/mtropolis/render.cpp
index 7034bdce565..b64f3fd2b71 100644
--- a/engines/mtropolis/render.cpp
+++ b/engines/mtropolis/render.cpp
@@ -25,6 +25,8 @@
 #include "graphics/surface.h"
 #include "graphics/managed_surface.h"
 
+#include "graphics/cursorman.h"
+
 namespace MTropolis {
 
 template<class TNumber, int TResolution>
@@ -70,8 +72,13 @@ MacFontFormatting::MacFontFormatting() : fontID(0), fontFlags(0), size(12) {
 MacFontFormatting::MacFontFormatting(uint16 fontID, uint8 fontFlags, uint16 size) : fontID(fontID), fontFlags(fontFlags), size(size) {
 }
 
-Window::Window(Runtime *runtime, int32 x, int32 y, int16 width, int16 height, const Graphics::PixelFormat &format) : _runtime(runtime), _x(x), _y(y) {
-	_surface.reset(new Graphics::ManagedSurface(width, height, format));
+WindowParameters::WindowParameters(Runtime *runtime, int32 x, int32 y, int16 width, int16 height, const Graphics::PixelFormat &format)
+	: runtime(runtime), x(x), y(y), width(width), height(height), format(format) {
+}
+
+Window::Window(const WindowParameters &windowParams)
+	: _runtime(windowParams.runtime), _x(windowParams.x), _y(windowParams.y), _strata(0), _isMouseTransparent(false) {
+	_surface.reset(new Graphics::ManagedSurface(windowParams.width, windowParams.height, windowParams.format));
 }
 
 Window::~Window() {
@@ -85,11 +92,24 @@ int32 Window::getY() const {
 	return _y;
 }
 
+int32 Window::getWidth() const {
+	return _surface->w;
+}
+
+int32 Window::getHeight() const {
+	return _surface->h;
+}
+
 void Window::setPosition(int32 x, int32 y) {
 	_x = x;
 	_y = y;
 }
 
+void Window::resizeWindow(int32 width, int32 height) {
+	Graphics::PixelFormat pixFmt = _surface->format;
+	_surface.reset();
+	_surface.reset(new Graphics::ManagedSurface(width, height, pixFmt));
+}
 
 const Common::SharedPtr<Graphics::ManagedSurface> &Window::getSurface() const {
 	return _surface;
@@ -99,6 +119,23 @@ const Graphics::PixelFormat& Window::getPixelFormat() const {
 	return _surface->format;
 }
 
+void Window::setStrata(int strata) {
+	_strata = strata;
+}
+
+int Window::getStrata() const {
+	return _strata;
+}
+
+// Mouse transparency = ignores mouse events
+void Window::setMouseTransparent(bool isTransparent) {
+	_isMouseTransparent = isTransparent;
+}
+
+bool Window::isMouseTransparent() const {
+	return _isMouseTransparent;
+}
+
 void Window::close() {
 	Runtime *runtime = _runtime;
 	_runtime = nullptr;
@@ -111,6 +148,16 @@ void Window::detachFromRuntime() {
 	_runtime = nullptr;
 }
 
+void Window::onMouseDown(int32 x, int32 y, int mouseButton) {
+}
+
+void Window::onMouseMove(int32 x, int32 y) {
+}
+
+void Window::onMouseUp(int32 x, int32 y, int mouseButton) {
+}
+
+
 namespace Render {
 
 uint32 resolveRGB(uint8 r, uint8 g, uint8 b, const Graphics::PixelFormat &fmt) {
diff --git a/engines/mtropolis/render.h b/engines/mtropolis/render.h
index f2ab278a84c..3e6284e2b90 100644
--- a/engines/mtropolis/render.h
+++ b/engines/mtropolis/render.h
@@ -58,26 +58,53 @@ struct MacFormattingSpan {
 	MacFontFormatting formatting;
 };
 
+struct WindowParameters {
+	Runtime *runtime;
+	int32 x;
+	int32 y;
+	int16 width;
+	int16 height;
+	const Graphics::PixelFormat format;
+
+	WindowParameters(Runtime *runtime, int32 x, int32 y, int16 width, int16 height, const Graphics::PixelFormat &format);
+};
+
 class Window {
 public:
-	Window(Runtime *runtime, int32 x, int32 y, int16 width, int16 height, const Graphics::PixelFormat &format);
+	explicit Window(const WindowParameters &windowParams);
 	~Window();
 
 	int32 getX() const;
 	int32 getY() const;
+	int32 getWidth() const;
+	int32 getHeight() const;
 	void setPosition(int32 x, int32 y);
+	void resizeWindow(int32 width, int32 height);	// Destroys contents
 
 	const Common::SharedPtr<Graphics::ManagedSurface> &getSurface() const;
 	const Graphics::PixelFormat &getPixelFormat() const;
 
+	void setStrata(int strata);
+	int getStrata() const;
+
+	// Mouse transparency = ignores mouse events
+	void setMouseTransparent(bool isTransparent);
+	bool isMouseTransparent() const;
+
 	void close();
 	void detachFromRuntime();
 
+	virtual void onMouseDown(int32 x, int32 y, int mouseButton);
+	virtual void onMouseMove(int32 x, int32 y);
+	virtual void onMouseUp(int32 x, int32 y, int mouseButton);
+
 private:
 	int32 _x;
 	int32 _y;
 	Common::SharedPtr<Graphics::ManagedSurface> _surface;
 	Runtime *_runtime;
+	int _strata;
+	bool _isMouseTransparent;
 };
 
 namespace Render {
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 3521a2d4b84..6c7a641f988 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -33,6 +33,7 @@
 #include "common/substream.h"
 #include "common/system.h"
 
+#include "graphics/cursorman.h"
 #include "graphics/managed_surface.h"
 #include "graphics/surface.h"
 #include "graphics/wincursor.h"
@@ -2111,12 +2112,56 @@ void Scheduler::removeEvent(const ScheduledEvent *evt) {
 	}
 }
 
+class DefaultCursor : public Graphics::Cursor {
+public:
+	uint16 getWidth() const override { return 16; }
+	uint16 getHeight() const override { return 16; }
+	uint16 getHotspotX() const override { return 1; }
+	uint16 getHotspotY() const override { return 2; }
+	byte getKeyColor() const override { return 0; }
+
+	const byte *getSurface() const override { return _cursorGraphic; }
+
+	const byte *getPalette() const override { return _cursorPalette; }
+	byte getPaletteStartIndex() const override { return 0; }
+	uint16 getPaletteCount() const override { return 3; }
+
+private:
+	static const byte _cursorGraphic[256];
+	static const byte _cursorPalette[9];
+};
+
+const byte DefaultCursor::_cursorGraphic[256] = {
+	1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	1, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	1, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	1, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	1, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	1, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0,
+	1, 2, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0,
+	1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0,
+	1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
+	1, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	1, 2, 2, 1, 1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0,
+	1, 2, 1, 0, 1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0,
+	1, 1, 0, 0, 0, 1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0,
+	1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
+};
+
+const byte DefaultCursor::_cursorPalette[9] = {
+	255, 0, 255,
+	0, 0, 0,
+	255, 255, 255
+};
+
 Runtime::SceneStackEntry::SceneStackEntry() {
 }
 
 Runtime::Runtime(OSystem *system) : _nextRuntimeGUID(1), _realDisplayMode(kColorDepthModeInvalid), _fakeDisplayMode(kColorDepthModeInvalid),
-									_displayWidth(1024), _displayHeight(768), _realTimeBase(0), _playTimeBase(0),
-									_sceneTransitionState(kSceneTransitionStateNotTransitioning), _system(system) {
+									_displayWidth(1024), _displayHeight(768), _realTimeBase(0), _playTimeBase(0), _sceneTransitionState(kSceneTransitionStateNotTransitioning),
+									_system(system), _lastFrameCursor(nullptr), _defaultCursor(new DefaultCursor()) {
 	_vthread.reset(new VThread());
 
 	for (int i = 0; i < kColorDepthModeCount; i++) {
@@ -2127,6 +2172,9 @@ Runtime::Runtime(OSystem *system) : _nextRuntimeGUID(1), _realDisplayMode(kColor
 
 	_realTimeBase = system->getMillis();
 	_playTimeBase = system->getMillis();
+
+	for (int i = 0; i < Actions::kMouseButtonCount; i++)
+		_mouseFocusFlags[Actions::kMouseButtonCount] = false;
 }
 
 bool Runtime::runFrame() {
@@ -2262,6 +2310,22 @@ bool Runtime::runFrame() {
 	return true;
 }
 
+struct WindowSortingBucket
+{
+	size_t originalIndex;
+	Window *window;
+
+	static bool sortPredicate(const WindowSortingBucket &a, const WindowSortingBucket &b) {
+		int aStrata = a.window->getStrata();
+		int bStrata = b.window->getStrata();
+
+		if (aStrata != bStrata)
+			return aStrata < bStrata;
+
+		return a.originalIndex < b.originalIndex;
+	}
+};
+
 void Runtime::drawFrame() {
 	int width = _system->getWidth();
 	int height = _system->getHeight();
@@ -2274,8 +2338,27 @@ void Runtime::drawFrame() {
 			Render::renderProject(this, mainWindow.get());
 	}
 
-	for (Common::Array<Common::SharedPtr<Window> >::const_iterator it = _windows.begin(), itEnd = _windows.end(); it != itEnd; ++it) {
-		const Window &window = *it->get();
+	const size_t numWindows = _windows.size();
+	WindowSortingBucket singleBucket;
+	Common::Array<WindowSortingBucket> multipleBuckets;
+	WindowSortingBucket *sortedBuckets = &singleBucket;
+
+	if (numWindows < 2)
+		sortedBuckets = &singleBucket;
+	else {
+		multipleBuckets.resize(numWindows);
+		sortedBuckets = &multipleBuckets[0];
+	}
+
+	for (size_t i = 0; i < numWindows; i++) {
+		sortedBuckets[i].originalIndex = i;
+		sortedBuckets[i].window = _windows[i].get();
+	}
+
+	Common::sort(sortedBuckets, sortedBuckets + numWindows, WindowSortingBucket::sortPredicate);
+	
+	for (size_t i = 0; i < numWindows; i++) {
+		const Window &window = *sortedBuckets[i].window;
 		const Graphics::ManagedSurface &surface = *window.getSurface();
 
 		int32 destLeft = window.getX();
@@ -2321,6 +2404,19 @@ void Runtime::drawFrame() {
 
 	_system->updateScreen();
 
+	Graphics::Cursor *cursor = nullptr;
+
+	// ...
+	if (cursor == nullptr)
+		cursor = _defaultCursor.get();
+
+	if (cursor != _lastFrameCursor) {
+		_lastFrameCursor = cursor;
+
+		CursorMan.showMouse(true);
+		CursorMan.replaceCursor(cursor);
+	}
+
 	_project->onPostRender();
 }
 
@@ -2697,6 +2793,73 @@ void Runtime::instantiateIfAlias(Common::SharedPtr<Modifier> &modifier, const Co
 	}
 }
 
+Common::SharedPtr<Window> Runtime::findTopWindow(int32 x, int32 y) const {
+	Common::SharedPtr<Window> bestWindow;
+	int bestStrata = 0;
+	for (const Common::SharedPtr<Window> &window : _windows) {
+		if ((!bestWindow || bestStrata <= window->getStrata()) && !window->isMouseTransparent() && x >= window->getX() && y >= window->getY()) {
+			int32 relX = x - window->getX();
+			int32 relY = y - window->getY();
+			if (relX < window->getWidth() && relY < window->getHeight()) {
+				bestStrata = window->getStrata();
+				bestWindow = window;
+			}
+		}
+	}
+
+	return bestWindow;
+}
+
+void Runtime::onMouseDown(int32 x, int32 y, Actions::MouseButton mButton) {
+	Common::SharedPtr<Window> focusWindow = _mouseFocusWindow.lock();
+	if (!focusWindow) {
+		focusWindow = findTopWindow(x, y);
+		if (!focusWindow)
+			return;
+
+		// New focus window, clear all leftover mouse button flags
+		for (int i = 0; i < Actions::kMouseButtonCount; i++)
+			_mouseFocusFlags[i] = false;
+
+		_mouseFocusWindow = focusWindow;
+	}
+
+	focusWindow->onMouseDown(x - focusWindow->getX(), y - focusWindow->getY(), mButton);
+
+	_mouseFocusFlags[mButton] = true;
+}
+
+void Runtime::onMouseMove(int32 x, int32 y) {
+	Common::SharedPtr<Window> focusWindow = _mouseFocusWindow.lock();
+	if (!focusWindow)
+		focusWindow = findTopWindow(x, y);
+
+	if (focusWindow)
+		focusWindow->onMouseMove(x - focusWindow->getX(), y - focusWindow->getY());
+
+	// TODO: Change mouse to focus window's cursor
+}
+
+void Runtime::onMouseUp(int32 x, int32 y, Actions::MouseButton mButton) {
+	Common::SharedPtr<Window> focusWindow = _mouseFocusWindow.lock();
+	if (!focusWindow)
+		return;
+
+	focusWindow->onMouseUp(x - focusWindow->getX(), y - focusWindow->getY(), mButton);
+	_mouseFocusFlags[mButton] = false;
+	bool anyTrue = false;
+	for (int i = 0; i < Actions::kMouseButtonCount; i++) {
+		if (_mouseFocusFlags[i]) {
+			anyTrue = true;
+			break;
+		}
+	}
+
+	if (!anyTrue)
+		_mouseFocusWindow.reset();
+}
+
+
 
 void Runtime::ensureMainWindowExists() {
 	// Maybe there's a better spot for this
@@ -2705,7 +2868,7 @@ void Runtime::ensureMainWindowExists() {
 
 		int32 centeredX = (static_cast<int32>(_displayWidth) - static_cast<int32>(presentationSettings.width)) / 2;
 		int32 centeredY = (static_cast<int32>(_displayHeight) - static_cast<int32>(presentationSettings.height)) / 2;
-		Common::SharedPtr<Window> mainWindow(new Window(this, centeredX, centeredY, presentationSettings.width, presentationSettings.height, _displayModePixelFormats[_realDisplayMode]));
+		Common::SharedPtr<Window> mainWindow(new Window(WindowParameters(this, centeredX, centeredY, presentationSettings.width, presentationSettings.height, _displayModePixelFormats[_realDisplayMode])));
 		addWindow(mainWindow);
 		_mainWindow.reset(mainWindow);
 	}
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 0fa596ec842..cf9e02f2f64 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -32,6 +32,7 @@
 
 #include "graphics/pixelformat.h"
 
+#include "mtropolis/actions.h"
 #include "mtropolis/data.h"
 #include "mtropolis/debug.h"
 #include "mtropolis/vthread.h"
@@ -1075,6 +1076,12 @@ public:
 
 	void instantiateIfAlias(Common::SharedPtr<Modifier> &modifier, const Common::WeakPtr<RuntimeObject> &relinkParent);
 
+	Common::SharedPtr<Window> findTopWindow(int32 x, int32 y) const;
+
+	void onMouseDown(int32 x, int32 y, Actions::MouseButton mButton);
+	void onMouseMove(int32 x, int32 y);
+	void onMouseUp(int32 x, int32 y, Actions::MouseButton mButton);
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	void debugSetEnabled(bool enabled);
 	void debugBreak();
@@ -1184,6 +1191,12 @@ private:
 	Scheduler _scheduler;
 	OSystem *_system;
 
+	Graphics::Cursor *_lastFrameCursor;
+	Common::SharedPtr<Graphics::Cursor> _defaultCursor;
+
+	Common::WeakPtr<Window> _mouseFocusWindow;
+	bool _mouseFocusFlags[Actions::kMouseButtonCount];
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	Common::SharedPtr<Debugger> _debugger;
 #endif


Commit: f44d85fd618b717cd658623a1f51b248823323ca
    https://github.com/scummvm/scummvm/commit/f44d85fd618b717cd658623a1f51b248823323ca
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Debug overlay work

Changed paths:
    engines/mtropolis/actions.h
    engines/mtropolis/debug.cpp
    engines/mtropolis/debug.h
    engines/mtropolis/mtropolis.h
    engines/mtropolis/runtime.cpp


diff --git a/engines/mtropolis/actions.h b/engines/mtropolis/actions.h
index 0af00a8df1a..14b39025aff 100644
--- a/engines/mtropolis/actions.h
+++ b/engines/mtropolis/actions.h
@@ -44,4 +44,4 @@ enum MouseButton {
 
 } // End of namespace MTropolis
 
-#endif
+#endif /* MTROPOLIS_ACTIONS_H */
diff --git a/engines/mtropolis/debug.cpp b/engines/mtropolis/debug.cpp
index 2881a669dfd..aaff725ca5e 100644
--- a/engines/mtropolis/debug.cpp
+++ b/engines/mtropolis/debug.cpp
@@ -27,6 +27,10 @@
 
 #include "graphics/fontman.h"
 
+#include "common/hash-ptr.h"
+
+
+
 namespace MTropolis {
 
 static const byte g_sceneTreeGraphic[] = {
@@ -105,6 +109,9 @@ class DebugToolWindowBase : public Window {
 public:
 	DebugToolWindowBase(DebuggerTool tool, const Common::String &title, Debugger *debugger, const WindowParameters &windowParams);
 
+	virtual void update() {}
+	void render();
+
 protected:
 	const int kTopBarHeight = 12;
 	const int kScrollBarWidth = 12;
@@ -118,13 +125,17 @@ protected:
 	virtual void toolOnMouseDown(int32 x, int32 y, int mouseButton) {}
 	virtual void toolOnMouseMove(int32 x, int32 y) {}
 	virtual void toolOnMouseUp(int32 x, int32 y, int mouseButton) {}
+	virtual void toolRenderSurface(int32 subAreaWidth, int32 subAreaHeight) {}
 
-	void refreshChrome();
+	void setDirty();
 
-	Debugger *_debugger;
 	Common::SharedPtr<Graphics::ManagedSurface> _toolSurface;
 
+	Debugger *_debugger;
+
 private:
+	void refreshChrome();
+
 	enum ToolWindowWidget {
 		kToolWindowWidgetNone,
 
@@ -142,14 +153,108 @@ private:
 	DebuggerTool _tool;
 
 	Common::String _title;
+	bool _isDirty;
+	int _scrollOffset;
 };
 
 DebugToolWindowBase::DebugToolWindowBase(DebuggerTool tool, const Common::String &title, Debugger *debugger, const WindowParameters &windowParams)
-	: Window(windowParams), _debugger(debugger), _tool(tool), _title(title), _activeWidget(kToolWindowWidgetNone), _isMouseCaptured(false) {
+	: Window(windowParams), _debugger(debugger), _tool(tool), _title(title), _activeWidget(kToolWindowWidgetNone), _isMouseCaptured(false), _isDirty(true), _scrollOffset(0) {
 
 	refreshChrome();
 }
 
+void DebugToolWindowBase::render() {
+	if (_isDirty) {
+		_isDirty = false;
+
+		bool needChromeUpdate = false;
+		int oldWidth = 0;
+		int oldHeight = 0;
+		if (_toolSurface) {
+			oldWidth = _toolSurface->w;
+			oldHeight = _toolSurface->h;
+			needChromeUpdate = true;
+		}
+
+		int32 renderWidth = getWidth() - kScrollBarWidth;
+		int32 renderHeight = getHeight() - kTopBarHeight;
+		toolRenderSurface(renderWidth, renderHeight);
+
+		if (_toolSurface && !needChromeUpdate) {
+			if (oldWidth != _toolSurface->w || oldHeight != _toolSurface->h)
+				needChromeUpdate = true;
+		}
+
+		if (needChromeUpdate)
+			refreshChrome();
+
+		if (_toolSurface) {
+			int32 contentsBottom = _toolSurface->h - _scrollOffset;
+			if (contentsBottom < renderHeight) {
+				_scrollOffset -= (renderHeight - contentsBottom);
+			}
+			if (_scrollOffset < 0)
+				_scrollOffset = 0;
+
+			int32 srcLeft = 0;
+			int32 srcTop = 0;
+			int32 srcRight = _toolSurface->w;
+			int32 srcBottom = _toolSurface->h;
+			int32 destLeft = 0;
+			int32 destRight = _toolSurface->w;
+			int32 destTop = -_scrollOffset;
+			int32 destBottom = _toolSurface->h - _scrollOffset;
+
+			if (srcTop < 0) {
+				int32 adjust = -srcTop;
+				destTop += adjust;
+				srcTop += adjust;
+			}
+			if (destTop < 0) {
+				int32 adjust = -destTop;
+				destTop += adjust;
+				srcTop += adjust;
+			}
+			if (srcBottom > _toolSurface->h) {
+				int32 adjust = srcBottom - _toolSurface->h;
+				destBottom += adjust;
+				srcBottom += adjust;
+			}
+			if (destBottom > renderHeight) {
+				int32 adjust = destBottom - renderHeight;
+				destBottom += adjust;
+				srcBottom += adjust;
+			}
+			if (srcLeft < 0) {
+				int32 adjust = -srcLeft;
+				destLeft += adjust;
+				srcLeft += adjust;
+			}
+			if (destLeft < 0) {
+				int32 adjust = -destLeft;
+				destLeft += adjust;
+				srcLeft += adjust;
+			}
+			if (srcRight > _toolSurface->w) {
+				int32 adjust = srcRight - _toolSurface->w;
+				destRight += adjust;
+				srcRight += adjust;
+			}
+			if (destRight > renderWidth) {
+				int32 adjust = destRight - renderWidth;
+				destRight += adjust;
+				srcRight += adjust;
+			}
+
+			if (srcLeft >= srcRight || srcTop >= srcBottom)
+				return;
+
+			getSurface()->fillRect(Common::Rect(0, kTopBarHeight, renderWidth, getHeight()), getSurface()->format.RGBToColor(255, 255, 255));
+			getSurface()->rawBlitFrom(*_toolSurface.get(), Common::Rect(srcLeft, srcTop, srcRight, srcBottom), Common::Point(destLeft, destTop + kTopBarHeight), nullptr);
+		}
+	}
+}
+
 void DebugToolWindowBase::onMouseDown(int32 x, int32 y, int mouseButton) {
 	if (mouseButton != Actions::kMouseButtonLeft)
 		return;
@@ -203,8 +308,9 @@ void DebugToolWindowBase::onMouseMove(int32 x, int32 y) {
 				newHeight = 100;
 
 			if (newWidth != getWidth() || newHeight != getHeight()) {
-				this->resizeWindow(newWidth, newHeight);
-				refreshChrome();
+				_toolSurface.reset();
+				resizeWindow(newWidth, newHeight);
+				_isDirty = true;
 			}
 		}
 	}
@@ -233,6 +339,10 @@ void DebugToolWindowBase::onMouseUp(int32 x, int32 y, int mouseButton) {
 	}
 }
 
+void DebugToolWindowBase::setDirty() {
+	_isDirty = true;
+}
+
 void DebugToolWindowBase::refreshChrome() {
 	Graphics::ManagedSurface *surface = getSurface().get();
 
@@ -275,6 +385,279 @@ void DebugToolWindowBase::refreshChrome() {
 	surface->drawThickLine(kCloseWidth - 4, 2, 2, kTopBarHeight - 4, 2, 2, whiteColor);
 }
 
+class DebugSceneTreeWindow : public DebugToolWindowBase {
+public:
+	DebugSceneTreeWindow(Debugger *debugger, const WindowParameters &windowParams);
+
+	void update() override;
+	void toolRenderSurface(int32 subAreaWidth, int32 subAreaHeight) override;
+
+	void toolOnMouseDown(int32 x, int32 y, int mouseButton) override;
+
+private:
+	static const int kRowHeight = 12;
+	static const int kBaseLeftPadding = 14;
+	static const int kExpanderLeftOffset = 8;
+	static const int kPerLevelSpacing = 14;
+
+	struct SceneTreeEntryUIState {
+		SceneTreeEntryUIState();
+
+		bool expanded;
+	};
+
+	struct SceneTreeEntry {
+		SceneTreeEntryUIState uiState;
+		size_t parentIndex;
+		int level;
+		bool hasChildren;
+		Common::WeakPtr<RuntimeObject> object;
+	};
+
+	struct RenderEntry {
+		size_t treeIndex;
+		size_t parentRenderIndex;
+	};
+
+	static void recursiveBuildTree(int level, size_t parentIndex, RuntimeObject *object, Common::Array<SceneTreeEntry> &tree);
+
+	Common::Array<SceneTreeEntry> _tree;
+	Common::Array<RenderEntry> _renderEntries;
+	bool _forceRender;
+};
+
+DebugSceneTreeWindow::SceneTreeEntryUIState::SceneTreeEntryUIState() : expanded(false) {
+}
+
+DebugSceneTreeWindow::DebugSceneTreeWindow(Debugger *debugger, const WindowParameters &windowParams)
+	: DebugToolWindowBase(kDebuggerToolSceneTree, "Project", debugger, windowParams), _forceRender(true) {
+}
+
+void DebugSceneTreeWindow::update() {
+	bool needRerender = _forceRender;
+
+	// This is super expensive but still less expensive than a redraw and we're only using it to debug,
+	// so kind of just eating the massive perf hit...
+	Common::HashMap<RuntimeObject *, SceneTreeEntryUIState> stateCache;
+	for (const SceneTreeEntry &treeEntry : _tree) {
+		Common::SharedPtr<RuntimeObject> obj = treeEntry.object.lock();
+		if (obj) {
+			stateCache[obj.get()] = treeEntry.uiState;
+		} else {
+			needRerender = true;
+			continue;
+		}
+	}
+
+	size_t oldSize = _tree.size();
+
+	// Keep existing reserve
+	_tree.resize(0);
+
+	Project *project = _debugger->getRuntime()->getProject();
+	if (project)
+		recursiveBuildTree(0, 0, project, _tree);
+
+	if (_tree.size() != oldSize)
+		needRerender = true;
+
+	for (SceneTreeEntry &treeEntry : _tree) {
+		Common::HashMap<RuntimeObject *, SceneTreeEntryUIState>::const_iterator oldStateIt = stateCache.find(treeEntry.object.lock().get());
+		if (oldStateIt != stateCache.end())
+			treeEntry.uiState = oldStateIt->_value;
+	}
+
+	if (needRerender) {
+		setDirty();
+		_renderEntries.clear();
+		_forceRender = false;
+	}
+}
+
+void DebugSceneTreeWindow::toolRenderSurface(int32 subAreaWidth, int32 subAreaHeight) {
+	Common::HashMap<const SceneTreeEntry *, size_t> treeToRenderIndex;
+	_renderEntries.clear();
+
+	treeToRenderIndex[&_tree[0]] = 0;
+
+	size_t lastParentIndex = _tree.size(); // So we can skip some hash map lookups, yuck
+	size_t lastParentRenderIndex = 0;
+	bool lastParentExpanded = true;
+
+	size_t numTreeNodes = _tree.size();
+	for (size_t i = 0; i < numTreeNodes; i++) {
+		const SceneTreeEntry &entry = _tree[i];
+		size_t parentIndex = entry.parentIndex;
+		size_t parentRenderIndex = 0;
+		bool isParentExpanded = false;
+		if (i == 0) {
+			isParentExpanded = true;
+			parentRenderIndex = 0;
+		} else if (parentIndex == lastParentIndex) {
+			isParentExpanded = lastParentExpanded;
+			parentRenderIndex = lastParentRenderIndex;
+		}else {
+			const SceneTreeEntry *parent = &_tree[entry.parentIndex];
+			if (parent->uiState.expanded) {
+				// Parent is expanded, figure out if it's actually rendered
+				Common::HashMap<const SceneTreeEntry *, size_t>::const_iterator t2r = treeToRenderIndex.find(parent);
+				if (t2r != treeToRenderIndex.end()) {
+					isParentExpanded = true;
+					parentRenderIndex = t2r->_value;
+				}
+			}
+
+			lastParentIndex = entry.parentIndex;
+			lastParentRenderIndex = parentRenderIndex;
+			lastParentExpanded = isParentExpanded;
+		}
+
+		if (isParentExpanded) {
+			treeToRenderIndex[&entry] = _renderEntries.size();
+
+			RenderEntry renderEntry;
+			renderEntry.treeIndex = i;
+			renderEntry.parentRenderIndex = parentRenderIndex;
+
+			_renderEntries.push_back(renderEntry);
+		}
+	}
+
+	Graphics::PixelFormat fmt = getSurface()->format;
+
+	int32 width = subAreaWidth;
+	int32 height = static_cast<int32>(_renderEntries.size()) * kRowHeight;
+	if (!_toolSurface || (height != _toolSurface->h || width != _toolSurface->w)) {
+		_toolSurface.reset();
+		_toolSurface.reset(new Graphics::ManagedSurface(subAreaWidth, height, fmt));
+	}
+
+	uint32 whiteColor = fmt.RGBToColor(255, 255, 255);
+	uint32 lightGrayColor = fmt.RGBToColor(192, 192, 192);
+	uint32 blackColor = fmt.RGBToColor(0, 0, 0);
+
+	const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
+
+	_toolSurface->fillRect(Common::Rect(0, 0, width, height), whiteColor);
+
+	for (size_t row = 0; row < _renderEntries.size(); row++) {
+		const RenderEntry &renderEntry = _renderEntries[row];
+		const SceneTreeEntry &entry = _tree[renderEntry.treeIndex];
+
+		Common::SharedPtr<RuntimeObject> obj = entry.object.lock();
+		if (!obj)
+			continue;	// ???
+
+		int32 y = static_cast<int32>(row) * kRowHeight + (kRowHeight - font->getFontAscent()) / 2;
+		int32 x = kBaseLeftPadding + kPerLevelSpacing * entry.level;
+
+		Common::String name;
+		if (obj->isModifier())
+			name = static_cast<const Modifier *>(obj.get())->getName();
+		else if (obj->isStructural())
+			name = static_cast<const Structural *>(obj.get())->getName();
+
+		font->drawString(_toolSurface.get(), name, x, y, width - x, blackColor, Graphics::kTextAlignLeft, 0, true);
+
+		if (entry.hasChildren) {
+			int32 expanderCenterX = x - kExpanderLeftOffset;
+			int32 expanderCenterY = static_cast<int32>(row) * kRowHeight + (kRowHeight / 2);
+			_toolSurface->frameRect(Common::Rect(expanderCenterX - 4, expanderCenterY - 4, expanderCenterX + 5, expanderCenterY + 5), blackColor);
+			_toolSurface->drawLine(expanderCenterX - 2, expanderCenterY, expanderCenterX + 2, expanderCenterY, blackColor);
+			if (!entry.uiState.expanded)
+				_toolSurface->drawLine(expanderCenterX, expanderCenterY - 2, expanderCenterX, expanderCenterY + 2, blackColor);
+		}
+	}
+
+	Common::Array<bool> haveRenderedParentTracers;
+	haveRenderedParentTracers.resize(_renderEntries.size());
+	for (size_t i = 0; i < _renderEntries.size(); i++)
+		haveRenderedParentTracers[i] = false;
+
+	for (size_t ri = 0; ri < _renderEntries.size(); ri++) {
+		size_t row = _renderEntries.size() - 1 - ri;
+		const RenderEntry &renderEntry = _renderEntries[row];
+
+		if (row == 0)
+			continue;
+
+		const RenderEntry &parentRenderEntry = _renderEntries[renderEntry.parentRenderIndex];
+		const SceneTreeEntry &treeEntry = _tree[renderEntry.treeIndex];
+		const SceneTreeEntry &parentTreeEntry = _tree[parentRenderEntry.treeIndex];
+
+		int32 x = kBaseLeftPadding + kPerLevelSpacing * treeEntry.level;
+
+		int32 parentTracerRightX = x - 2;
+		int32 parentTracerY = static_cast<int32>(row) * kRowHeight + (kRowHeight / 2);
+		if (treeEntry.hasChildren)
+			parentTracerRightX -= kExpanderLeftOffset + 7;
+
+		int32 parentTracerLeftX = kBaseLeftPadding + kPerLevelSpacing * parentTreeEntry.level - kExpanderLeftOffset;
+
+		_toolSurface->drawLine(parentTracerRightX, parentTracerY, parentTracerLeftX, parentTracerY, lightGrayColor);
+		if (!haveRenderedParentTracers[renderEntry.parentRenderIndex]) {
+			haveRenderedParentTracers[renderEntry.parentRenderIndex] = true;
+
+			int32 parentTracerTopY = static_cast<int32>(renderEntry.parentRenderIndex + 1) * kRowHeight;
+			_toolSurface->drawLine(parentTracerLeftX, parentTracerY, parentTracerLeftX, parentTracerTopY, lightGrayColor);
+		}
+	}
+}
+
+void DebugSceneTreeWindow::toolOnMouseDown(int32 x, int32 y, int mouseButton) {
+	if (mouseButton != Actions::kMouseButtonLeft)
+		return;
+
+	if (y < 0)
+		return;
+
+	int32 row = y / kRowHeight;
+
+	if (row >= _renderEntries.size())
+		return;
+
+	const RenderEntry &renderEntry = _renderEntries[row];
+	SceneTreeEntry &treeEntry = _tree[renderEntry.treeIndex];
+
+	int32 expanderCenterX = kBaseLeftPadding - kExpanderLeftOffset;
+	int32 expanderCenterY = row * kRowHeight + kRowHeight / 2;
+
+	if (x >= expanderCenterX - 5 && x <= expanderCenterX + 5 && y >= expanderCenterX - 5 && y <= expanderCenterY + 5) {
+		// Clicked the expander
+		treeEntry.uiState.expanded = !treeEntry.uiState.expanded;
+		_forceRender = true;
+		return;
+	}
+}
+
+void DebugSceneTreeWindow::recursiveBuildTree(int level, size_t parentIndex, RuntimeObject *object, Common::Array<SceneTreeEntry> &tree) {
+	SceneTreeEntry treeEntry;
+	treeEntry.level = level;
+	treeEntry.object = object->getSelfReference();
+	treeEntry.parentIndex = parentIndex;
+	treeEntry.hasChildren = false;
+
+	size_t thisIndex = tree.size();
+	tree.push_back(treeEntry);
+
+	if (object->isStructural()) {
+		Structural *structural = static_cast<Structural *>(object);
+		for (const Common::SharedPtr<Modifier> &modifier : structural->getModifiers())
+			recursiveBuildTree(level + 1, thisIndex, modifier.get(), tree);
+		for (const Common::SharedPtr<Structural> &child : structural->getChildren())
+			recursiveBuildTree(level + 1, thisIndex, child.get(), tree);
+	} else if (object->isModifier()) {
+		IModifierContainer *childContainer = static_cast<Modifier *>(object)->getChildContainer();
+		if (childContainer) {
+			for (const Common::SharedPtr<Modifier> &child : childContainer->getModifiers())
+				recursiveBuildTree(level + 1, thisIndex, child.get(), tree);
+		}
+	}
+
+	if (tree.size() - thisIndex > 1)
+		tree[thisIndex].hasChildren = true;
+}
+
 class DebugToolsWindow : public Window {
 public:
 	DebugToolsWindow(Debugger *debugger, const WindowParameters &windowParams);
@@ -377,6 +760,13 @@ void Debugger::runFrame(uint32 msec) {
 			}
 		}
 	}
+
+	for (const Common::SharedPtr<DebugToolWindowBase> &toolWindow : _toolWindows) {
+		if (toolWindow) {
+			toolWindow->update();
+			toolWindow->render();
+		}
+	}
 }
 
 void Debugger::setPaused(bool paused) {
@@ -387,6 +777,10 @@ bool Debugger::isPaused() const {
 	return _paused;
 }
 
+Runtime *Debugger::getRuntime() const {
+	return _runtime;
+}
+
 void Debugger::notify(DebugSeverity severity, const Common::String& str) {
 	const int toastNotificationHeight = 15;
 
@@ -523,13 +917,13 @@ void Debugger::openToolWindow(DebuggerTool tool) {
 	if (tool < 0 || tool >= kDebuggerToolCount)
 		return;	// This should never happen
 
-	Common::SharedPtr<Window> &windowRef = _toolWindows[tool];
+	Common::SharedPtr<DebugToolWindowBase> &windowRef = _toolWindows[tool];
 	if (windowRef)
 		return;
 
 	switch (tool) {
 	case kDebuggerToolSceneTree:
-		windowRef.reset(new DebugToolWindowBase(kDebuggerToolSceneTree, "SceneTree", this, WindowParameters(_runtime, 32, 32, 100, 320, _runtime->getRenderPixelFormat())));
+		windowRef.reset(new DebugSceneTreeWindow(this, WindowParameters(_runtime, 32, 32, 100, 320, _runtime->getRenderPixelFormat())));
 		break;
 	case kDebuggerToolInspector:
 		windowRef.reset(new DebugToolWindowBase(kDebuggerToolInspector, "Inspector", this, WindowParameters(_runtime, 32, 32, 100, 320, _runtime->getRenderPixelFormat())));
diff --git a/engines/mtropolis/debug.h b/engines/mtropolis/debug.h
index 8869d0a651b..f6a52f1817a 100644
--- a/engines/mtropolis/debug.h
+++ b/engines/mtropolis/debug.h
@@ -37,6 +37,7 @@ class Runtime;
 class Window;
 class Structural;
 class Modifier;
+class DebugToolWindowBase;
 
 struct IDebuggable;
 
@@ -88,6 +89,8 @@ public:
 	void setPaused(bool paused);
 	bool isPaused() const;
 
+	Runtime *getRuntime() const;
+
 	void notify(DebugSeverity severity, const Common::String &str);
 	void notifyFmt(DebugSeverity severity, const char *fmt, ...);
 	void vnotifyFmt(DebugSeverity severity, const char *fmt, va_list args);
@@ -114,7 +117,7 @@ private:
 	Runtime *_runtime;
 	Common::SharedPtr<Window> _sceneStatusWindow;
 	Common::SharedPtr<Window> _toolsWindow;
-	Common::SharedPtr<Window> _toolWindows[kDebuggerToolCount];
+	Common::SharedPtr<DebugToolWindowBase> _toolWindows[kDebuggerToolCount];
 	Common::Array<ToastNotification> _toastNotifications;
 };
 
diff --git a/engines/mtropolis/mtropolis.h b/engines/mtropolis/mtropolis.h
index 2677cf6de56..933e8febe8d 100644
--- a/engines/mtropolis/mtropolis.h
+++ b/engines/mtropolis/mtropolis.h
@@ -71,4 +71,4 @@ private:
 
 } // End of namespace MTropolis
 
-#endif /* MTROPOLIS_H */
+#endif /* MTROPOLIS_MTROPOLIS_H */
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 6c7a641f988..dfcacdf7617 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -3470,6 +3470,10 @@ void Project::loadBootStream(size_t streamIndex) {
 					if (_haveProjectStructuralDef)
 						error("Multiple project structural defs");
 
+					const Data::ProjectStructuralDef *def = static_cast<const Data::ProjectStructuralDef *>(dataObject.get());
+					_name = def->name;
+					_guid = def->guid;
+
 					_haveProjectStructuralDef = true;
 
 					ChildLoaderContext loaderContext;


Commit: 8acc9406391cd20b1efbc558eda29fa97bbdba9a
    https://github.com/scummvm/scummvm/commit/8acc9406391cd20b1efbc558eda29fa97bbdba9a
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add go to scene buttons to debugger scene tree view

Changed paths:
    engines/mtropolis/debug.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/debug.cpp b/engines/mtropolis/debug.cpp
index aaff725ca5e..b305c721c34 100644
--- a/engines/mtropolis/debug.cpp
+++ b/engines/mtropolis/debug.cpp
@@ -111,12 +111,14 @@ public:
 
 	virtual void update() {}
 	void render();
+	void trySetScrollOffset(int32 scrollOffset);
 
 protected:
 	const int kTopBarHeight = 12;
 	const int kScrollBarWidth = 12;
 	const int kCloseWidth = 12;
 	const int kResizeHeight = 12;
+	const int kMinScrollBarHandleSize = 20;
 
 	void onMouseDown(int32 x, int32 y, int mouseButton) override;
 	void onMouseMove(int32 x, int32 y) override;
@@ -135,12 +137,16 @@ protected:
 
 private:
 	void refreshChrome();
+	void cancelScrolling();
 
 	enum ToolWindowWidget {
 		kToolWindowWidgetNone,
 
 		kToolWindowWidgetClose,
-		kToolWindowWidgetScroll,
+		kToolWindowWidgetScrollEmpty,
+		kToolWindowWidgetScrollHandle,
+		kToolWindowWidgetScrollChannelUp,
+		kToolWindowWidgetScrollChannelDown,
 		kToolWindowWidgetResize,
 		kToolWindowWidgetMove,
 	};
@@ -155,10 +161,21 @@ private:
 	Common::String _title;
 	bool _isDirty;
 	int _scrollOffset;
+
+	int _scrollBarHandleSize;
+	int _scrollBarHandleOffset;
+	int _scrollBarHandleMaxOffset;
+	int _maxScrollOffset;
+	bool _haveScrollBar;
+	int32 _scrollStartOffset;
+
+	bool _havePreferredScrollOffset;
+	int32 _preferredScrollOffset;
 };
 
 DebugToolWindowBase::DebugToolWindowBase(DebuggerTool tool, const Common::String &title, Debugger *debugger, const WindowParameters &windowParams)
-	: Window(windowParams), _debugger(debugger), _tool(tool), _title(title), _activeWidget(kToolWindowWidgetNone), _isMouseCaptured(false), _isDirty(true), _scrollOffset(0) {
+	: Window(windowParams), _debugger(debugger), _tool(tool), _title(title), _activeWidget(kToolWindowWidgetNone), _isMouseCaptured(false),
+	  _isDirty(true), _scrollOffset(0), _haveScrollBar(false), _havePreferredScrollOffset(false) {
 
 	refreshChrome();
 }
@@ -185,10 +202,14 @@ void DebugToolWindowBase::render() {
 				needChromeUpdate = true;
 		}
 
-		if (needChromeUpdate)
-			refreshChrome();
-
 		if (_toolSurface) {
+			bool needToResetHandle = false;
+			if (_havePreferredScrollOffset) {
+				_scrollOffset = _preferredScrollOffset;
+				_havePreferredScrollOffset = false;
+				needToResetHandle = true;
+			}
+
 			int32 contentsBottom = _toolSurface->h - _scrollOffset;
 			if (contentsBottom < renderHeight) {
 				_scrollOffset -= (renderHeight - contentsBottom);
@@ -196,6 +217,23 @@ void DebugToolWindowBase::render() {
 			if (_scrollOffset < 0)
 				_scrollOffset = 0;
 
+			int surfaceHeight = _toolSurface->h;
+			if (surfaceHeight > renderHeight) {
+				int channelSpace = getHeight() - kTopBarHeight - kMinScrollBarHandleSize - kResizeHeight;
+
+				if (!_haveScrollBar || oldHeight != surfaceHeight || needToResetHandle) {
+					_maxScrollOffset = surfaceHeight - renderHeight;
+					_scrollBarHandleSize = renderHeight * channelSpace / surfaceHeight + kMinScrollBarHandleSize;
+					_scrollBarHandleMaxOffset = (channelSpace + kMinScrollBarHandleSize - _scrollBarHandleSize);
+					_scrollBarHandleOffset = _scrollOffset * _scrollBarHandleMaxOffset / _maxScrollOffset;
+				}
+
+				_haveScrollBar = true;
+			} else {
+				_haveScrollBar = false;
+				cancelScrolling();
+			}
+
 			int32 srcLeft = 0;
 			int32 srcTop = 0;
 			int32 srcRight = _toolSurface->w;
@@ -251,10 +289,22 @@ void DebugToolWindowBase::render() {
 
 			getSurface()->fillRect(Common::Rect(0, kTopBarHeight, renderWidth, getHeight()), getSurface()->format.RGBToColor(255, 255, 255));
 			getSurface()->rawBlitFrom(*_toolSurface.get(), Common::Rect(srcLeft, srcTop, srcRight, srcBottom), Common::Point(destLeft, destTop + kTopBarHeight), nullptr);
+		} else {
+			_haveScrollBar = false;
+			cancelScrolling();
 		}
+
+		if (needChromeUpdate)
+			refreshChrome();
 	}
 }
 
+void DebugToolWindowBase::trySetScrollOffset(int32 scrollOffset) {
+	_havePreferredScrollOffset = true;
+	_preferredScrollOffset = scrollOffset;
+}
+
+
 void DebugToolWindowBase::onMouseDown(int32 x, int32 y, int mouseButton) {
 	if (mouseButton != Actions::kMouseButtonLeft)
 		return;
@@ -279,18 +329,32 @@ void DebugToolWindowBase::onMouseDown(int32 x, int32 y, int mouseButton) {
 			_activeWidget = kToolWindowWidgetResize;
 			_resizeStartWidth = getWidth();
 			_resizeStartHeight = getHeight();
+		} else {
+			if (_haveScrollBar) {
+				int32 relativeToScrollHandle = y - kTopBarHeight - _scrollBarHandleOffset;
+				if (relativeToScrollHandle < 0)
+					_activeWidget = kToolWindowWidgetScrollChannelUp;
+				else if (relativeToScrollHandle >= _scrollBarHandleSize)
+					_activeWidget = kToolWindowWidgetScrollChannelDown;
+				else {
+					_activeWidget = kToolWindowWidgetScrollHandle;
+					_scrollStartOffset = _scrollBarHandleOffset;
+				}
+
+				setDirty();
+			} else {
+				_activeWidget = kToolWindowWidgetScrollEmpty;
+			}
 		}
-		else
-			_activeWidget = kToolWindowWidgetScroll;
 	} else {
 		_activeWidget = kToolWindowWidgetNone;
-		toolOnMouseDown(x, y - kTopBarHeight, mouseButton);
+		toolOnMouseDown(x, y - kTopBarHeight + _scrollOffset, mouseButton);
 	}
 }
 
 void DebugToolWindowBase::onMouseMove(int32 x, int32 y) {
 	if (_activeWidget == kToolWindowWidgetNone)
-		toolOnMouseMove(x, y - kTopBarHeight);
+		toolOnMouseMove(x, y - kTopBarHeight + _scrollOffset);
 	else {
 		if (_activeWidget == kToolWindowWidgetMove) {
 			int32 relX = x - _dragStartX;
@@ -310,7 +374,20 @@ void DebugToolWindowBase::onMouseMove(int32 x, int32 y) {
 			if (newWidth != getWidth() || newHeight != getHeight()) {
 				_toolSurface.reset();
 				resizeWindow(newWidth, newHeight);
-				_isDirty = true;
+				setDirty();
+			}
+		} else if (_activeWidget == kToolWindowWidgetScrollHandle) {
+			int32 desiredOffset = y - _dragStartY + _scrollStartOffset;
+			if (desiredOffset < 0)
+				desiredOffset = 0;
+			else if (desiredOffset > _scrollBarHandleMaxOffset)
+				desiredOffset = _scrollBarHandleMaxOffset;
+
+			// Try to avoid scrolling unnecessarily, especially with zero offset
+			if (desiredOffset != _scrollBarHandleOffset) {
+				_scrollBarHandleOffset = desiredOffset;
+				_scrollOffset = _scrollBarHandleOffset * _maxScrollOffset / _scrollBarHandleMaxOffset;
+				setDirty();
 			}
 		}
 	}
@@ -326,7 +403,7 @@ void DebugToolWindowBase::onMouseUp(int32 x, int32 y, int mouseButton) {
 	_isMouseCaptured = false;
 
 	if (_activeWidget == kToolWindowWidgetNone)
-		toolOnMouseUp(x, y - kTopBarHeight, mouseButton);
+		toolOnMouseUp(x, y - kTopBarHeight + _scrollOffset, mouseButton);
 	else {
 		if (_activeWidget == kToolWindowWidgetClose) {
 			if (x < kCloseWidth && y < kTopBarHeight) {
@@ -334,6 +411,9 @@ void DebugToolWindowBase::onMouseUp(int32 x, int32 y, int mouseButton) {
 				return;
 			}
 		}
+		if (_activeWidget == kToolWindowWidgetScrollHandle) {
+			setDirty();
+		}
 
 		_activeWidget = kToolWindowWidgetNone;
 	}
@@ -343,6 +423,11 @@ void DebugToolWindowBase::setDirty() {
 	_isDirty = true;
 }
 
+void DebugToolWindowBase::cancelScrolling() {
+	if (_activeWidget == kToolWindowWidgetScrollChannelDown || _activeWidget == kToolWindowWidgetScrollChannelUp || _activeWidget == kToolWindowWidgetScrollHandle)
+		_activeWidget = kToolWindowWidgetNone;
+}
+
 void DebugToolWindowBase::refreshChrome() {
 	Graphics::ManagedSurface *surface = getSurface().get();
 
@@ -355,7 +440,9 @@ void DebugToolWindowBase::refreshChrome() {
 	uint32 topBarColor = fmt.RGBToColor(192, 192, 192);
 	uint32 topTextColor = blackColor;
 
-	uint32 inactiveScrollColor = fmt.RGBToColor(225, 225, 225);
+	uint32 scrollBarChannelColor = fmt.RGBToColor(225, 225, 225);
+	uint32 scrollBarHandleInactiveColor = fmt.RGBToColor(160, 160, 160);
+	uint32 scrollBarHandleActiveColor = fmt.RGBToColor(128, 128, 128);
 
 	int width = surface->w;
 	int height = surface->h;
@@ -379,7 +466,16 @@ void DebugToolWindowBase::refreshChrome() {
 
 	font->drawString(surface, _title, kCloseWidth, titleY, titleAvailableWidth, topTextColor, Graphics::kTextAlignCenter, 0, true);
 
-	surface->fillRect(Common::Rect(width - kScrollBarWidth, kTopBarHeight, width, height - kResizeHeight), inactiveScrollColor);
+	surface->fillRect(Common::Rect(width - kScrollBarWidth, kTopBarHeight, width, height - kResizeHeight), scrollBarChannelColor);
+
+	if (_haveScrollBar) {
+		uint32 scrollHandleColor = scrollBarHandleInactiveColor;
+		if (_activeWidget == kToolWindowWidgetScrollHandle)
+			scrollHandleColor = scrollBarHandleActiveColor;
+
+		surface->fillRect(Common::Rect(width - kScrollBarWidth, kTopBarHeight + _scrollBarHandleOffset, width, kTopBarHeight + _scrollBarHandleOffset + _scrollBarHandleSize), scrollHandleColor);
+	}
+
 	surface->fillRect(Common::Rect(0, 0, kCloseWidth, kTopBarHeight), closeColor);
 	surface->drawThickLine(2, 2, kCloseWidth - 4, kTopBarHeight - 4, 2, 2, whiteColor);
 	surface->drawThickLine(kCloseWidth - 4, 2, 2, kTopBarHeight - 4, 2, 2, whiteColor);
@@ -399,11 +495,20 @@ private:
 	static const int kBaseLeftPadding = 14;
 	static const int kExpanderLeftOffset = 8;
 	static const int kPerLevelSpacing = 14;
+	static const int kTypeIndicatorPadding = 12;
+	static const int kSceneStackBaseHeight = 18;
+	static const int kSceneStackRowHeight = 14;
+	static const int kSceneStackGoToButtonWidth = 36;
+	static const int kSceneStackGoToButtonFirstY = 15;
+	static const int kSceneStackGoToButtonHeight = 12;
+	static const int kSceneStackGoToButtonX = 2;
+	
 
 	struct SceneTreeEntryUIState {
 		SceneTreeEntryUIState();
 
 		bool expanded;
+		bool selected;
 	};
 
 	struct SceneTreeEntry {
@@ -421,21 +526,64 @@ private:
 
 	static void recursiveBuildTree(int level, size_t parentIndex, RuntimeObject *object, Common::Array<SceneTreeEntry> &tree);
 
+	static uint32 getColorForObject(const RuntimeObject *object, const Graphics::PixelFormat &fmt);
+
+	int32 _treeYOffset;
 	Common::Array<SceneTreeEntry> _tree;
 	Common::Array<RenderEntry> _renderEntries;
+	Common::Array<Common::WeakPtr<Structural> > _sceneStack;
+	Common::WeakPtr<Structural> _mainScene;
+	Common::WeakPtr<Structural> _sharedScene;
+	Common::WeakPtr<RuntimeObject> _latentScrollTo;
 	bool _forceRender;
 };
 
-DebugSceneTreeWindow::SceneTreeEntryUIState::SceneTreeEntryUIState() : expanded(false) {
+DebugSceneTreeWindow::SceneTreeEntryUIState::SceneTreeEntryUIState() : expanded(false), selected(false) {
 }
 
 DebugSceneTreeWindow::DebugSceneTreeWindow(Debugger *debugger, const WindowParameters &windowParams)
-	: DebugToolWindowBase(kDebuggerToolSceneTree, "Project", debugger, windowParams), _forceRender(true) {
+	: DebugToolWindowBase(kDebuggerToolSceneTree, "Project", debugger, windowParams), _forceRender(true), _treeYOffset(20) {
 }
 
 void DebugSceneTreeWindow::update() {
 	bool needRerender = _forceRender;
 
+	Runtime *runtime = _debugger->getRuntime();
+
+	Common::Array<Common::SharedPtr<Structural> > newSceneStack;
+	runtime->getSceneStack(newSceneStack);
+
+	bool sceneStackChanged = false;
+	if (newSceneStack.size() != _sceneStack.size())
+		sceneStackChanged = true;
+	else {
+		for (size_t i = 0; i < newSceneStack.size(); i++) {
+			if (newSceneStack[i] != _sceneStack[i].lock()) {
+				sceneStackChanged = true;
+				break;
+			}
+		}
+	}
+
+	if (sceneStackChanged) {
+		needRerender = true;
+		_sceneStack.clear();
+		for (size_t i = 0; i < newSceneStack.size(); i++)
+			_sceneStack.push_back(newSceneStack[i]);
+	}
+	
+	Common::SharedPtr<Structural> sharedScene = runtime->getActiveSharedScene();
+	Common::SharedPtr<Structural> mainScene = runtime->getActiveMainScene();
+
+	if (_sharedScene != Common::WeakPtr<Structural>(sharedScene)) {
+		_sharedScene = sharedScene;
+		needRerender = true;
+	}
+	if (_mainScene != Common::WeakPtr<Structural>(mainScene)) {
+		_mainScene = mainScene;
+		needRerender = true;
+	}
+
 	// This is super expensive but still less expensive than a redraw and we're only using it to debug,
 	// so kind of just eating the massive perf hit...
 	Common::HashMap<RuntimeObject *, SceneTreeEntryUIState> stateCache;
@@ -454,10 +602,12 @@ void DebugSceneTreeWindow::update() {
 	// Keep existing reserve
 	_tree.resize(0);
 
+	// Generate the tree
 	Project *project = _debugger->getRuntime()->getProject();
 	if (project)
 		recursiveBuildTree(0, 0, project, _tree);
 
+	// If the tree changed, we need to re-render
 	if (_tree.size() != oldSize)
 		needRerender = true;
 
@@ -467,6 +617,22 @@ void DebugSceneTreeWindow::update() {
 			treeEntry.uiState = oldStateIt->_value;
 	}
 
+	if (!_latentScrollTo.expired()) {
+		for (SceneTreeEntry &treeEntry : _tree) {
+			if (treeEntry.object == _latentScrollTo) {
+				size_t parentIndex = treeEntry.parentIndex;
+				do {
+					_tree[parentIndex].uiState.expanded = true;
+					parentIndex = _tree[parentIndex].parentIndex;
+				} while (parentIndex != 0);
+				_tree[0].uiState.expanded = true;
+				break;
+			}
+		}
+
+		needRerender = true;
+	}
+
 	if (needRerender) {
 		setDirty();
 		_renderEntries.clear();
@@ -475,6 +641,7 @@ void DebugSceneTreeWindow::update() {
 }
 
 void DebugSceneTreeWindow::toolRenderSurface(int32 subAreaWidth, int32 subAreaHeight) {
+	// Render tree
 	Common::HashMap<const SceneTreeEntry *, size_t> treeToRenderIndex;
 	_renderEntries.clear();
 
@@ -496,7 +663,7 @@ void DebugSceneTreeWindow::toolRenderSurface(int32 subAreaWidth, int32 subAreaHe
 		} else if (parentIndex == lastParentIndex) {
 			isParentExpanded = lastParentExpanded;
 			parentRenderIndex = lastParentRenderIndex;
-		}else {
+		} else {
 			const SceneTreeEntry *parent = &_tree[entry.parentIndex];
 			if (parent->uiState.expanded) {
 				// Parent is expanded, figure out if it's actually rendered
@@ -523,10 +690,13 @@ void DebugSceneTreeWindow::toolRenderSurface(int32 subAreaWidth, int32 subAreaHe
 		}
 	}
 
+	// Draw
+	_treeYOffset = kSceneStackBaseHeight + kSceneStackRowHeight * _sceneStack.size();
+
 	Graphics::PixelFormat fmt = getSurface()->format;
 
 	int32 width = subAreaWidth;
-	int32 height = static_cast<int32>(_renderEntries.size()) * kRowHeight;
+	int32 height = static_cast<int32>(_renderEntries.size()) * kRowHeight + _treeYOffset;
 	if (!_toolSurface || (height != _toolSurface->h || width != _toolSurface->w)) {
 		_toolSurface.reset();
 		_toolSurface.reset(new Graphics::ManagedSurface(subAreaWidth, height, fmt));
@@ -540,6 +710,29 @@ void DebugSceneTreeWindow::toolRenderSurface(int32 subAreaWidth, int32 subAreaHe
 
 	_toolSurface->fillRect(Common::Rect(0, 0, width, height), whiteColor);
 
+	// Draw scene stack
+	font->drawString(_toolSurface.get(), "Scene stack:", 2, 2, width, blackColor);
+	for (size_t i = 0; i < _sceneStack.size(); i++) {
+		Common::SharedPtr<Structural> structural = _sceneStack[i].lock();
+		if (!structural)
+			continue;
+
+		Common::String str = structural->getName();
+		if (structural == _mainScene.lock())
+			str += " (Main)";
+		if (structural == _sharedScene.lock())
+			str += " (Shared)";
+
+		font->drawString(_toolSurface.get(), str, kSceneStackGoToButtonWidth + 4, 16 + i * kSceneStackRowHeight, width, blackColor);
+
+		int buttonX = kSceneStackGoToButtonX;
+		int buttonY = kSceneStackGoToButtonFirstY + i * kSceneStackRowHeight;
+
+		_toolSurface->frameRect(Common::Rect(buttonX, buttonY, buttonX + kSceneStackGoToButtonWidth, buttonY + kSceneStackGoToButtonHeight), blackColor);
+		font->drawString(_toolSurface.get(), "Go To", buttonX + 1, buttonY + 1, kSceneStackGoToButtonWidth - 2, blackColor, Graphics::kTextAlignCenter);
+	}
+
+	// Draw tree
 	for (size_t row = 0; row < _renderEntries.size(); row++) {
 		const RenderEntry &renderEntry = _renderEntries[row];
 		const SceneTreeEntry &entry = _tree[renderEntry.treeIndex];
@@ -548,7 +741,9 @@ void DebugSceneTreeWindow::toolRenderSurface(int32 subAreaWidth, int32 subAreaHe
 		if (!obj)
 			continue;	// ???
 
-		int32 y = static_cast<int32>(row) * kRowHeight + (kRowHeight - font->getFontAscent()) / 2;
+		int32 rowTopY = static_cast<int32>(row) * kRowHeight + _treeYOffset;
+		int32 rowBottomY = rowTopY + kRowHeight;
+		int32 y = rowTopY + (kRowHeight - font->getFontAscent()) / 2;
 		int32 x = kBaseLeftPadding + kPerLevelSpacing * entry.level;
 
 		Common::String name;
@@ -557,11 +752,26 @@ void DebugSceneTreeWindow::toolRenderSurface(int32 subAreaWidth, int32 subAreaHe
 		else if (obj->isStructural())
 			name = static_cast<const Structural *>(obj.get())->getName();
 
-		font->drawString(_toolSurface.get(), name, x, y, width - x, blackColor, Graphics::kTextAlignLeft, 0, true);
+		uint32 objIndicatorColor = getColorForObject(obj.get(), fmt);
+		{
+			int32 indicatorCenterX = x + kTypeIndicatorPadding / 2;
+			int32 indicatorCenterY = rowTopY + (kRowHeight / 2);
+			_toolSurface->fillRect(Common::Rect(indicatorCenterX - 4, indicatorCenterY - 4, indicatorCenterX + 4, indicatorCenterY + 4), objIndicatorColor);
+		}
+
+		int textX = x + kTypeIndicatorPadding;
+		int strWidth = font->getStringWidth(name);
+		if (strWidth > width - textX)
+			strWidth = width - textX;
+
+		bool isSelected = entry.uiState.selected;
+		if (isSelected)
+			_toolSurface->fillRect(Common::Rect(textX - 1, rowTopY, textX + strWidth + 1, rowBottomY), blackColor);
+		font->drawString(_toolSurface.get(), name, textX, y, strWidth, isSelected ? whiteColor : blackColor, Graphics::kTextAlignLeft, 0, true);
 
 		if (entry.hasChildren) {
 			int32 expanderCenterX = x - kExpanderLeftOffset;
-			int32 expanderCenterY = static_cast<int32>(row) * kRowHeight + (kRowHeight / 2);
+			int32 expanderCenterY = rowTopY + (kRowHeight / 2);
 			_toolSurface->frameRect(Common::Rect(expanderCenterX - 4, expanderCenterY - 4, expanderCenterX + 5, expanderCenterY + 5), blackColor);
 			_toolSurface->drawLine(expanderCenterX - 2, expanderCenterY, expanderCenterX + 2, expanderCenterY, blackColor);
 			if (!entry.uiState.expanded)
@@ -585,10 +795,12 @@ void DebugSceneTreeWindow::toolRenderSurface(int32 subAreaWidth, int32 subAreaHe
 		const SceneTreeEntry &treeEntry = _tree[renderEntry.treeIndex];
 		const SceneTreeEntry &parentTreeEntry = _tree[parentRenderEntry.treeIndex];
 
+		int32 rowTopY = static_cast<int32>(row) * kRowHeight + _treeYOffset;
+
 		int32 x = kBaseLeftPadding + kPerLevelSpacing * treeEntry.level;
 
 		int32 parentTracerRightX = x - 2;
-		int32 parentTracerY = static_cast<int32>(row) * kRowHeight + (kRowHeight / 2);
+		int32 parentTracerY = rowTopY + (kRowHeight / 2);
 		if (treeEntry.hasChildren)
 			parentTracerRightX -= kExpanderLeftOffset + 7;
 
@@ -598,20 +810,38 @@ void DebugSceneTreeWindow::toolRenderSurface(int32 subAreaWidth, int32 subAreaHe
 		if (!haveRenderedParentTracers[renderEntry.parentRenderIndex]) {
 			haveRenderedParentTracers[renderEntry.parentRenderIndex] = true;
 
-			int32 parentTracerTopY = static_cast<int32>(renderEntry.parentRenderIndex + 1) * kRowHeight;
+			int32 parentTracerTopY = static_cast<int32>(renderEntry.parentRenderIndex + 1) * kRowHeight + _treeYOffset;
 			_toolSurface->drawLine(parentTracerLeftX, parentTracerY, parentTracerLeftX, parentTracerTopY, lightGrayColor);
 		}
+
+		if (treeEntry.object == _latentScrollTo)
+			trySetScrollOffset(rowTopY);
 	}
+
+	_latentScrollTo.reset();
 }
 
 void DebugSceneTreeWindow::toolOnMouseDown(int32 x, int32 y, int mouseButton) {
 	if (mouseButton != Actions::kMouseButtonLeft)
 		return;
 
-	if (y < 0)
+	for (int i = 0; i < _sceneStack.size(); i++) {
+		int buttonLeft = kSceneStackGoToButtonX;
+		int buttonRight = buttonLeft + kSceneStackGoToButtonWidth;
+		int buttonTop = kSceneStackGoToButtonFirstY + i * kSceneStackRowHeight;
+		int buttonBottom = buttonTop + kSceneStackGoToButtonHeight;
+
+		if (x >= buttonLeft && x < buttonRight && y >= buttonTop && y < buttonBottom) {
+			_forceRender = true;
+			_latentScrollTo = _sceneStack[i];
+			return;
+		}
+	}
+
+	if (y < _treeYOffset)
 		return;
 
-	int32 row = y / kRowHeight;
+	int32 row = (y - _treeYOffset) / kRowHeight;
 
 	if (row >= _renderEntries.size())
 		return;
@@ -619,8 +849,8 @@ void DebugSceneTreeWindow::toolOnMouseDown(int32 x, int32 y, int mouseButton) {
 	const RenderEntry &renderEntry = _renderEntries[row];
 	SceneTreeEntry &treeEntry = _tree[renderEntry.treeIndex];
 
-	int32 expanderCenterX = kBaseLeftPadding - kExpanderLeftOffset;
-	int32 expanderCenterY = row * kRowHeight + kRowHeight / 2;
+	int32 expanderCenterX = kBaseLeftPadding - kExpanderLeftOffset + treeEntry.level * kPerLevelSpacing;
+	int32 expanderCenterY = row * kRowHeight + kRowHeight / 2 + _treeYOffset;
 
 	if (x >= expanderCenterX - 5 && x <= expanderCenterX + 5 && y >= expanderCenterX - 5 && y <= expanderCenterY + 5) {
 		// Clicked the expander
@@ -658,6 +888,26 @@ void DebugSceneTreeWindow::recursiveBuildTree(int level, size_t parentIndex, Run
 		tree[thisIndex].hasChildren = true;
 }
 
+uint32 DebugSceneTreeWindow::getColorForObject(const RuntimeObject *object, const Graphics::PixelFormat &fmt) {
+	if (object->isStructural()) {
+		return fmt.RGBToColor(128, 128, 128);
+	} else if (object->isModifier()) {
+		const Modifier *mod = static_cast<const Modifier *>(object);
+		if (mod->isAlias())
+			return fmt.RGBToColor(255, 0, 255);
+		else if (mod->isVariable())
+			return fmt.RGBToColor(0, 0, 255);
+		else if (mod->isBehavior())
+			return fmt.RGBToColor(196, 0, 208);
+		else if (mod->isCompoundVariable())
+			return fmt.RGBToColor(100, 100, 200);
+		else
+			return fmt.RGBToColor(0, 196, 128);
+	} else {
+		return fmt.RGBToColor(0, 0, 0);
+	}
+}
+
 class DebugToolsWindow : public Window {
 public:
 	DebugToolsWindow(Debugger *debugger, const WindowParameters &windowParams);
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index a55ef0ef460..23abff26048 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -36,6 +36,8 @@ class BehaviorModifier : public Modifier, public IModifierContainer {
 public:
 	bool load(ModifierLoaderContext &context, const Data::BehaviorModifier &data);
 
+	bool isBehavior() const override { return true; }
+
 	const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const override;
 	void appendModifier(const Common::SharedPtr<Modifier> &modifier) override;
 
@@ -558,6 +560,9 @@ public:
 
 	IModifierContainer *getChildContainer() override;
 
+	bool isCompoundVariable() const override { return true; }
+
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Compound Variable Modifier"; }
 	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index ca0db128350..0c5afba9720 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -217,7 +217,6 @@ MacObsidianResources::~MacObsidianResources() {
 }
 
 MTropolisEngine::MTropolisEngine(OSystem *syst, const MTropolisGameDescription *gameDesc) : Engine(syst), _gameDescription(gameDesc) {
-
 	if (gameDesc->gameID == GID_OBSIDIAN && _gameDescription->desc.platform == Common::kPlatformWindows) {
 		const Common::FSNode gameDataDir(ConfMan.get("path"));
 		SearchMan.addSubDirectoryMatching(gameDataDir, "Obsidian");
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index dfcacdf7617..8b317a38129 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -2523,7 +2523,7 @@ void Runtime::executeCompleteTransitionToScene(const Common::SharedPtr<Structura
 		queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneStarted, 0), targetSharedScene.get(), false, true);
 
 		SceneStackEntry sharedSceneEntry;
-		sharedSceneEntry.scene = targetScene;
+		sharedSceneEntry.scene = targetSharedScene;
 
 		_sceneStack[0] = sharedSceneEntry;
 	}
@@ -3003,6 +3003,12 @@ const Common::SharedPtr<Structural> &Runtime::getActiveSharedScene() const {
 	return _activeSharedScene;
 }
 
+void Runtime::getSceneStack(Common::Array<Common::SharedPtr<Structural> >& sceneStack) const {
+	sceneStack.clear();
+	for (const SceneStackEntry &stackEntry : _sceneStack)
+		sceneStack.push_back(stackEntry.scene);
+}
+
 bool Runtime::mustDraw() const {
 	if (_sceneTransitionState == kSceneTransitionStateWaitingForDraw)
 		return true;
@@ -4068,6 +4074,14 @@ bool Modifier::isVariable() const {
 	return false;
 }
 
+bool Modifier::isBehavior() const {
+	return false;
+}
+
+bool Modifier::isCompoundVariable() const {
+	return false;
+}
+
 bool Modifier::isModifier() const {
 	return true;
 }
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index cf9e02f2f64..a65cfadb829 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -1058,6 +1058,7 @@ public:
 
 	const Common::SharedPtr<Structural> &getActiveMainScene() const;
 	const Common::SharedPtr<Structural> &getActiveSharedScene() const;
+	void getSceneStack(Common::Array<Common::SharedPtr<Structural> > &sceneStack) const;
 
 	bool mustDraw() const;
 
@@ -1671,6 +1672,8 @@ public:
 
 	virtual bool isAlias() const;
 	virtual bool isVariable() const;
+	virtual bool isBehavior() const;
+	virtual bool isCompoundVariable() const;
 	bool isModifier() const override;
 
 	// This should only return a propagation container if messages should actually be propagated (i.e. NOT switched-off behaviors!)


Commit: 55e0290adf5c7d123cf9daecdce381093eb48aca
    https://github.com/scummvm/scummvm/commit/55e0290adf5c7d123cf9daecdce381093eb48aca
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Update pointer API

Changed paths:
    engines/mtropolis/debug.cpp
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/debug.cpp b/engines/mtropolis/debug.cpp
index b305c721c34..57b475479ca 100644
--- a/engines/mtropolis/debug.cpp
+++ b/engines/mtropolis/debug.cpp
@@ -575,11 +575,11 @@ void DebugSceneTreeWindow::update() {
 	Common::SharedPtr<Structural> sharedScene = runtime->getActiveSharedScene();
 	Common::SharedPtr<Structural> mainScene = runtime->getActiveMainScene();
 
-	if (_sharedScene != Common::WeakPtr<Structural>(sharedScene)) {
+	if (_sharedScene.lock() != sharedScene) {
 		_sharedScene = sharedScene;
 		needRerender = true;
 	}
-	if (_mainScene != Common::WeakPtr<Structural>(mainScene)) {
+	if (_mainScene.lock() != mainScene) {
 		_mainScene = mainScene;
 		needRerender = true;
 	}
@@ -618,8 +618,9 @@ void DebugSceneTreeWindow::update() {
 	}
 
 	if (!_latentScrollTo.expired()) {
+		Common::SharedPtr<RuntimeObject> scrollToTarget = _latentScrollTo.lock();
 		for (SceneTreeEntry &treeEntry : _tree) {
-			if (treeEntry.object == _latentScrollTo) {
+			if (treeEntry.object.lock() == scrollToTarget) {
 				size_t parentIndex = treeEntry.parentIndex;
 				do {
 					_tree[parentIndex].uiState.expanded = true;
@@ -814,8 +815,11 @@ void DebugSceneTreeWindow::toolRenderSurface(int32 subAreaWidth, int32 subAreaHe
 			_toolSurface->drawLine(parentTracerLeftX, parentTracerY, parentTracerLeftX, parentTracerTopY, lightGrayColor);
 		}
 
-		if (treeEntry.object == _latentScrollTo)
-			trySetScrollOffset(rowTopY);
+		if (!_latentScrollTo.expired()) {
+			Common::SharedPtr<RuntimeObject> scrollToTarget = _latentScrollTo.lock();
+			if (treeEntry.object.lock() == scrollToTarget)
+				trySetScrollOffset(rowTopY);
+		}
 	}
 
 	_latentScrollTo.reset();
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 01954cdbc00..823c02487da 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -481,7 +481,7 @@ MiniscriptInstructionOutcome Set::execute(MiniscriptThread *thread) const {
 	} else {
 		VariableModifier *var = nullptr;
 		if (target.value.getType() == DynamicValueTypes::kObject) {
-			Common::SharedPtr<RuntimeObject> obj = target.value.getObject().lock();
+			Common::SharedPtr<RuntimeObject> obj = target.value.getObject().object.lock();
 			if (obj && obj->isModifier() && static_cast<const Modifier *>(obj.get())->isVariable())
 				var = static_cast<VariableModifier *>(obj.get());
 		}
@@ -526,7 +526,7 @@ MiniscriptInstructionOutcome Send::execute(MiniscriptThread *thread) const {
 		return kMiniscriptInstructionOutcomeFailed;
 	}
 
-	Common::SharedPtr<RuntimeObject> obj = targetValue.getObject().lock();
+	Common::SharedPtr<RuntimeObject> obj = targetValue.getObject().object.lock();
 
 	if (!obj) {
 		thread->error("Invalid message destination");
@@ -837,7 +837,7 @@ MiniscriptInstructionOutcome GetChild::execute(MiniscriptThread *thread) const {
 
 		if (_isLValue) {
 			if (indexableValueSlot.value.getType() == DynamicValueTypes::kObject) {
-				Common::SharedPtr<RuntimeObject> obj = indexableValueSlot.value.getObject().lock();
+				Common::SharedPtr<RuntimeObject> obj = indexableValueSlot.value.getObject().object.lock();
 				if (!obj) {
 					thread->error("Tried to write '" + attrib + "' to an invalid object reference");
 					return kMiniscriptInstructionOutcomeFailed;
@@ -875,7 +875,7 @@ MiniscriptInstructionOutcome GetChild::execute(MiniscriptThread *thread) const {
 
 		if (_isLValue) {
 			if (indexableValueSlot.value.getType() == DynamicValueTypes::kObject) {
-				Common::SharedPtr<RuntimeObject> obj = indexableValueSlot.value.getObject().lock();
+				Common::SharedPtr<RuntimeObject> obj = indexableValueSlot.value.getObject().object.lock();
 				if (!obj) {
 					thread->error("Tried to read '" + attrib + "' to an invalid object reference");
 					return kMiniscriptInstructionOutcomeFailed;
@@ -932,7 +932,7 @@ MiniscriptInstructionOutcome GetChild::readRValueAttrib(MiniscriptThread *thread
 		}
 		break;
 	case DynamicValueTypes::kObject: {
-			Common::SharedPtr<RuntimeObject> obj = valueSrcDest.getObject().lock();
+			Common::SharedPtr<RuntimeObject> obj = valueSrcDest.getObject().object.lock();
 			if (!obj) {
 				thread->error("Unable to read attribute '" + attrib + "' from invalid object");
 				return kMiniscriptInstructionOutcomeFailed;
@@ -1006,7 +1006,7 @@ MiniscriptInstructionOutcome PushValue::execute(MiniscriptThread *thread) const
 		value.setBool(_value.b);
 		break;
 	case DataType::kDataTypeLocalRef:
-		value.setObject(thread->getRefs()->getRefByIndex(_value.ref));
+		value.setObject(ObjectReference(thread->getRefs()->getRefByIndex(_value.ref)));
 		break;
 	case DataType::kDataTypeGlobalRef:
 		thread->error("Global references are not implemented");
@@ -1043,7 +1043,7 @@ MiniscriptInstructionOutcome PushGlobal::execute(MiniscriptThread *thread) const
 		value = thread->getMessageProperties()->getValue();
 		break;
 	case kGlobalRefSource:
-		value.setObject(thread->getMessageProperties()->getSource());
+		value.setObject(ObjectReference(thread->getMessageProperties()->getSource()));
 		break;
 	case kGlobalRefMouse:
 		thread->error("'mouse' global ref not yet implemented");
@@ -1052,10 +1052,10 @@ MiniscriptInstructionOutcome PushGlobal::execute(MiniscriptThread *thread) const
 		value.setInt(thread->getRuntime()->getPlayTime() * 60 / 1000);
 		break;
 	case kGlobalRefSharedScene:
-		value.setObject(thread->getRuntime()->getActiveSharedScene());
+		value.setObject(ObjectReference(thread->getRuntime()->getActiveSharedScene()));
 		break;
 	case kGlobalRefActiveScene:
-		value.setObject(thread->getRuntime()->getActiveMainScene());
+		value.setObject(ObjectReference(thread->getRuntime()->getActiveMainScene()));
 		break;
 	default:
 		assert(false);
@@ -1216,7 +1216,7 @@ MiniscriptInstructionOutcome MiniscriptThread::dereferenceRValue(size_t offset,
 
 	switch (stackValue.value.getType()) {
 	case DynamicValueTypes::kObject: {
-			Common::SharedPtr<RuntimeObject> obj = stackValue.value.getObject().lock();
+			Common::SharedPtr<RuntimeObject> obj = stackValue.value.getObject().object.lock();
 			if (obj && obj->isModifier()) {
 				const Modifier *modifier = static_cast<const Modifier *>(obj.get());
 				if (modifier->isVariable()) {
@@ -1297,7 +1297,7 @@ VThreadState MiniscriptThread::resume(const ResumeTaskData &taskData) {
 
 MiniscriptInstructionOutcome MiniscriptThread::tryLoadVariable(MiniscriptStackValue &stackValue) {
 	if (stackValue.value.getType() == DynamicValueTypes::kObject) {
-		Common::SharedPtr<RuntimeObject> obj = stackValue.value.getObject().lock();
+		Common::SharedPtr<RuntimeObject> obj = stackValue.value.getObject().object.lock();
 		if (obj && obj->isModifier() && static_cast<Modifier *>(obj.get())->isVariable()) {
 			VariableModifier *varMod = static_cast<VariableModifier *>(obj.get());
 			varMod->getValue(stackValue.value);
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 8008cfeb4e6..c15a0b8309a 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -186,7 +186,7 @@ bool ObjectReferenceVariableModifier::setValue(const DynamicValue &value) {
 
 void ObjectReferenceVariableModifier::getValue(DynamicValue &dest) const {
 	if (_isResolved) {
-		if (_object.expired())
+		if (_object.object.expired())
 			dest.clear();
 		else
 			dest.setObject(_object);
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index 85e8445eaf2..47be0bda0af 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -108,7 +108,7 @@ private:
 
 	Event _setToSourceParentWhen;
 
-	mutable Common::WeakPtr<RuntimeObject> _object;
+	mutable ObjectReference _object;
 	mutable Common::String _objectPath;
 	mutable bool _isResolved;
 };
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 8b317a38129..ef9cf94858d 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -309,7 +309,7 @@ void DynamicListDefaultSetter::defaultSet(Common::String &value) {
 void DynamicListDefaultSetter::defaultSet(Common::SharedPtr<DynamicList> &value) {
 }
 
-void DynamicListDefaultSetter::defaultSet(Common::WeakPtr<RuntimeObject> &value) {
+void DynamicListDefaultSetter::defaultSet(ObjectReference &value) {
 }
 
 bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const int32 *&outPtr) {
@@ -382,7 +382,7 @@ bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const C
 	return true;
 }
 
-bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const Common::WeakPtr<RuntimeObject> *&outPtr) {
+bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const ObjectReference *&outPtr) {
 	if (dynValue.getType() != DynamicValueTypes::kObject)
 		return false;
 	outPtr = &dynValue.getObject();
@@ -430,7 +430,7 @@ void DynamicListValueExporter::exportValue(DynamicValue &dynValue, const Common:
 	dynValue.setList(value);
 }
 
-void DynamicListValueExporter::exportValue(DynamicValue &dynValue, const Common::WeakPtr<RuntimeObject> &value) {
+void DynamicListValueExporter::exportValue(DynamicValue &dynValue, const ObjectReference &value) {
 	dynValue.setObject(value);
 }
 
@@ -736,7 +736,7 @@ bool DynamicList::changeToType(DynamicValueTypes::DynamicValueType type) {
 		_container = new DynamicListContainer<Common::SharedPtr<DynamicList> >();
 		break;
 	case DynamicValueTypes::kObject:
-		_container = new DynamicListContainer<Common::WeakPtr<RuntimeObject> >();
+		_container = new DynamicListContainer<ObjectReference>();
 		break;
 	}
 
@@ -943,7 +943,7 @@ const Common::SharedPtr<DynamicList> &DynamicValue::getList() const {
 	return _list;
 }
 
-const Common::WeakPtr<RuntimeObject> &DynamicValue::getObject() const {
+const ObjectReference &DynamicValue::getObject() const {
 	assert(_type == DynamicValueTypes::kObject);
 	return _obj;
 }
@@ -1065,13 +1065,17 @@ void DynamicValue::setWriteProxy(const Common::SharedPtr<DynamicList> &list, con
 	_list = listRef;
 }
 
-void DynamicValue::setObject(const Common::WeakPtr<RuntimeObject> &value) {
+void DynamicValue::setObject(const ObjectReference &value) {
 	if (_type != DynamicValueTypes::kObject)
 		clear();
 	_type = DynamicValueTypes::kObject;
 	_obj = value;
 }
 
+void DynamicValue::setObject(const Common::WeakPtr<RuntimeObject> &value) {
+	setObject(ObjectReference(value));
+}
+
 void DynamicValue::swap(DynamicValue &other) {
 	internalSwap(_type, other._type);
 	internalSwap(_str, other._str);
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index a65cfadb829..f823e23f584 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -355,6 +355,28 @@ struct VarReference {
 	}
 };
 
+struct ObjectReference {
+	Common::WeakPtr<RuntimeObject> object;
+
+	inline ObjectReference() {
+	}
+
+	inline explicit ObjectReference(const Common::WeakPtr<RuntimeObject> object) : object(object) {
+	}
+
+	inline bool operator==(const ObjectReference &other) const {
+		return !object.owner_before(other.object) && !other.object.owner_before(object);
+	}
+
+	inline bool operator!=(const ObjectReference &other) const {
+		return !((*this) == other);
+	}
+
+	inline void reset() {
+		object.reset();
+	}
+};
+
 struct AngleMagVector {
 	double angleRadians;
 	double magnitude;
@@ -461,7 +483,7 @@ struct DynamicListDefaultSetter {
 	static void defaultSet(Event &value);
 	static void defaultSet(Common::String &value);
 	static void defaultSet(Common::SharedPtr<DynamicList> &value);
-	static void defaultSet(Common::WeakPtr<RuntimeObject> &value);
+	static void defaultSet(ObjectReference &value);
 };
 
 struct DynamicListValueImporter {
@@ -475,7 +497,7 @@ struct DynamicListValueImporter {
 	static bool importValue(const DynamicValue &dynValue, const Event *&outPtr);
 	static bool importValue(const DynamicValue &dynValue, const Common::String *&outPtr);
 	static bool importValue(const DynamicValue &dynValue, const Common::SharedPtr<DynamicList> *&outPtr);
-	static bool importValue(const DynamicValue &dynValue, const Common::WeakPtr<RuntimeObject> *&outPtr);
+	static bool importValue(const DynamicValue &dynValue, const ObjectReference *&outPtr);
 };
 
 struct DynamicListValueExporter {
@@ -489,7 +511,7 @@ struct DynamicListValueExporter {
 	static void exportValue(DynamicValue &dynValue, const Event &value);
 	static void exportValue(DynamicValue &dynValue, const Common::String &value);
 	static void exportValue(DynamicValue &dynValue, const Common::SharedPtr<DynamicList> &value);
-	static void exportValue(DynamicValue &dynValue, const Common::WeakPtr<RuntimeObject> &value);
+	static void exportValue(DynamicValue &dynValue, const ObjectReference &value);
 };
 
 template<class T>
@@ -667,7 +689,7 @@ struct DynamicValue {
 	const Common::String &getString() const;
 	const bool &getBool() const;
 	const Common::SharedPtr<DynamicList> &getList() const;
-	const Common::WeakPtr<RuntimeObject> &getObject() const;
+	const ObjectReference &getObject() const;
 	const DynamicValueReadProxy &getReadProxy() const;
 	const DynamicValueWriteProxy &getWriteProxy() const;
 	const Common::SharedPtr<DynamicList> &getReadProxyList() const;
@@ -686,6 +708,7 @@ struct DynamicValue {
 	void setString(const Common::String &value);
 	void setBool(bool value);
 	void setList(const Common::SharedPtr<DynamicList> &value);
+	void setObject(const ObjectReference &value);
 	void setObject(const Common::WeakPtr<RuntimeObject> &value);
 	void setReadProxy(const Common::SharedPtr<DynamicList> &list, const DynamicValueReadProxy &readProxy);
 	void setWriteProxy(const Common::SharedPtr<DynamicList> &list, const DynamicValueWriteProxy &writeProxy);
@@ -727,7 +750,7 @@ private:
 	ValueUnion _value;
 	Common::String _str;
 	Common::SharedPtr<DynamicList> _list;
-	Common::WeakPtr<RuntimeObject> _obj;
+	ObjectReference _obj;
 };
 
 struct MessengerSendSpec {


Commit: 78742414b430cdd5ff28e306f9a20704ce9f1c57
    https://github.com/scummvm/scummvm/commit/78742414b430cdd5ff28e306f9a20704ce9f1c57
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Stepthrough debugger work

Changed paths:
    engines/mtropolis/debug.cpp
    engines/mtropolis/debug.h
    engines/mtropolis/elements.cpp
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h
    engines/mtropolis/vthread.cpp
    engines/mtropolis/vthread.h


diff --git a/engines/mtropolis/debug.cpp b/engines/mtropolis/debug.cpp
index 57b475479ca..e7952db1a2a 100644
--- a/engines/mtropolis/debug.cpp
+++ b/engines/mtropolis/debug.cpp
@@ -190,8 +190,8 @@ void DebugToolWindowBase::render() {
 		if (_toolSurface) {
 			oldWidth = _toolSurface->w;
 			oldHeight = _toolSurface->h;
+		} else
 			needChromeUpdate = true;
-		}
 
 		int32 renderWidth = getWidth() - kScrollBarWidth;
 		int32 renderHeight = getHeight() - kTopBarHeight;
@@ -642,6 +642,9 @@ void DebugSceneTreeWindow::update() {
 }
 
 void DebugSceneTreeWindow::toolRenderSurface(int32 subAreaWidth, int32 subAreaHeight) {
+	if (_tree.size() == 0)
+		return;
+
 	// Render tree
 	Common::HashMap<const SceneTreeEntry *, size_t> treeToRenderIndex;
 	_renderEntries.clear();
@@ -912,6 +915,91 @@ uint32 DebugSceneTreeWindow::getColorForObject(const RuntimeObject *object, cons
 	}
 }
 
+// Step through ("debugger") window
+class DebugStepThroughWindow : public DebugToolWindowBase {
+public:
+	DebugStepThroughWindow(Debugger *debugger, const WindowParameters &windowParams);
+
+	void update() override;
+	void toolRenderSurface(int32 subAreaWidth, int32 subAreaHeight) override;
+
+	void toolOnMouseDown(int32 x, int32 y, int mouseButton) override;
+
+private:
+	static const int kRowHeight = 14;
+
+	Common::Array<Common::SharedPtr<DebugPrimaryTaskList> > _primaryTasks;
+	Common::Array<size_t> _primaryTaskRowStarts;
+	Common::Array<size_t> _primaryTaskNumEntries;
+	size_t _totalRows;
+};
+
+DebugStepThroughWindow::DebugStepThroughWindow(Debugger *debugger, const WindowParameters &windowParams)
+	: DebugToolWindowBase(kDebuggerToolStepThrough, "Debugger", debugger, windowParams), _totalRows(0) {
+}
+
+void DebugStepThroughWindow::update() {
+	setDirty();
+
+	_primaryTasks.clear();
+	_debugger->getRuntime()->debugGetPrimaryTaskList(_primaryTasks);
+
+	_primaryTaskRowStarts.resize(_primaryTasks.size());
+	_primaryTaskNumEntries.resize(_primaryTasks.size());
+
+	_totalRows = 0;
+	for (size_t i = 0; i < _primaryTasks.size(); i++) {
+		_totalRows++;
+		_primaryTaskRowStarts[i] = _totalRows;
+
+		size_t numEntries = _primaryTasks[i]->getItems().size();
+		_primaryTaskNumEntries[i] = numEntries;
+		_totalRows += numEntries;
+	}
+}
+
+void DebugStepThroughWindow::toolRenderSurface(int32 subAreaWidth, int32 subAreaHeight) {
+
+	const Graphics::PixelFormat fmt = _debugger->getRuntime()->getRenderPixelFormat();
+
+	uint32 whiteColor = fmt.RGBToColor(255, 255, 255);
+	uint32 blackColor = fmt.RGBToColor(0, 0, 0);
+
+	int32 renderHeight = subAreaHeight;
+	int32 renderWidth = subAreaWidth;
+
+	if (_primaryTasks.size() > 0)
+		renderHeight = static_cast<int32>(_primaryTaskRowStarts.back() + _primaryTaskNumEntries.back()) * kRowHeight;
+
+	if (!_toolSurface || renderWidth != _toolSurface->w || renderHeight != _toolSurface->h) {
+		_toolSurface.reset();
+		_toolSurface.reset(new Graphics::ManagedSurface(renderWidth, renderHeight, fmt));
+	}
+
+	const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
+
+	for (size_t catIndex = 0; catIndex < _primaryTasks.size(); catIndex++) {
+		int32 startY = (_primaryTaskRowStarts[catIndex] - 1) * kRowHeight;
+
+		const DebugPrimaryTaskList *taskList = _primaryTasks[catIndex].get();
+
+		font->drawString(_toolSurface.get(), taskList->getName(), 2, startY + 2, renderWidth - 2, blackColor);
+
+		const Common::Array<IDebuggable *> &items = taskList->getItems();
+
+		const size_t numChildren = items.size();
+		for (size_t chIndex = 0; chIndex < numChildren; chIndex++) {
+			int32 rowY = (_primaryTaskRowStarts[catIndex] + chIndex) * kRowHeight;
+			font->drawString(_toolSurface.get(), items[chIndex]->debugGetName(), 10, startY + 2, renderWidth - 2, blackColor);
+		}
+	}
+}
+
+void DebugStepThroughWindow::toolOnMouseDown(int32 x, int32 y, int mouseButton) {
+	if (mouseButton != Actions::kMouseButtonLeft)
+		return;
+}
+
 class DebugToolsWindow : public Window {
 public:
 	DebugToolsWindow(Debugger *debugger, const WindowParameters &windowParams);
@@ -944,6 +1032,26 @@ void DebugInspector::onDestroyed() {
 	_debuggable = nullptr;
 }
 
+void DebugInspector::onDebuggableRelocated(IDebuggable *debuggable) {
+	_debuggable = debuggable;
+}
+
+DebugPrimaryTaskList::DebugPrimaryTaskList(const Common::String &name) : _name(name) {
+}
+
+void DebugPrimaryTaskList::addItem(IDebuggable *debuggable) {
+	_primaryTasks.push_back(debuggable);
+}
+
+const Common::Array<IDebuggable*> &DebugPrimaryTaskList::getItems() const {
+	return _primaryTasks;
+}
+
+const Common::String &DebugPrimaryTaskList::getName() const {
+	return _name;
+}
+
+
 Debugger::Debugger(Runtime *runtime) : _paused(false), _runtime(runtime) {
 	refreshSceneStatus();
 
@@ -1177,13 +1285,13 @@ void Debugger::openToolWindow(DebuggerTool tool) {
 
 	switch (tool) {
 	case kDebuggerToolSceneTree:
-		windowRef.reset(new DebugSceneTreeWindow(this, WindowParameters(_runtime, 32, 32, 100, 320, _runtime->getRenderPixelFormat())));
+		windowRef.reset(new DebugSceneTreeWindow(this, WindowParameters(_runtime, 32, 32, 250, 120, _runtime->getRenderPixelFormat())));
 		break;
 	case kDebuggerToolInspector:
 		windowRef.reset(new DebugToolWindowBase(kDebuggerToolInspector, "Inspector", this, WindowParameters(_runtime, 32, 32, 100, 320, _runtime->getRenderPixelFormat())));
 		break;
 	case kDebuggerToolStepThrough:
-		windowRef.reset(new DebugToolWindowBase(kDebuggerToolStepThrough, "Debugger", this, WindowParameters(_runtime, 32, 32, 100, 320, _runtime->getRenderPixelFormat())));
+		windowRef.reset(new DebugStepThroughWindow(this, WindowParameters(_runtime, 32, 32, 100, 320, _runtime->getRenderPixelFormat())));
 		break;
 	default:
 		assert(false);
diff --git a/engines/mtropolis/debug.h b/engines/mtropolis/debug.h
index f6a52f1817a..5ee6ffbc937 100644
--- a/engines/mtropolis/debug.h
+++ b/engines/mtropolis/debug.h
@@ -27,7 +27,12 @@
 #include "common/hashmap.h"
 
 #define MTROPOLIS_DEBUG_VTHREAD_STACKS
+
 #define MTROPOLIS_DEBUG_ENABLE
+#if defined(MTROPOLIS_DEBUG_ENABLE) && !defined(MTROPOLIS_DEBUG_VTHREAD_STACKS)
+// VThread stack debugging is mandatory when debugging
+#define MTROPOLIS_DEBUG_VTHREAD_STACKS
+#endif
 
 namespace MTropolis {
 
@@ -54,10 +59,26 @@ public:
 
 	virtual void onDestroyed();
 
+	void onDebuggableRelocated(IDebuggable *debuggable);
+
 private:
 	IDebuggable *_debuggable;
 };
 
+class DebugPrimaryTaskList {
+public:
+	explicit DebugPrimaryTaskList(const Common::String &name);
+
+	void addItem(IDebuggable *debuggable);
+	const Common::Array<IDebuggable *> &getItems() const;
+
+	const Common::String &getName() const;
+
+private:
+	Common::String _name;
+	Common::Array<IDebuggable *> _primaryTasks;
+};
+
 enum DebugSeverity {
 	kDebugSeverityInfo,
 	kDebugSeverityWarning,
@@ -84,6 +105,7 @@ public:
 	explicit Debugger(Runtime *runtime);
 	~Debugger();
 
+	// runFrame runs after the frame, before rendering, and before event processing for the following frame
 	void runFrame(uint32 msec);
 
 	void setPaused(bool paused);
@@ -121,14 +143,6 @@ private:
 	Common::Array<ToastNotification> _toastNotifications;
 };
 
-#define MTROPOLIS_DEBUG_NOTIFY(...) \
-		(static_cast<const IDebuggable *>(this)->debugGetDebugger()->notifyFmt(__VA_ARGS__));
-
-
-#else /* MTROPOLIS_DEBUG_ENABLE */
-
-#define MTROPOLIS_DEBUG_NOTIFY(...) ((void)0)
-
 #endif /* !MTROPOLIS_DEBUG_ENABLE */
 
 struct IDebuggable {
@@ -137,7 +151,6 @@ struct IDebuggable {
 	virtual const char *debugGetTypeName() const = 0;
 	virtual const Common::String &debugGetName() const = 0;
 	virtual Common::SharedPtr<DebugInspector> debugGetInspector() const = 0;
-	virtual Debugger *debugGetDebugger() const = 0;
 #endif
 };
 
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index e827dece2e5..481d0a23a6a 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -100,10 +100,10 @@ bool MovieElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWrite
 
 VThreadState MovieElement::consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
 	if (Event::create(EventIDs::kPlay, 0).respondsTo(msg->getEvent())) {
-		StartPlayingTaskData *startPlayingTaskData = runtime->getVThread().pushTask(this, &MovieElement::startPlayingTask);
+		StartPlayingTaskData *startPlayingTaskData = runtime->getVThread().pushTask("MovieElement::startPlayingTask", this, &MovieElement::startPlayingTask);
 		startPlayingTaskData->runtime = runtime;
 
-		ChangeFlagTaskData *becomeVisibleTaskData = runtime->getVThread().pushTask(static_cast<VisualElement *>(this), &MovieElement::changeVisibilityTask);
+		ChangeFlagTaskData *becomeVisibleTaskData = runtime->getVThread().pushTask("MovieElement::changeVisibilityTask", static_cast<VisualElement *>(this), &MovieElement::changeVisibilityTask);
 		becomeVisibleTaskData->desiredFlag = true;
 		becomeVisibleTaskData->runtime = runtime;
 	}
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 823c02487da..b57b0cbfbc8 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -1154,7 +1154,7 @@ MiniscriptThread::MiniscriptThread(Runtime *runtime, const Common::SharedPtr<Mes
 }
 
 void MiniscriptThread::runOnVThread(VThread &vthread, const Common::SharedPtr<MiniscriptThread> &thread) {
-	ResumeTaskData *taskData = vthread.pushTask(resumeTask);
+	ResumeTaskData *taskData = vthread.pushTask("MiniscriptThread::resumeTask", resumeTask);
 	taskData->thread = thread;
 }
 
@@ -1274,7 +1274,7 @@ VThreadState MiniscriptThread::resume(const ResumeTaskData &taskData) {
 
 	// Requeue now so that any VThread tasks queued by instructions run in front of the resume
 	{
-		ResumeTaskData *requeueData = _runtime->getVThread().pushTask(resumeTask);
+		ResumeTaskData *requeueData = _runtime->getVThread().pushTask("MiniscriptThread::resumeTask", resumeTask);
 		requeueData->thread = taskData.thread;
 	}
 
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 0a91b4be78d..a07d8e03f02 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -82,13 +82,13 @@ bool BehaviorModifier::respondsToEvent(const Event &evt) const {
 VThreadState BehaviorModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
 	if (_switchable) {
 		if (_disableWhen.respondsTo(msg->getEvent())) {
-			SwitchTaskData *taskData = runtime->getVThread().pushTask(this, &BehaviorModifier::switchTask);
+			SwitchTaskData *taskData = runtime->getVThread().pushTask("BehaviorModifier::switchTask", this, &BehaviorModifier::switchTask);
 			taskData->targetState = false;
 			taskData->eventID = EventIDs::kParentDisabled;
 			taskData->runtime = runtime;
 		}
 		if (_enableWhen.respondsTo(msg->getEvent())) {
-			SwitchTaskData *taskData = runtime->getVThread().pushTask(this, &BehaviorModifier::switchTask);
+			SwitchTaskData *taskData = runtime->getVThread().pushTask("BehaviorModifier::switchTask", this, &BehaviorModifier::switchTask);
 			taskData->targetState = true;
 			taskData->eventID = EventIDs::kParentEnabled;
 			taskData->runtime = runtime;
@@ -103,7 +103,7 @@ VThreadState BehaviorModifier::switchTask(const SwitchTaskData &taskData) {
 		_isEnabled = taskData.targetState;
 
 		if (_children.size() > 0) {
-			PropagateTaskData *propagateData = taskData.runtime->getVThread().pushTask(this, &BehaviorModifier::propagateTask);
+			PropagateTaskData *propagateData = taskData.runtime->getVThread().pushTask("BehaviorModifier::propagateTask", this, &BehaviorModifier::propagateTask);
 			propagateData->eventID = taskData.eventID;
 			propagateData->index = 0;
 			propagateData->runtime = taskData.runtime;
@@ -115,7 +115,7 @@ VThreadState BehaviorModifier::switchTask(const SwitchTaskData &taskData) {
 
 VThreadState BehaviorModifier::propagateTask(const PropagateTaskData &taskData) {
 	if (taskData.index + 1 < _children.size()) {
-		PropagateTaskData *propagateData = taskData.runtime->getVThread().pushTask(this, &BehaviorModifier::propagateTask);
+		PropagateTaskData *propagateData = taskData.runtime->getVThread().pushTask("BehaviorModifier::propagateTask", this, &BehaviorModifier::propagateTask);
 		propagateData->eventID = taskData.eventID;
 		propagateData->index = taskData.index + 1;
 		propagateData->runtime = taskData.runtime;
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index ef9cf94858d..abaaa3a922f 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -1597,9 +1597,6 @@ const Common::WeakPtr<RuntimeObject>& MessageProperties::getSource() const {
 }
 
 Structural::Structural() : _parent(nullptr) {
-#ifdef MTROPOLIS_DEBUG_ENABLE
-	_debugger = nullptr;
-#endif
 }
 
 Structural::~Structural() {
@@ -1763,10 +1760,6 @@ Common::SharedPtr<DebugInspector> Structural::debugGetInspector() const {
 	return _debugInspector;
 }
 
-Debugger* Structural::debugGetDebugger() const {
-	return _debugger;
-}
-
 DebugInspector *Structural::debugCreateInspector() {
 	return new DebugInspector(this);
 }
@@ -2190,16 +2183,16 @@ bool Runtime::runFrame() {
 	_realTime = timeMillis - _realTimeBase;
 	_playTime = timeMillis - _playTimeBase;
 
+	for (;;) {
 #ifdef MTROPOLIS_DEBUG_ENABLE
-	if (_debugger)
-		_debugger->runFrame(realMSec);
+		if (_debugger->isPaused())
+			break;
 #endif
 
-	for (;;) {
 		VThreadState state = _vthread->step();
 		if (state != kVThreadReturn) {
 			// Still doing blocking tasks
-			return false;
+			break;
 		}
 
 		if (_queuedProjectDesc) {
@@ -2310,6 +2303,12 @@ bool Runtime::runFrame() {
 		break;
 	}
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	if (_debugger)
+		_debugger->runFrame(realMSec);
+
+#endif
+
 	// Frame completed
 	return true;
 }
@@ -2421,7 +2420,8 @@ void Runtime::drawFrame() {
 		CursorMan.replaceCursor(cursor);
 	}
 
-	_project->onPostRender();
+	if (_project)
+		_project->onPostRender();
 }
 
 Common::SharedPtr<Structural> Runtime::findDefaultSharedSceneForScene(Structural *scene) {
@@ -2731,7 +2731,7 @@ void Runtime::loadScene(const Common::SharedPtr<Structural>& scene) {
 }
 
 void Runtime::sendMessageOnVThread(const Common::SharedPtr<MessageDispatch> &dispatch) {
-	DispatchMethodTaskData *taskData = _vthread->pushTask(this, &Runtime::dispatchMessageTask);
+	DispatchMethodTaskData *taskData = _vthread->pushTask("Runtime::dispatchMessageTask", this, &Runtime::dispatchMessageTask);
 	taskData->dispatch = dispatch;
 }
 
@@ -2743,7 +2743,7 @@ VThreadState Runtime::dispatchMessageTask(const DispatchMethodTaskData &data) {
 		return kVThreadReturn;
 	else {
 		// Requeue propagation after whatever happens with this propagation step
-		DispatchMethodTaskData *requeueData = _vthread->pushTask(this, &Runtime::dispatchMessageTask);
+		DispatchMethodTaskData *requeueData = _vthread->pushTask("Runtime::dispatchMessageTask", this, &Runtime::dispatchMessageTask);
 		requeueData->dispatch = dispatchPtr;
 
 		return dispatch.continuePropagating(this);
@@ -2925,13 +2925,13 @@ Project *Runtime::getProject() const {
 }
 
 void Runtime::postConsumeMessageTask(IMessageConsumer *consumer, const Common::SharedPtr<MessageProperties> &msg) {
-	ConsumeMessageTaskData *params = _vthread->pushTask(this, &Runtime::consumeMessageTask);
+	ConsumeMessageTaskData *params = _vthread->pushTask("Runtime::consumeMessageTask", this, &Runtime::consumeMessageTask);
 	params->consumer = consumer;
 	params->message = msg;
 }
 
 void Runtime::postConsumeCommandTask(Structural *structural, const Common::SharedPtr<MessageProperties> &msg) {
-	ConsumeCommandTaskData *params = _vthread->pushTask(this, &Runtime::consumeCommandTask);
+	ConsumeCommandTaskData *params = _vthread->pushTask("Runtime::consumeMessageTask", this, &Runtime::consumeCommandTask);
 	params->structural = structural;
 	params->message = msg;
 }
@@ -3046,10 +3046,47 @@ void Runtime::debugBreak() {
 	_debugger->setPaused(true);
 }
 
-Debugger* Runtime::debugGetDebugger() const {
+Debugger *Runtime::debugGetDebugger() const {
 	return _debugger.get();
 }
 
+void Runtime::debugGetPrimaryTaskList(Common::Array<Common::SharedPtr<DebugPrimaryTaskList> > &primaryTaskLists) {
+	{
+		Common::SharedPtr<DebugPrimaryTaskList> vthreadTaskList(new DebugPrimaryTaskList("Execute"));
+		primaryTaskLists.push_back(vthreadTaskList);
+	}
+
+	{
+		Common::SharedPtr<DebugPrimaryTaskList> projectQueueTaskList(new DebugPrimaryTaskList("Project queue"));
+		primaryTaskLists.push_back(projectQueueTaskList);
+	}
+
+	{
+		Common::SharedPtr<DebugPrimaryTaskList> messageQueueTaskList(new DebugPrimaryTaskList("Message queue"));
+		primaryTaskLists.push_back(messageQueueTaskList);
+	}
+
+	{
+		Common::SharedPtr<DebugPrimaryTaskList> teardownTaskList(new DebugPrimaryTaskList("Teardowns"));
+		primaryTaskLists.push_back(teardownTaskList);
+	}
+
+	{
+		Common::SharedPtr<DebugPrimaryTaskList> llstTasks(new DebugPrimaryTaskList("Low-level scene transitions"));
+		primaryTaskLists.push_back(llstTasks);
+	}
+
+	{
+		Common::SharedPtr<DebugPrimaryTaskList> hlstTasks(new DebugPrimaryTaskList("High-level scene transitions"));
+		primaryTaskLists.push_back(hlstTasks);
+	}
+
+	{
+		Common::SharedPtr<DebugPrimaryTaskList> scheduledEventsTasks(new DebugPrimaryTaskList("Scheduled events"));
+		primaryTaskLists.push_back(scheduledEventsTasks);
+	}
+}
+
 #endif /* MTROPOLIS_DEBUG_ENABLE */
 
 const Common::Array<Common::SharedPtr<Modifier> > &IModifierContainer::getModifiers() const {
@@ -4153,10 +4190,6 @@ Common::SharedPtr<DebugInspector> Modifier::debugGetInspector() const {
 	return _debugInspector;
 }
 
-Debugger *Modifier::debugGetDebugger() const {
-	return _debugger;
-}
-
 DebugInspector *Modifier::debugCreateInspector() {
 	return new DebugInspector(this);
 
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index f823e23f584..34713207323 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -81,6 +81,10 @@ struct ModifierLoaderContext;
 struct PlugInModifierLoaderContext;
 template<typename TElement, typename TElementData> class ElementFactory;
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+class DebugPrimaryTaskList;
+#endif
+
 char invariantToLower(char c);
 Common::String toCaseInsensitive(const Common::String &str);
 bool caseInsensitiveEqual(const Common::String &str1, const Common::String &str2);
@@ -1110,6 +1114,8 @@ public:
 	void debugSetEnabled(bool enabled);
 	void debugBreak();
 	Debugger *debugGetDebugger() const;
+
+	void debugGetPrimaryTaskList(Common::Array<Common::SharedPtr<DebugPrimaryTaskList> > &primaryTaskLists);
 #endif
 
 private:
@@ -1343,7 +1349,6 @@ public:
 	SupportStatus debugGetSupportStatus() const override;
 	const Common::String &debugGetName() const override;
 	Common::SharedPtr<DebugInspector> debugGetInspector() const override;
-	Debugger *debugGetDebugger() const override;
 
 	virtual DebugInspector *debugCreateInspector();
 #endif
@@ -1364,7 +1369,6 @@ protected:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	Common::SharedPtr<DebugInspector> _debugInspector;
-	Debugger *_debugger;
 #endif
 };
 
@@ -1728,7 +1732,6 @@ public:
 	SupportStatus debugGetSupportStatus() const override;
 	const Common::String &debugGetName() const override;
 	Common::SharedPtr<DebugInspector> debugGetInspector() const override;
-	Debugger *debugGetDebugger() const override;
 
 	virtual DebugInspector *debugCreateInspector();
 #endif
diff --git a/engines/mtropolis/vthread.cpp b/engines/mtropolis/vthread.cpp
index 83b68139966..edb7ee0a4c3 100644
--- a/engines/mtropolis/vthread.cpp
+++ b/engines/mtropolis/vthread.cpp
@@ -23,12 +23,20 @@
 
 namespace MTropolis {
 
+VThreadTaskData::VThreadTaskData() {
+}
+
 VThreadTaskData::~VThreadTaskData() {
 }
 
-VThread::VThread() : _faultID(nullptr), _stackUnalignedBase(nullptr), _stackAlignedBase(nullptr), _size(0), _alignment(1), _used(0) {
+#ifdef MTROPOLIS_DEBUG_ENABLE
+void VThreadTaskData::debugInit(const char *name) {
+	_debugName = name;
 }
+#endif
 
+VThread::VThread() : _faultID(nullptr), _stackUnalignedBase(nullptr), _stackAlignedBase(nullptr), _size(0), _alignment(1), _used(0) {
+}
 
 VThread::~VThread() {
 	void *dataPtr;
diff --git a/engines/mtropolis/vthread.h b/engines/mtropolis/vthread.h
index 993dbab1a3a..c2a92bbceae 100644
--- a/engines/mtropolis/vthread.h
+++ b/engines/mtropolis/vthread.h
@@ -44,14 +44,29 @@ struct VThreadFaultIdentifierSingleton {
 template<typename T>
 VThreadFaultIdentifier VThreadFaultIdentifierSingleton<T>::_identifier;
 
-class VThreadTaskData {
-
+class VThreadTaskData : public IDebuggable {
 public:
+	VThreadTaskData();
 	virtual ~VThreadTaskData();
 
 	virtual VThreadState destructAndRunTask() = 0;
 	virtual void relocateTo(void *newPosition) = 0;
 	virtual bool handlesFault(const VThreadFaultIdentifier *faultID) const = 0;
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
+public:
+	void debugInit(const char *name);
+
+protected:
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
+	const char *debugGetTypeName() const override { return "Task"; }
+	const Common::String &debugGetName() const override { return _debugName; }
+	Common::SharedPtr<DebugInspector> debugGetInspector() const override { return _debugInspector; }
+
+	Common::SharedPtr<DebugInspector> _debugInspector;
+
+	Common::String _debugName;
+#endif
 };
 
 struct VThreadStackFrame {
@@ -66,7 +81,6 @@ struct VThreadStackFrame {
 
 template<typename TClass, typename TData>
 class VThreadMethodData : public VThreadTaskData {
-
 public:
 	VThreadMethodData(const VThreadFaultIdentifier *faultID, TClass *target, VThreadState (TClass::*method)(const TData &data));
 	VThreadMethodData(const VThreadMethodData &other);
@@ -118,25 +132,25 @@ public:
 	~VThread();
 
 	template<typename TClass, typename TData>
-	TData *pushTask(TClass *obj, VThreadState (TClass::*method)(const TData &data));
+	TData *pushTask(const char *name, TClass *obj, VThreadState (TClass::*method)(const TData &data));
 
 	template<typename TData>
-	TData *pushTask(VThreadState (*func)(const TData &data));
+	TData *pushTask(const char *name, VThreadState (*func)(const TData &data));
 
 	template<typename TFaultType, typename TClass, typename TData>
-	TData *pushFaultHandler(TClass *obj, VThreadState (TClass::*method)(const TData &data));
+	TData *pushFaultHandler(const char *name, TClass *obj, VThreadState (TClass::*method)(const TData &data));
 
 	template<typename TFaultType, typename TData>
-	TData *pushFaultHandler(VThreadState (*func)(const TData &data));
+	TData *pushFaultHandler(const char *name, VThreadState (*func)(const TData &data));
 
 	VThreadState step();
 
 private:
 	template<typename TClass, typename TData>
-	TData *pushTaskWithFaultHandler(const VThreadFaultIdentifier *faultID, TClass *obj, VThreadState (TClass::*method)(const TData &data));
+	TData *pushTaskWithFaultHandler(const VThreadFaultIdentifier *faultID, const char *name, TClass *obj, VThreadState (TClass::*method)(const TData &data));
 
 	template<typename TData>
-	TData *pushTaskWithFaultHandler(const VThreadFaultIdentifier *faultID, VThreadState (*func)(const TData &data));
+	TData *pushTaskWithFaultHandler(const VThreadFaultIdentifier *faultID, const char *name, VThreadState (*func)(const TData &data));
 
 	void reserveFrame(size_t size, size_t alignment, void *&outFramePtr, void *&outUnadjustedDataPtr, size_t &outPrevFrameOffset);
 	bool popFrame(void *&dataPtr, void *&outFramePtr);
@@ -189,9 +203,15 @@ void VThreadMethodData<TClass, TData>::relocateTo(void *newPosition) {
 	void *adjustedPtr = static_cast<VThreadMethodData<TClass, TData> *>(static_cast<VThreadTaskData *>(newPosition));
 
 #if __cplusplus >= 201103L
-	new (adjustedPtr) VThreadMethodData<TClass, TData>(static_cast<VThreadMethodData<TClass, TData> &&>(*this));
+	VThreadMethodData<TClass, TData> *relocated = new (adjustedPtr) VThreadMethodData<TClass, TData>(static_cast<VThreadMethodData<TClass, TData> &&>(*this));
 #else
-	new (adjustedPtr) VThreadMethodData<TClass, TData>(*this);
+	VThreadMethodData<TClass, TData> *relocated = new (adjustedPtr) VThreadMethodData<TClass, TData>(*this);
+#endif
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	relocated->_debugInspector->onDebuggableRelocated(this);
+#else
+	(void)relocated;
 #endif
 }
 
@@ -244,9 +264,15 @@ void VThreadFunctionData<TData>::relocateTo(void *newPosition) {
 	void *adjustedPtr = static_cast<VThreadFunctionData<TData> *>(static_cast<VThreadTaskData *>(newPosition));
 
 #if __cplusplus >= 201103L
-	new (adjustedPtr) VThreadFunctionData<TData>(static_cast<VThreadFunctionData<TData> &&>(*this));
+	VThreadFunctionData<TData> *relocated = new (adjustedPtr) VThreadFunctionData<TData>(static_cast<VThreadFunctionData<TData> &&>(*this));
+#else
+	VThreadFunctionData<TData> *relocated = new (adjustedPtr) VThreadFunctionData<TData>(*this);
+#endif
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	relocated->_debugInspector->onDebuggableRelocated(this);
 #else
-	new (adjustedPtr) VThreadFunctionData<TData>(*this);
+	(void)relocated;
 #endif
 }
 
@@ -261,28 +287,28 @@ TData &VThreadFunctionData<TData>::getData() {
 }
 
 template<typename TClass, typename TData>
-TData *VThread::pushTask(TClass *obj, VThreadState (TClass::*method)(const TData &data)) {
-	return this->pushTaskWithFaultHandler(nullptr, obj, method);
+TData *VThread::pushTask(const char *name, TClass *obj, VThreadState (TClass::*method)(const TData &data)) {
+	return this->pushTaskWithFaultHandler(nullptr, name, obj, method);
 }
 
 template<typename TData>
-TData *VThread::pushTask(VThreadState (*func)(const TData &data)) {
-	return this->pushTaskWithFaultHandler(nullptr, func);
+TData *VThread::pushTask(const char *name, VThreadState (*func)(const TData &data)) {
+	return this->pushTaskWithFaultHandler(nullptr, name, func);
 }
 
 template<typename TFaultType, typename TClass, typename TData>
-TData *VThread::pushFaultHandler(TClass *obj, VThreadState (TClass::*method)(const TData &data)) {
-	return this->pushTaskWithFaultHandler(&VThreadFaultIdentifierSingleton<TFaultType>::_identifier, obj, method);
+TData *VThread::pushFaultHandler(const char *name, TClass *obj, VThreadState (TClass::*method)(const TData &data)) {
+	return this->pushTaskWithFaultHandler(&VThreadFaultIdentifierSingleton<TFaultType>::_identifier, name, obj, method);
 }
 
 template<typename TFaultType, typename TData>
-TData *VThread::pushFaultHandler(VThreadState (*func)(const TData &data)) {
-	return this->pushTaskWithFaultHandler(&VThreadFaultIdentifierSingleton<TFaultType>::_identifier, func);
+TData *VThread::pushFaultHandler(const char *name, VThreadState (*func)(const TData &data)) {
+	return this->pushTaskWithFaultHandler(&VThreadFaultIdentifierSingleton<TFaultType>::_identifier, name, func);
 }
 
 
 template<typename TClass, typename TData>
-TData *VThread::pushTaskWithFaultHandler(const VThreadFaultIdentifier *faultID, TClass *obj, VThreadState (TClass::*method)(const TData &data)) {
+TData *VThread::pushTaskWithFaultHandler(const VThreadFaultIdentifier *faultID, const char *name, TClass *obj, VThreadState (TClass::*method)(const TData &data)) {
 	typedef VThreadMethodData<TClass, TData> FrameData_t; 
 
 	const size_t frameAlignment = alignof(VThreadStackFrame);
@@ -304,12 +330,15 @@ TData *VThread::pushTaskWithFaultHandler(const VThreadFaultIdentifier *faultID,
 	frame->prevFrame = reinterpret_cast<VThreadStackFrame *>(static_cast<char *>(_stackAlignedBase) + prevFrameOffset);
 	frame->data = frameData;
 #endif
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	frameData->debugInit(name);
+#endif
 
 	return &frameData->getData();
 }
 
 template<typename TData>
-TData *VThread::pushTaskWithFaultHandler(const VThreadFaultIdentifier *faultID, VThreadState (*func)(const TData &data)) {
+TData *VThread::pushTaskWithFaultHandler(const VThreadFaultIdentifier *faultID, const char *name, VThreadState (*func)(const TData &data)) {
 	typedef VThreadFunctionData<TData> FrameData_t;
 
 	const size_t frameAlignment = alignof(VThreadStackFrame);
@@ -331,6 +360,9 @@ TData *VThread::pushTaskWithFaultHandler(const VThreadFaultIdentifier *faultID,
 	frame->prevFrame = reinterpret_cast<VThreadStackFrame *>(static_cast<char *>(_stackAlignedBase) + prevFrameOffset);
 	frame->data = frameData;
 #endif
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	frameData->debugInit(name);
+#endif
 
 	return &frameData->getData();
 }


Commit: 286a5dac9222469fba31882036f1422570c89d53
    https://github.com/scummvm/scummvm/commit/286a5dac9222469fba31882036f1422570c89d53
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix stack reloc crash

Changed paths:
    engines/mtropolis/vthread.h


diff --git a/engines/mtropolis/vthread.h b/engines/mtropolis/vthread.h
index c2a92bbceae..0638405a788 100644
--- a/engines/mtropolis/vthread.h
+++ b/engines/mtropolis/vthread.h
@@ -209,7 +209,8 @@ void VThreadMethodData<TClass, TData>::relocateTo(void *newPosition) {
 #endif
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
-	relocated->_debugInspector->onDebuggableRelocated(this);
+	if (relocated->_debugInspector)
+		relocated->_debugInspector->onDebuggableRelocated(this);
 #else
 	(void)relocated;
 #endif
@@ -270,7 +271,8 @@ void VThreadFunctionData<TData>::relocateTo(void *newPosition) {
 #endif
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
-	relocated->_debugInspector->onDebuggableRelocated(this);
+	if (relocated->_debugInspector)
+		relocated->_debugInspector->onDebuggableRelocated(this);
 #else
 	(void)relocated;
 #endif


Commit: 389ad1c195b2114c4e532029571dd81067c52b24
    https://github.com/scummvm/scummvm/commit/389ad1c195b2114c4e532029571dd81067c52b24
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Work to get object reference vars working, refactor a bunch of subfield reference stuff

Changed paths:
    engines/mtropolis/debug.cpp
    engines/mtropolis/elements.cpp
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/miniscript.h
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/debug.cpp b/engines/mtropolis/debug.cpp
index e7952db1a2a..4569c6a8243 100644
--- a/engines/mtropolis/debug.cpp
+++ b/engines/mtropolis/debug.cpp
@@ -990,7 +990,7 @@ void DebugStepThroughWindow::toolRenderSurface(int32 subAreaWidth, int32 subArea
 		const size_t numChildren = items.size();
 		for (size_t chIndex = 0; chIndex < numChildren; chIndex++) {
 			int32 rowY = (_primaryTaskRowStarts[catIndex] + chIndex) * kRowHeight;
-			font->drawString(_toolSurface.get(), items[chIndex]->debugGetName(), 10, startY + 2, renderWidth - 2, blackColor);
+			font->drawString(_toolSurface.get(), items[chIndex]->debugGetName(), 10, rowY + 2, renderWidth - 2, blackColor);
 		}
 	}
 }
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 481d0a23a6a..c4ba0abf86a 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -91,7 +91,7 @@ bool MovieElement::readAttribute(MiniscriptThread *thread, DynamicValue &result,
 bool MovieElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
 
 	if (attrib == "paused") {
-		writeProxy = DynamicValueWriteFuncHelper<MovieElement, &MovieElement::scriptSetPaused>::create(this);
+		DynamicValueWriteFuncHelper<MovieElement, &MovieElement::scriptSetPaused>::create(this, writeProxy);
 		return true;
 	}
 	
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index b57b0cbfbc8..24f04e8d346 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -459,8 +459,9 @@ MiniscriptInstructionOutcome UnimplementedInstruction::execute(MiniscriptThread
 }
 
 MiniscriptInstructionOutcome Set::execute(MiniscriptThread *thread) const {
-	if (thread->getStackSize() < 2) {
-		thread->error("Stack underflow");
+	if (thread->getStackSize() != 2) {
+		// Sets are only allowed when they would empty the stack
+		thread->error("Invalid stack state for set instruction");
 		return kMiniscriptInstructionOutcomeFailed;
 	}
 
@@ -473,8 +474,8 @@ MiniscriptInstructionOutcome Set::execute(MiniscriptThread *thread) const {
 	MiniscriptStackValue &target = thread->getStackValueFromTop(1);
 
 	if (target.value.getType() == DynamicValueTypes::kWriteProxy) {
-		const DynamicValueWriteProxy &proxy = target.value.getWriteProxy();
-		if (!proxy.ifc->write(srcValue.value, proxy.objectRef, proxy.ptrOrOffset)) {
+		const DynamicValueWriteProxyPOD &proxy = target.value.getWriteProxyPOD();
+		if (!proxy.ifc->write(thread, srcValue.value, proxy.objectRef, proxy.ptrOrOffset)) {
 			thread->error("Failed to assign value");
 			return kMiniscriptInstructionOutcomeFailed;
 		}
@@ -487,7 +488,7 @@ MiniscriptInstructionOutcome Set::execute(MiniscriptThread *thread) const {
 		}
 
 		if (var != nullptr) {
-			if (!var->setValue(srcValue.value)) {
+			if (!var->varSetValue(srcValue.value)) {
 				thread->error("Couldn't assign value to variable, probably wrong type");
 				return kMiniscriptInstructionOutcomeFailed;
 			}
@@ -505,8 +506,9 @@ Send::Send(const Event &evt, const MessageFlags &messageFlags) : _evt(evt), _mes
 }
 
 MiniscriptInstructionOutcome Send::execute(MiniscriptThread *thread) const {
-	if (thread->getStackSize() < 2) {
-		thread->error("Stack underflow");
+	if (thread->getStackSize() != 2) {
+		// Send instructions are only allowed if they empty the stack
+		thread->error("Invalid stack state for send instruction");
 		return kMiniscriptInstructionOutcomeFailed;
 	}
 
@@ -522,14 +524,14 @@ MiniscriptInstructionOutcome Send::execute(MiniscriptThread *thread) const {
 	DynamicValue &payloadValue = thread->getStackValueFromTop(1).value;
 
 	if (targetValue.getType() != DynamicValueTypes::kObject) {
-		thread->error("Invalid message destination");
+		thread->error("Invalid message destination (target isn't an object reference)");
 		return kMiniscriptInstructionOutcomeFailed;
 	}
 
 	Common::SharedPtr<RuntimeObject> obj = targetValue.getObject().object.lock();
 
 	if (!obj) {
-		thread->error("Invalid message destination");
+		thread->error("Invalid message destination (object reference is invalid)");
 		return kMiniscriptInstructionOutcomeFailed;
 	}
 
@@ -849,11 +851,14 @@ MiniscriptInstructionOutcome GetChild::execute(MiniscriptThread *thread) const {
 					return kMiniscriptInstructionOutcomeFailed;
 				}
 			} else if (indexableValueSlot.value.getType() == DynamicValueTypes::kWriteProxy) {
-				const DynamicValueWriteProxy &proxy = indexableValueSlot.value.getWriteProxy();
-				if (!proxy.ifc->refAttribIndexed(indexableValueSlot.value, proxy.objectRef, proxy.ptrOrOffset, attrib, indexSlot.value)) {
-					thread->error("Can't write to attribute '" + attrib + "'");
+				DynamicValueWriteProxy proxy = indexableValueSlot.value.getWriteProxyTEMP();
+
+				if (!proxy.pod.ifc->refAttribIndexed(thread, proxy, proxy.pod.objectRef, proxy.pod.ptrOrOffset, attrib, indexSlot.value)) {
+					thread->error("Can't write to indexed attribute '" + attrib + "'");
 					return kMiniscriptInstructionOutcomeFailed;
 				}
+
+				indexableValueSlot.value.setWriteProxy(proxy);
 			} else {
 				thread->error("Tried to l-value index something that was not writeable");
 				return kMiniscriptInstructionOutcomeFailed;
@@ -887,13 +892,14 @@ MiniscriptInstructionOutcome GetChild::execute(MiniscriptThread *thread) const {
 					return kMiniscriptInstructionOutcomeFailed;
 				}
 
-				indexableValueSlot.value.setWriteProxy(nullptr, writeProxy);
+				indexableValueSlot.value.setWriteProxy(writeProxy);
 			} else if (indexableValueSlot.value.getType() == DynamicValueTypes::kWriteProxy) {
-				const DynamicValueWriteProxy &proxy = indexableValueSlot.value.getWriteProxy();
-				if (!proxy.ifc->refAttrib(indexableValueSlot.value, proxy.objectRef, proxy.ptrOrOffset, attrib)) {
+				DynamicValueWriteProxy proxy = indexableValueSlot.value.getWriteProxyTEMP();
+				if (!proxy.pod.ifc->refAttrib(thread, proxy, proxy.pod.objectRef, proxy.pod.ptrOrOffset, attrib)) {
 					thread->error("Can't write to attribute '" + attrib + "'");
 					return kMiniscriptInstructionOutcomeFailed;
 				}
+				indexableValueSlot.value.setWriteProxy(proxy);
 			} else {
 				thread->error("Tried to l-value index something that was not writeable");
 				return kMiniscriptInstructionOutcomeFailed;
@@ -1099,8 +1105,8 @@ MiniscriptInstructionOutcome PushGlobal::executeFindFilteredParent(MiniscriptThr
 
 		if (isMatch)
 			break;
-		else if (obj->isElement()) {
-			ref = static_cast<Element *>(obj.get())->getParent()->getSelfReference();
+		else if (obj->isStructural()) {
+			ref = static_cast<Structural *>(obj.get())->getParent()->getSelfReference();
 		} else if (obj->isModifier()) {
 			ref = static_cast<Modifier *>(obj.get())->getParent();
 		} else {
@@ -1120,6 +1126,14 @@ MiniscriptInstructionOutcome PushGlobal::executeFindFilteredParent(MiniscriptThr
 PushString::PushString(const Common::String &str) : _str(str) {
 }
 
+MiniscriptInstructionOutcome PushString::execute(MiniscriptThread *thread) const {
+	DynamicValue str;
+	str.setString(_str);
+	thread->pushValue(str);
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 Jump::Jump(uint32 instrOffset, bool isConditional) : _instrOffset(instrOffset), _isConditional(isConditional) {
 }
 
@@ -1220,7 +1234,7 @@ MiniscriptInstructionOutcome MiniscriptThread::dereferenceRValue(size_t offset,
 			if (obj && obj->isModifier()) {
 				const Modifier *modifier = static_cast<const Modifier *>(obj.get());
 				if (modifier->isVariable()) {
-					static_cast<const VariableModifier *>(modifier)->getValue(stackValue.value);
+					static_cast<const VariableModifier *>(modifier)->varGetValue(stackValue.value);
 				}
 			}
 		} break;
@@ -1228,8 +1242,8 @@ MiniscriptInstructionOutcome MiniscriptThread::dereferenceRValue(size_t offset,
 		this->error("Attempted to dereference an lvalue proxy");
 		return kMiniscriptInstructionOutcomeFailed;
 	case DynamicValueTypes::kReadProxy: {
-			const DynamicValueReadProxy &readProxy = stackValue.value.getReadProxy();
-			if (!readProxy.ifc->read(stackValue.value, readProxy.objectRef, readProxy.ptrOrOffset)) {
+			const DynamicValueReadProxyPOD &readProxy = stackValue.value.getReadProxyPOD();
+			if (!readProxy.ifc->read(this, stackValue.value, readProxy.objectRef, readProxy.ptrOrOffset)) {
 				this->error("Failed to access a proxy value");
 				return kMiniscriptInstructionOutcomeFailed;
 			}
@@ -1300,7 +1314,7 @@ MiniscriptInstructionOutcome MiniscriptThread::tryLoadVariable(MiniscriptStackVa
 		Common::SharedPtr<RuntimeObject> obj = stackValue.value.getObject().object.lock();
 		if (obj && obj->isModifier() && static_cast<Modifier *>(obj.get())->isVariable()) {
 			VariableModifier *varMod = static_cast<VariableModifier *>(obj.get());
-			varMod->getValue(stackValue.value);
+			varMod->varGetValue(stackValue.value);
 		}
 	}
 
diff --git a/engines/mtropolis/miniscript.h b/engines/mtropolis/miniscript.h
index 045277f4929..587526d11c9 100644
--- a/engines/mtropolis/miniscript.h
+++ b/engines/mtropolis/miniscript.h
@@ -326,6 +326,8 @@ namespace MiniscriptInstructions {
 		explicit PushString(const Common::String &str);
 
 	private:
+		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
+
 		Common::String _str;
 	};
 
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index a07d8e03f02..0b3ae1d1266 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -788,7 +788,7 @@ bool CompoundVariableModifier::readAttribute(MiniscriptThread *thread, DynamicVa
 	if (!var || !var->isModifier())
 		return false;
 
-	static_cast<VariableModifier *>(var)->getValue(result);
+	static_cast<VariableModifier *>(var)->varGetValue(result);
 	return true;
 }
 
@@ -805,7 +805,7 @@ bool CompoundVariableModifier::writeRefAttribute(MiniscriptThread *thread, Dynam
 	if (!var || !var->isModifier())
 		return false;
 
-	writeProxy = DynamicValueWriteFuncHelper<VariableModifier, &VariableModifier::setValue>::create(static_cast<VariableModifier *>(var));
+	writeProxy = static_cast<VariableModifier *>(var)->createWriteProxy();
 	return true;
 }
 
@@ -840,7 +840,7 @@ bool BooleanVariableModifier::load(ModifierLoaderContext &context, const Data::B
 	return true;
 }
 
-bool BooleanVariableModifier::setValue(const DynamicValue &value) {
+bool BooleanVariableModifier::varSetValue(const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kBoolean)
 		_value = value.getBool();
 	else
@@ -849,7 +849,7 @@ bool BooleanVariableModifier::setValue(const DynamicValue &value) {
 	return true;
 }
 
-void BooleanVariableModifier::getValue(DynamicValue &dest) const {
+void BooleanVariableModifier::varGetValue(DynamicValue &dest) const {
 	dest.setBool(_value);
 }
 
@@ -866,7 +866,7 @@ bool IntegerVariableModifier::load(ModifierLoaderContext& context, const Data::I
 	return true;
 }
 
-bool IntegerVariableModifier::setValue(const DynamicValue &value) {
+bool IntegerVariableModifier::varSetValue(const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kFloat)
 		_value = static_cast<int32>(floor(value.getFloat() + 0.5));
 	else if (value.getType() == DynamicValueTypes::kInteger)
@@ -877,7 +877,7 @@ bool IntegerVariableModifier::setValue(const DynamicValue &value) {
 	return true;
 }
 
-void IntegerVariableModifier::getValue(DynamicValue &dest) const {
+void IntegerVariableModifier::varGetValue(DynamicValue &dest) const {
 	dest.setInt(_value);
 }
 
@@ -895,7 +895,7 @@ bool IntegerRangeVariableModifier::load(ModifierLoaderContext& context, const Da
 	return true;
 }
 
-bool IntegerRangeVariableModifier::setValue(const DynamicValue &value) {
+bool IntegerRangeVariableModifier::varSetValue(const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kIntegerRange)
 		_range = value.getIntRange();
 	else
@@ -904,7 +904,7 @@ bool IntegerRangeVariableModifier::setValue(const DynamicValue &value) {
 	return true;
 }
 
-void IntegerRangeVariableModifier::getValue(DynamicValue &dest) const {
+void IntegerRangeVariableModifier::varGetValue(DynamicValue &dest) const {
 	dest.setIntRange(_range);
 }
 
@@ -922,7 +922,7 @@ bool VectorVariableModifier::load(ModifierLoaderContext &context, const Data::Ve
 	return true;
 }
 
-bool VectorVariableModifier::setValue(const DynamicValue &value) {
+bool VectorVariableModifier::varSetValue(const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kVector)
 		_vector = value.getVector();
 	else
@@ -931,7 +931,7 @@ bool VectorVariableModifier::setValue(const DynamicValue &value) {
 	return true;
 }
 
-void VectorVariableModifier::getValue(DynamicValue &dest) const {
+void VectorVariableModifier::varGetValue(DynamicValue &dest) const {
 	dest.setVector(_vector);
 }
 
@@ -949,7 +949,7 @@ bool PointVariableModifier::load(ModifierLoaderContext &context, const Data::Poi
 	return true;
 }
 
-bool PointVariableModifier::setValue(const DynamicValue &value) {
+bool PointVariableModifier::varSetValue(const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kPoint)
 		_value = value.getPoint();
 	else
@@ -958,7 +958,7 @@ bool PointVariableModifier::setValue(const DynamicValue &value) {
 	return true;
 }
 
-void PointVariableModifier::getValue(DynamicValue &dest) const {
+void PointVariableModifier::varGetValue(DynamicValue &dest) const {
 	dest.setPoint(_value);
 }
 
@@ -975,7 +975,7 @@ bool FloatingPointVariableModifier::load(ModifierLoaderContext &context, const D
 	return true;
 }
 
-bool FloatingPointVariableModifier::setValue(const DynamicValue &value) {
+bool FloatingPointVariableModifier::varSetValue(const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kInteger)
 		_value = value.getInt();
 	else if (value.getType() == DynamicValueTypes::kFloat)
@@ -986,7 +986,7 @@ bool FloatingPointVariableModifier::setValue(const DynamicValue &value) {
 	return true;
 }
 
-void FloatingPointVariableModifier::getValue(DynamicValue &dest) const {
+void FloatingPointVariableModifier::varGetValue(DynamicValue &dest) const {
 	dest.setFloat(_value);
 }
 
@@ -1003,7 +1003,7 @@ bool StringVariableModifier::load(ModifierLoaderContext &context, const Data::St
 	return true;
 }
 
-bool StringVariableModifier::setValue(const DynamicValue &value) {
+bool StringVariableModifier::varSetValue(const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kString)
 		_value = value.getString();
 	else
@@ -1012,7 +1012,7 @@ bool StringVariableModifier::setValue(const DynamicValue &value) {
 	return true;
 }
 
-void StringVariableModifier::getValue(DynamicValue &dest) const {
+void StringVariableModifier::varGetValue(DynamicValue &dest) const {
 	dest.setString(_value);
 }
 
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 23abff26048..b89ed4861ea 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -589,8 +589,8 @@ class BooleanVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::BooleanVariableModifier &data);
 
-	bool setValue(const DynamicValue &value) override;
-	void getValue(DynamicValue &dest) const override;
+	bool varSetValue(const DynamicValue &value) override;
+	void varGetValue(DynamicValue &dest) const override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Boolean Variable Modifier"; }
@@ -607,8 +607,8 @@ class IntegerVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::IntegerVariableModifier &data);
 
-	bool setValue(const DynamicValue &value) override;
-	void getValue(DynamicValue &dest) const override;
+	bool varSetValue(const DynamicValue &value) override;
+	void varGetValue(DynamicValue &dest) const override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Integer Variable Modifier"; }
@@ -625,8 +625,8 @@ class IntegerRangeVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::IntegerRangeVariableModifier &data);
 
-	bool setValue(const DynamicValue &value) override;
-	void getValue(DynamicValue &dest) const override;
+	bool varSetValue(const DynamicValue &value) override;
+	void varGetValue(DynamicValue &dest) const override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Integer Range Variable Modifier"; }
@@ -643,8 +643,8 @@ class VectorVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::VectorVariableModifier &data);
 
-	bool setValue(const DynamicValue &value) override;
-	void getValue(DynamicValue &dest) const override;
+	bool varSetValue(const DynamicValue &value) override;
+	void varGetValue(DynamicValue &dest) const override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Vector Variable Modifier"; }
@@ -661,8 +661,8 @@ class PointVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::PointVariableModifier &data);
 
-	bool setValue(const DynamicValue &value) override;
-	void getValue(DynamicValue &dest) const override;
+	bool varSetValue(const DynamicValue &value) override;
+	void varGetValue(DynamicValue &dest) const override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Point Variable Modifier"; }
@@ -679,8 +679,8 @@ class FloatingPointVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::FloatingPointVariableModifier &data);
 
-	bool setValue(const DynamicValue &value) override;
-	void getValue(DynamicValue &dest) const override;
+	bool varSetValue(const DynamicValue &value) override;
+	void varGetValue(DynamicValue &dest) const override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Floating Point Variable Modifier"; }
@@ -697,8 +697,8 @@ class StringVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::StringVariableModifier &data);
 
-	bool setValue(const DynamicValue &value) override;
-	void getValue(DynamicValue &dest) const override;
+	bool varSetValue(const DynamicValue &value) override;
+	void varGetValue(DynamicValue &dest) const override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "String Variable Modifier"; }
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index c15a0b8309a..3faec701fb2 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -150,7 +150,7 @@ Common::SharedPtr<Modifier> MediaCueMessengerModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new MediaCueMessengerModifier(*this));
 }
 
-ObjectReferenceVariableModifier::ObjectReferenceVariableModifier() : _isResolved(false) {
+ObjectReferenceVariableModifier::ObjectReferenceVariableModifier() {
 }
 
 bool ObjectReferenceVariableModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::ObjectReferenceVariableModifier &data) {
@@ -163,43 +163,78 @@ bool ObjectReferenceVariableModifier::load(const PlugInModifierLoaderContext &co
 		return false;
 
 	_objectPath = data.objectPath.str;
-	_isResolved = false;
+	_object.reset();
 
 	return true;
 }
 
-bool ObjectReferenceVariableModifier::setValue(const DynamicValue &value) {
-	if (value.getType() == DynamicValueTypes::kNull) {
-		_object.reset();
-		_objectPath.clear();
-		_isResolved = true;
-	} else if (value.getType() == DynamicValueTypes::kObject) {
-		_object = value.getObject();
-		_objectPath.clear();
-		_isResolved = true;
-	} else {
-		return false;
+// Object reference variables are somewhat unusual in that they don't store a simple value,
+// they instead have "object" and "path" attributes AND as a value, they resolve to the
+// modifier itself.
+bool ObjectReferenceVariableModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "path") {
+		result.setString(_objectPath);
+		return true;
 	}
+	if (attrib == "object") {
+		if (_object.object.expired())
+			resolve();
 
-	return true;
+		if (_object.object.expired())
+			result.setObject(nullptr);
+		result.setObject(_object);
+		return true;
+	}
+
+	return VariableModifier::readAttribute(thread, result, attrib);
 }
 
-void ObjectReferenceVariableModifier::getValue(DynamicValue &dest) const {
-	if (_isResolved) {
-		if (_object.object.expired())
-			dest.clear();
-		else
-			dest.setObject(_object);
-	} else {
-		error("Resolving default objects from variable modifiers is not implemented!");
-		dest.clear();
+bool ObjectReferenceVariableModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
+	if (attrib == "path") {
+		DynamicValueWriteFuncHelper<ObjectReferenceVariableModifier, &ObjectReferenceVariableModifier::dynSetPath>::create(this, result);
+		return true;
 	}
+	if (attrib == "object") {
+		DynamicValueWriteFuncHelper<ObjectReferenceVariableModifier, &ObjectReferenceVariableModifier::dynSetObject>::create(this, result);
+		return true;
+	}
+
+	return VariableModifier::writeRefAttribute(thread, result, attrib);
+}
+
+bool ObjectReferenceVariableModifier::varSetValue(const DynamicValue &value) {
+	return false;
+}
+
+void ObjectReferenceVariableModifier::varGetValue(DynamicValue &dest) const {
+	dest.setObject(this->getSelfReference());
 }
 
 Common::SharedPtr<Modifier> ObjectReferenceVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new ObjectReferenceVariableModifier(*this));
 }
 
+bool ObjectReferenceVariableModifier::dynSetPath(const DynamicValue &value) {
+	if (value.getType() != DynamicValueTypes::kString)
+		return false;
+
+	_isResolved = false;
+	_objectPath = value.getString();
+	_object.reset();
+
+	return true;
+}
+bool ObjectReferenceVariableModifier::dynSetObject(const DynamicValue& value) {
+
+}
+
+void ObjectReferenceVariableModifier::resolve() {
+	if (_isResolved)
+		return;
+
+	_isResolved = true;
+}
+
 MidiModifier::MidiModifier() : _plugIn(nullptr) {
 }
 
@@ -338,7 +373,7 @@ bool ListVariableModifier::load(const PlugInModifierLoaderContext &context, cons
 	return true;
 }
 
-bool ListVariableModifier::setValue(const DynamicValue &value) {
+bool ListVariableModifier::varSetValue(const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kList)
 		_list = value.getList()->clone();
 	else
@@ -347,7 +382,7 @@ bool ListVariableModifier::setValue(const DynamicValue &value) {
 	return true;
 }
 
-void ListVariableModifier::getValue(DynamicValue &dest) const {
+void ListVariableModifier::varGetValue(DynamicValue &dest) const {
 	dest.setList(_list);
 }
 
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index 47be0bda0af..c3b9ce7f1fb 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -96,8 +96,11 @@ public:
 
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::ObjectReferenceVariableModifier &data);
 
-	bool setValue(const DynamicValue &value) override;
-	void getValue(DynamicValue &dest) const override;
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
+	bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib);
+
+	bool varSetValue(const DynamicValue &value) override;
+	void varGetValue(DynamicValue &dest) const override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Object Reference Variable Modifier"; }
@@ -106,11 +109,15 @@ public:
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
+	bool dynSetPath(const DynamicValue &value);
+	bool dynSetObject(const DynamicValue &value);
+
+	void resolve();
+
 	Event _setToSourceParentWhen;
 
 	mutable ObjectReference _object;
 	mutable Common::String _objectPath;
-	mutable bool _isResolved;
 };
 
 class MidiModifier : public Modifier {
@@ -176,8 +183,8 @@ public:
 
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::ListVariableModifier &data);
 
-	bool setValue(const DynamicValue &value) override;
-	void getValue(DynamicValue &dest) const override;
+	bool varSetValue(const DynamicValue &value) override;
+	void varGetValue(DynamicValue &dest) const override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "List Variable Modifier"; }
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index abaaa3a922f..5d4d2a6b727 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -226,6 +226,19 @@ bool Point16::load(const Data::Point &point) {
 	return true;
 }
 
+bool Point16::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib) {
+	if (attrib == "x") {
+		DynamicValueWriteIntegerHelper<int16>::create(&x, proxy);
+		return true;
+	}
+	if (attrib == "y") {
+		DynamicValueWriteIntegerHelper<int16>::create(&y, proxy);
+		return true;
+	}
+
+	return false;
+}
+
 bool Rect16::load(const Data::Rect &rect) {
 	top = rect.top;
 	left = rect.left;
@@ -245,6 +258,19 @@ bool IntRange::load(const Data::IntRange &range) {
 	return true;
 }
 
+bool IntRange::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib) {
+	if (attrib == "start") {
+		DynamicValueWriteIntegerHelper<int32>::create(&min, proxy);
+		return true;
+	}
+	if (attrib == "y") {
+		DynamicValueWriteIntegerHelper<int32>::create(&max, proxy);
+		return true;
+	}
+
+	return false;
+}
+
 bool Label::load(const Data::Label &label) {
 	id = label.labelID;
 	superGroupID = label.superGroupID;
@@ -441,6 +467,10 @@ bool DynamicListContainer<void>::setAtIndex(size_t index, const DynamicValue &dy
 	return true;
 }
 
+bool DynamicListContainer<void>::expandToMinimumSize(size_t sz) {
+	return false;
+}
+
 bool DynamicListContainer<void>::getAtIndex(size_t index, DynamicValue &dynValue) const {
 	dynValue.clear();
 	return true;
@@ -454,6 +484,10 @@ const void *DynamicListContainer<void>::getConstArrayPtr() const {
 	return nullptr;
 }
 
+void *DynamicListContainer<void>::getArrayPtr() {
+	return nullptr;
+}
+
 size_t DynamicListContainer<void>::getSize() const {
 	return _size;
 }
@@ -477,7 +511,7 @@ bool DynamicListContainer<VarReference>::setAtIndex(size_t index, const DynamicV
 		_array.resize(requiredSize);
 		_strings.resize(requiredSize);
 
-		for (size_t i = prevSize; i < index; prevSize++) {
+		for (size_t i = prevSize; i < index; i++) {
 			_array[i].guid = 0;
 		}
 
@@ -495,6 +529,21 @@ bool DynamicListContainer<VarReference>::setAtIndex(size_t index, const DynamicV
 	return true;
 }
 
+bool DynamicListContainer<VarReference>::expandToMinimumSize(size_t sz) {
+	if (_array.size() < sz) {
+		size_t prevSize = _array.size();
+		_array.resize(sz);
+		_strings.resize(sz);
+
+		for (size_t i = prevSize; i < sz; prevSize++) {
+			_array[i].guid = 0;
+			_array[i].source = nullptr;
+		}
+	}
+
+	return true;
+}
+
 bool DynamicListContainer<VarReference>::getAtIndex(size_t index, DynamicValue &dynValue) const {
 	// TODO: Refactor this whole thing to use linkInternalReferences
 	if (index >= _array.size())
@@ -516,6 +565,10 @@ const void *DynamicListContainer<VarReference>::getConstArrayPtr() const {
 	return &_array;
 }
 
+void *DynamicListContainer<VarReference>::getArrayPtr() {
+	return &_array;
+}
+
 size_t DynamicListContainer<VarReference>::getSize() const {
 	return _array.size();
 }
@@ -603,6 +656,76 @@ const Common::Array<bool> &DynamicList::getBool() const {
 	return *static_cast<const Common::Array<bool> *>(_container->getConstArrayPtr());
 }
 
+const Common::Array<Common::SharedPtr<DynamicList> > &DynamicList::getList() const {
+	assert(_type == DynamicValueTypes::kList);
+	return *static_cast<const Common::Array<Common::SharedPtr<DynamicList> > *>(_container->getConstArrayPtr());
+}
+
+const Common::Array<ObjectReference> &DynamicList::getObjectReference() const {
+	assert(_type == DynamicValueTypes::kObject);
+	return *static_cast<const Common::Array<ObjectReference> *>(_container->getConstArrayPtr());
+}
+
+Common::Array<int32> &DynamicList::getInt() {
+	assert(_type == DynamicValueTypes::kInteger);
+	return *static_cast<Common::Array<int32> *>(_container->getArrayPtr());
+}
+
+Common::Array<double> &DynamicList::getFloat() {
+	assert(_type == DynamicValueTypes::kFloat);
+	return *static_cast<Common::Array<double> *>(_container->getArrayPtr());
+}
+
+Common::Array<Point16> &DynamicList::getPoint() {
+	assert(_type == DynamicValueTypes::kPoint);
+	return *static_cast<Common::Array<Point16> *>(_container->getArrayPtr());
+}
+
+Common::Array<IntRange> &DynamicList::getIntRange() {
+	assert(_type == DynamicValueTypes::kIntegerRange);
+	return *static_cast<Common::Array<IntRange> *>(_container->getArrayPtr());
+}
+
+Common::Array<AngleMagVector> &DynamicList::getVector() {
+	assert(_type == DynamicValueTypes::kVector);
+	return *static_cast<Common::Array<AngleMagVector> *>(_container->getArrayPtr());
+}
+
+Common::Array<Label> &DynamicList::getLabel() {
+	assert(_type == DynamicValueTypes::kLabel);
+	return *static_cast<Common::Array<Label> *>(_container->getArrayPtr());
+}
+
+Common::Array<Event> &DynamicList::getEvent() {
+	assert(_type == DynamicValueTypes::kEvent);
+	return *static_cast<Common::Array<Event> *>(_container->getArrayPtr());
+}
+
+Common::Array<VarReference> &DynamicList::getVarReference() {
+	assert(_type == DynamicValueTypes::kVariableReference);
+	return *static_cast<Common::Array<VarReference> *>(_container->getArrayPtr());
+}
+
+Common::Array<Common::String> &DynamicList::getString() {
+	assert(_type == DynamicValueTypes::kString);
+	return *static_cast<Common::Array<Common::String> *>(_container->getArrayPtr());
+}
+
+Common::Array<bool> &DynamicList::getBool() {
+	assert(_type == DynamicValueTypes::kBoolean);
+	return *static_cast<Common::Array<bool> *>(_container->getArrayPtr());
+}
+
+Common::Array<Common::SharedPtr<DynamicList> > &DynamicList::getList() {
+	assert(_type == DynamicValueTypes::kList);
+	return *static_cast<Common::Array<Common::SharedPtr<DynamicList> > *>(_container->getArrayPtr());
+}
+
+Common::Array<ObjectReference> &DynamicList::getObjectReference() {
+	assert(_type == DynamicValueTypes::kObject);
+	return *static_cast<Common::Array<ObjectReference> *>(_container->getArrayPtr());
+}
+
 bool DynamicList::setAtIndex(size_t index, const DynamicValue &value) {
 	if (_type != value.getType()) {
 		if (_container != nullptr && _container->getSize() != 0)
@@ -617,6 +740,11 @@ bool DynamicList::setAtIndex(size_t index, const DynamicValue &value) {
 	}
 }
 
+void DynamicList::expandToMinimumSize(size_t sz) {
+	if (_container)
+		_container->expandToMinimumSize(sz);
+}
+
 bool DynamicList::getAtIndex(size_t index, DynamicValue &value) const {
 	if (_container == nullptr || index >= _container->getSize())
 		return false;
@@ -694,6 +822,12 @@ Common::SharedPtr<DynamicList> DynamicList::clone() const {
 	return clonedList;
 }
 
+void DynamicList::createWriteProxyForIndex(size_t index, DynamicValueWriteProxy &proxy) {
+	proxy.pod.ifc = &WriteProxyInterface::_instance;
+	proxy.pod.objectRef = this;
+	proxy.pod.ptrOrOffset = index;
+}
+
 bool DynamicList::changeToType(DynamicValueTypes::DynamicValueType type) {
 	switch (type) {
 	case DynamicValueTypes::kNull:
@@ -762,6 +896,76 @@ void DynamicList::initFromOther(const DynamicList &other) {
 	}
 }
 
+bool DynamicList::WriteProxyInterface::write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const {
+	return static_cast<DynamicList *>(objectRef)->setAtIndex(ptrOrOffset, value);
+}
+
+bool DynamicList::WriteProxyInterface::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const {
+	DynamicList *list = static_cast<DynamicList *>(objectRef);
+	switch (list->getType()) {
+	case DynamicValueTypes::kPoint:
+		list->expandToMinimumSize(ptrOrOffset + 1);
+		return list->getPoint()[ptrOrOffset].refAttrib(thread, proxy, attrib);
+		break;
+	case DynamicValueTypes::kIntegerRange:
+		list->expandToMinimumSize(ptrOrOffset + 1);
+		return list->getIntRange()[ptrOrOffset].refAttrib(thread, proxy, attrib);
+	case DynamicValueTypes::kVector:
+		list->expandToMinimumSize(ptrOrOffset + 1);
+		return list->getVector()[ptrOrOffset].refAttrib(thread, proxy, attrib);
+	case DynamicValueTypes::kObject: {
+			if (list->getSize() <= ptrOrOffset)
+				return false;
+
+			Common::SharedPtr<RuntimeObject> obj = list->getObjectReference()[ptrOrOffset].object.lock();
+			proxy.containerList.reset();
+			if (!obj && !obj->writeRefAttribute(thread, proxy, attrib))
+				return false;
+
+			return true;
+		}
+	default:
+		return false;
+	}
+}
+
+bool DynamicList::WriteProxyInterface::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const {
+	DynamicList *list = static_cast<DynamicList *>(objectRef);
+	switch (list->getType()) {
+	case DynamicValueTypes::kList: {
+			if (list->getSize() <= ptrOrOffset)
+				return false;
+
+			Common::SharedPtr<DynamicList> subList = list->getList()[ptrOrOffset];
+
+			size_t subIndex = 0;
+			if (!subList->dynamicValueToIndex(subIndex, index))
+				return false;
+
+			subList->createWriteProxyForIndex(subIndex, proxy);
+			proxy.containerList = subList;
+			return true;
+		}
+	case DynamicValueTypes::kObject: {
+			if (list->getSize() <= ptrOrOffset)
+				return false;
+
+			Common::SharedPtr<RuntimeObject> obj = list->getObjectReference()[ptrOrOffset].object.lock();
+			proxy.containerList.reset();
+			if (!obj && !obj->writeRefAttributeIndexed(thread, proxy, attrib, index))
+				return false;
+
+			return true;
+		}
+	default:
+		return false;
+	}
+
+	return false;
+}
+
+DynamicList::WriteProxyInterface DynamicList::WriteProxyInterface::_instance;
+
 DynamicValue::DynamicValue() : _type(DynamicValueTypes::kNull) {
 }
 
@@ -948,22 +1152,41 @@ const ObjectReference &DynamicValue::getObject() const {
 	return _obj;
 }
 
-const DynamicValueReadProxy& DynamicValue::getReadProxy() const {
+const DynamicValueReadProxyPOD &DynamicValue::getReadProxyPOD() const {
 	assert(_type == DynamicValueTypes::kReadProxy);
 	return _value.asReadProxy;
 }
 
-const DynamicValueWriteProxy &DynamicValue::getWriteProxy() const {
+const DynamicValueWriteProxyPOD &DynamicValue::getWriteProxyPOD() const {
 	assert(_type == DynamicValueTypes::kWriteProxy);
 	return _value.asWriteProxy;
 }
 
-const Common::SharedPtr<DynamicList> &DynamicValue::getReadProxyList() const {
+
+DynamicValueReadProxy DynamicValue::getReadProxyTEMP() const {
+	assert(_type == DynamicValueTypes::kReadProxy);
+
+	DynamicValueReadProxy proxy;
+	proxy.pod = _value.asReadProxy;
+	proxy.containerList = _list;
+	return proxy;
+}
+
+DynamicValueWriteProxy DynamicValue::getWriteProxyTEMP() const {
+	assert(_type == DynamicValueTypes::kWriteProxy);
+
+	DynamicValueWriteProxy proxy;
+	proxy.pod = _value.asWriteProxy;
+	proxy.containerList = _list;
+	return proxy;
+}
+
+const Common::SharedPtr<DynamicList> &DynamicValue::getReadProxyContainer() const {
 	assert(_type == DynamicValueTypes::kReadProxy);
 	return _list;
 }
 
-const Common::SharedPtr<DynamicList> &DynamicValue::getWriteProxyList() const {
+const Common::SharedPtr<DynamicList> &DynamicValue::getWriteProxyContainer() const {
 	assert(_type == DynamicValueTypes::kWriteProxy);
 	return _list;
 }
@@ -1047,21 +1270,21 @@ void DynamicValue::setList(const Common::SharedPtr<DynamicList> &value) {
 	_list = value;
 }
 
-void DynamicValue::setReadProxy(const Common::SharedPtr<DynamicList> &list, const DynamicValueReadProxy &readProxy) {
-	Common::SharedPtr<DynamicList> listRef = list;	// Back up list ref in case this is a self-assign
+void DynamicValue::setReadProxy(const DynamicValueReadProxy &readProxy) {
+	Common::SharedPtr<DynamicList> listRef = readProxy.containerList;	// Back up list ref in case this is a self-assign
 	if (_type != DynamicValueTypes::kReadProxy)
 		clear();
 	_type = DynamicValueTypes::kReadProxy;
-	_value.asReadProxy = readProxy;
+	_value.asReadProxy = readProxy.pod;
 	_list = listRef;
 }
 
-void DynamicValue::setWriteProxy(const Common::SharedPtr<DynamicList> &list, const DynamicValueWriteProxy &writeProxy) {
-	Common::SharedPtr<DynamicList> listRef = list; // Back up list ref in case this is a self-assign
+void DynamicValue::setWriteProxy(const DynamicValueWriteProxy &writeProxy) {
+	Common::SharedPtr<DynamicList> listRef = writeProxy.containerList; // Back up list ref in case this is a self-assign
 	if (_type != DynamicValueTypes::kWriteProxy)
 		clear();
 	_type = DynamicValueTypes::kWriteProxy;
-	_value.asWriteProxy = writeProxy;
+	_value.asWriteProxy = writeProxy.pod;
 	_list = listRef;
 }
 
@@ -1416,6 +1639,43 @@ bool Event::load(const Data::Event &data) {
 	return true;
 }
 
+bool AngleMagVector::dynSetAngleDegrees(const DynamicValue &value) {
+	double degrees = 0.0;
+	switch (value.getType())
+	{
+	case DynamicValueTypes::kInteger:
+		degrees = static_cast<double>(value.getInt());
+		break;
+	case DynamicValueTypes::kFloat:
+		degrees = value.getFloat();
+		break;
+	default:
+		return false;
+	}
+
+	angleRadians = degrees * (M_PI / 180.0);
+
+	return true;
+}
+
+void AngleMagVector::dynGetAngleDegrees(DynamicValue &value) const {
+	value.setFloat(angleRadians * (180.0 / M_PI));
+}
+
+
+bool AngleMagVector::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib) {
+	if (attrib == "angle") {
+		DynamicValueWriteFuncHelper<AngleMagVector, &AngleMagVector::dynSetAngleDegrees>::create(this, proxy);
+		return true;
+	}
+	if (attrib == "magnitude") {
+		DynamicValueWriteFloatHelper<double>::create(&magnitude, proxy);
+		return true;
+	}
+
+	return false;
+}
+
 void IPlugInModifierRegistrar::registerPlugInModifier(const char *name, const IPlugInModifierFactoryAndDataFactory *loaderFactory) {
 	return this->registerPlugInModifier(name, loaderFactory, loaderFactory);
 }
@@ -3996,11 +4256,11 @@ bool VisualElement::readAttribute(MiniscriptThread *thread, DynamicValue &result
 
 bool VisualElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
 	if (attrib == "visible") {
-		writeProxy = DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetVisibility>::create(this);
+		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetVisibility>::create(this, writeProxy);
 		return true;
 	}
 	if (attrib == "direct") {
-		writeProxy = DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetDirect>::create(this);
+		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetDirect>::create(this, writeProxy);
 		return true;
 	}
 
@@ -4200,6 +4460,28 @@ bool VariableModifier::isVariable() const {
 	return true;
 }
 
+DynamicValueWriteProxy VariableModifier::createWriteProxy() {
+	DynamicValueWriteProxy proxy;
+	proxy.pod.objectRef = this;
+	proxy.pod.ptrOrOffset = 0;
+	proxy.pod.ifc = &VariableModifier::WriteProxyInterface::_instance;
+	return proxy;
+}
+
+bool VariableModifier::WriteProxyInterface::write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const {
+	return static_cast<VariableModifier *>(objectRef)->varSetValue(dest);
+}
+
+bool VariableModifier::WriteProxyInterface::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &dest, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const {
+	return static_cast<VariableModifier *>(objectRef)->writeRefAttribute(thread, dest, attrib);
+}
+
+bool VariableModifier::WriteProxyInterface::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &dest, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const {
+	return static_cast<VariableModifier *>(objectRef)->writeRefAttributeIndexed(thread, dest, attrib, index);
+}
+
+VariableModifier::WriteProxyInterface VariableModifier::WriteProxyInterface::_instance;
+
 bool Modifier::loadTypicalHeader(const Data::TypicalModifierHeader &typicalHeader) {
 	if (!_modifierFlags.load(typicalHeader.modifierFlags))
 		return false;
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 34713207323..f4af3ea2d92 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -70,6 +70,7 @@ class Runtime;
 class Structural;
 class VisualElement;
 class Window;
+struct DynamicValue;
 struct IMessageConsumer;
 struct IModifierContainer;
 struct IModifierFactory;
@@ -264,6 +265,8 @@ struct Point16 {
 		result.y = y;
 		return result;
 	}
+
+	bool refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib);
 };
 
 struct Rect16 {
@@ -307,6 +310,8 @@ struct IntRange {
 	inline bool operator!=(const IntRange &other) const {
 		return !((*this) == other);
 	}
+
+	bool refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib);
 };
 
 struct Label {
@@ -392,6 +397,11 @@ struct AngleMagVector {
 	inline bool operator!=(const AngleMagVector &other) const {
 		return !((*this) == other);
 	}
+
+	bool dynSetAngleDegrees(const DynamicValue &value);
+	void dynGetAngleDegrees(DynamicValue &value) const;
+
+	bool refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib);
 };
 
 struct ColorRGB8 {
@@ -414,63 +424,49 @@ struct DynamicValue;
 struct DynamicList;
 
 struct IDynamicValueReadInterface {
-	virtual bool read(DynamicValue &dest, const void *objectRef, uintptr_t ptrOrOffset) const = 0;
-	virtual bool readAttrib(DynamicValue &dest, const void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const = 0;
-	virtual bool readAttribIndexed(DynamicValue &dest, const void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const = 0;
+	virtual bool read(MiniscriptThread *thread, DynamicValue &dest, const void *objectRef, uintptr_t ptrOrOffset) const = 0;
+	virtual bool readAttrib(MiniscriptThread *thread, DynamicValue &dest, const void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const = 0;
+	virtual bool readAttribIndexed(MiniscriptThread *thread, DynamicValue &dest, const void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const = 0;
 };
 
 struct IDynamicValueWriteInterface {
-	virtual bool write(const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const = 0;
-	virtual bool refAttrib(DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const = 0;
-	virtual bool refAttribIndexed(DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const = 0;
+	virtual bool write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const = 0;
+	virtual bool refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const = 0;
+	virtual bool refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const = 0;
 };
 
-struct DynamicValueReadProxy {
+struct DynamicValueReadProxyPOD {
 	uintptr_t ptrOrOffset;
 	const void *objectRef;
 	IDynamicValueReadInterface *ifc;
 };
 
-struct DynamicValueWriteProxy {
+struct DynamicValueReadProxy {
+	DynamicValueReadProxyPOD pod;
+	Common::SharedPtr<DynamicList> containerList;
+};
+
+struct DynamicValueWriteProxyPOD {
 	uintptr_t ptrOrOffset;
 	void *objectRef;
 	IDynamicValueWriteInterface *ifc;
 };
 
-template<class TClass, bool (TClass::*TWriteMethod)(const DynamicValue &dest)>
-struct DynamicValueWriteFuncHelper : public IDynamicValueWriteInterface {
-	bool write(const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const override {
-		return (static_cast<TClass *>(objectRef)->*TWriteMethod)(dest);
-	}
-	bool refAttrib(DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override {
-		return false;
-	}
-	bool refAttribIndexed(DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override {
-		return false;
-	}
-
-	static DynamicValueWriteProxy create(TClass *obj) {
-		DynamicValueWriteProxy proxy;
-		proxy.ptrOrOffset = 0;
-		proxy.objectRef = obj;
-		proxy.ifc = &_instance;
-		return proxy;
-	}
-
-private:
-	static DynamicValueWriteFuncHelper _instance;
+struct DynamicValueWriteProxy {
+	DynamicValueWriteProxyPOD pod;
+	Common::SharedPtr<DynamicList> containerList;
 };
 
-template<class TClass, bool (TClass::*TWriteMethod)(const DynamicValue &dest)>
-DynamicValueWriteFuncHelper<TClass, TWriteMethod> DynamicValueWriteFuncHelper<TClass, TWriteMethod>::_instance;
 
 class DynamicListContainerBase {
 public:
 	virtual ~DynamicListContainerBase();
 	virtual bool setAtIndex(size_t index, const DynamicValue &dynValue) = 0;
 	virtual bool getAtIndex(size_t index, DynamicValue &dynValue) const = 0;
+	virtual bool expandToMinimumSize(size_t sz) = 0;
 	virtual void setFrom(const DynamicListContainerBase &other) = 0; // Only supports setting same type!
 	virtual const void *getConstArrayPtr() const = 0;
+	virtual void *getArrayPtr() = 0;
 	virtual size_t getSize() const = 0;
 	virtual bool compareEqual(const DynamicListContainerBase &other) const = 0;
 	virtual DynamicListContainerBase *clone() const = 0;
@@ -523,8 +519,10 @@ class DynamicListContainer : public DynamicListContainerBase {
 public:
 	bool setAtIndex(size_t index, const DynamicValue &dynValue) override;
 	bool getAtIndex(size_t index, DynamicValue &dynValue) const override;
+	bool expandToMinimumSize(size_t sz) override;
 	void setFrom(const DynamicListContainerBase &other) override;
 	const void *getConstArrayPtr() const override;
+	void *getArrayPtr() override;
 	size_t getSize() const override;
 	bool compareEqual(const DynamicListContainerBase &other) const override;
 	DynamicListContainerBase *clone() const override;
@@ -540,8 +538,10 @@ public:
 
 	bool setAtIndex(size_t index, const DynamicValue &dynValue) override;
 	bool getAtIndex(size_t index, DynamicValue &dynValue) const override;
+	bool expandToMinimumSize(size_t sz) override;
 	void setFrom(const DynamicListContainerBase &other) override;
 	const void *getConstArrayPtr() const override;
+	void *getArrayPtr() override;
 	size_t getSize() const override;
 	bool compareEqual(const DynamicListContainerBase &other) const override;
 	DynamicListContainerBase *clone() const override;
@@ -555,8 +555,10 @@ class DynamicListContainer<VarReference> : public DynamicListContainerBase {
 public:
 	bool setAtIndex(size_t index, const DynamicValue &dynValue) override;
 	bool getAtIndex(size_t index, DynamicValue &dynValue) const override;
+	bool expandToMinimumSize(size_t sz) override;
 	void setFrom(const DynamicListContainerBase &other) override;
 	const void *getConstArrayPtr() const override;
+	void *getArrayPtr() override;
 	size_t getSize() const override;
 	bool compareEqual(const DynamicListContainerBase &other) const override;
 	DynamicListContainerBase *clone() const override;
@@ -591,6 +593,20 @@ bool DynamicListContainer<T>::setAtIndex(size_t index, const DynamicValue &dynVa
 	return true;
 }
 
+template<class T>
+bool DynamicListContainer<T>::expandToMinimumSize(size_t sz) {
+	_array.reserve(sz);
+	if (_array.size() < sz) {
+		T defaultValue;
+		DynamicListDefaultSetter::defaultSet(defaultValue);
+		while (_array.size() < sz) {
+			_array.push_back(defaultValue);
+		}
+	}
+
+	return true;
+}
+
 template<class T>
 bool DynamicListContainer<T>::getAtIndex(size_t index, DynamicValue &dynValue) const {
 	if (index >= _array.size())
@@ -610,6 +626,11 @@ const void *DynamicListContainer<T>::getConstArrayPtr() const {
 	return &_array;
 }
 
+template<class T>
+void *DynamicListContainer<T>::getArrayPtr() {
+	return &_array;
+}
+
 template<class T>
 size_t DynamicListContainer<T>::getSize() const {
 	return _array.size();
@@ -643,9 +664,25 @@ struct DynamicList {
 	const Common::Array<VarReference> &getVarReference() const;
 	const Common::Array<Common::String> &getString() const;
 	const Common::Array<bool> &getBool() const;
+	const Common::Array<Common::SharedPtr<DynamicList> > &getList() const;
+	const Common::Array<ObjectReference> &getObjectReference() const;
+
+	Common::Array<int32> &getInt();
+	Common::Array<double> &getFloat();
+	Common::Array<Point16> &getPoint();
+	Common::Array<IntRange> &getIntRange();
+	Common::Array<AngleMagVector> &getVector();
+	Common::Array<Label> &getLabel();
+	Common::Array<Event> &getEvent();
+	Common::Array<VarReference> &getVarReference();
+	Common::Array<Common::String> &getString();
+	Common::Array<bool> &getBool();
+	Common::Array<Common::SharedPtr<DynamicList> > &getList();
+	Common::Array<ObjectReference> &getObjectReference();
 
 	bool getAtIndex(size_t index, DynamicValue &value) const;
 	bool setAtIndex(size_t index, const DynamicValue &value);
+	void expandToMinimumSize(size_t sz);
 	size_t getSize() const;
 
 	static bool dynamicValueToIndex(size_t &outIndex, const DynamicValue &value);
@@ -661,7 +698,17 @@ struct DynamicList {
 
 	Common::SharedPtr<DynamicList> clone() const;
 
+	void createWriteProxyForIndex(size_t index, DynamicValueWriteProxy &proxy);
+
 private:
+	struct WriteProxyInterface : public IDynamicValueWriteInterface {
+		bool write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const override;
+		bool refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override;
+		bool refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
+
+		static WriteProxyInterface _instance;
+	};
+
 	void clear();
 	void initFromOther(const DynamicList &other);
 	bool changeToType(DynamicValueTypes::DynamicValueType type);
@@ -694,10 +741,12 @@ struct DynamicValue {
 	const bool &getBool() const;
 	const Common::SharedPtr<DynamicList> &getList() const;
 	const ObjectReference &getObject() const;
-	const DynamicValueReadProxy &getReadProxy() const;
-	const DynamicValueWriteProxy &getWriteProxy() const;
-	const Common::SharedPtr<DynamicList> &getReadProxyList() const;
-	const Common::SharedPtr<DynamicList> &getWriteProxyList() const;
+	const DynamicValueReadProxyPOD &getReadProxyPOD() const;
+	const DynamicValueWriteProxyPOD &getWriteProxyPOD() const;
+	DynamicValueReadProxy getReadProxyTEMP() const;
+	DynamicValueWriteProxy getWriteProxyTEMP() const;
+	const Common::SharedPtr<DynamicList> &getReadProxyContainer() const;
+	const Common::SharedPtr<DynamicList> &getWriteProxyContainer() const;
 
 	void clear();
 
@@ -714,8 +763,8 @@ struct DynamicValue {
 	void setList(const Common::SharedPtr<DynamicList> &value);
 	void setObject(const ObjectReference &value);
 	void setObject(const Common::WeakPtr<RuntimeObject> &value);
-	void setReadProxy(const Common::SharedPtr<DynamicList> &list, const DynamicValueReadProxy &readProxy);
-	void setWriteProxy(const Common::SharedPtr<DynamicList> &list, const DynamicValueWriteProxy &writeProxy);
+	void setReadProxy(const DynamicValueReadProxy &readProxy);
+	void setWriteProxy(const DynamicValueWriteProxy &writeProxy);
 
 	DynamicValue &operator=(const DynamicValue &other);
 
@@ -737,8 +786,8 @@ private:
 		Event asEvent;
 		Point16 asPoint;
 		bool asBool;
-		DynamicValueReadProxy asReadProxy;
-		DynamicValueWriteProxy asWriteProxy;
+		DynamicValueReadProxyPOD asReadProxy;
+		DynamicValueWriteProxyPOD asWriteProxy;
 	};
 
 	template<class T>
@@ -757,6 +806,101 @@ private:
 	ObjectReference _obj;
 };
 
+template<class TFloat>
+struct DynamicValueWriteFloatHelper : public IDynamicValueWriteInterface {
+	bool write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const override {
+		TFloat &dest = *static_cast<TFloat *>(objectRef);
+		switch (value.getType()) {
+		case DynamicValueTypes::kFloat:
+			dest = static_cast<TFloat>(value.getFloat());
+			return true;
+		case DynamicValueTypes::kInteger:
+			dest = static_cast<TFloat>(value.getInt());
+			return true;
+		default:
+			return false;
+		}
+	}
+	bool refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override {
+		return false;
+	}
+	bool refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override {
+		return false;
+	}
+
+	static void create(TFloat *floatValue, DynamicValueWriteProxy &proxy) {
+		proxy.pod.ptrOrOffset = 0;
+		proxy.pod.objectRef = floatValue;
+		proxy.pod.ifc = &_instance;
+	}
+
+private:
+	static DynamicValueWriteFloatHelper _instance;
+};
+
+template<class TFloat>
+DynamicValueWriteFloatHelper<TFloat> DynamicValueWriteFloatHelper<TFloat>::_instance;
+
+template<class TInteger>
+struct DynamicValueWriteIntegerHelper : public IDynamicValueWriteInterface {
+	bool write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const override {
+		TInteger &dest = *static_cast<TInteger *>(objectRef);
+		switch (value.getType()) {
+		case DynamicValueTypes::kFloat:
+			dest = static_cast<TInteger>(floor(value.getFloat() + 0.5));
+			return true;
+		case DynamicValueTypes::kInteger:
+			dest = static_cast<TInteger>(value.getInt());
+			return true;
+		default:
+			return false;
+		}
+	}
+	bool refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override {
+		return false;
+	}
+	bool refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override {
+		return false;
+	}
+
+	static void create(TInteger *intValue, DynamicValueWriteProxy &proxy) {
+		proxy.pod.ptrOrOffset = 0;
+		proxy.pod.objectRef = intValue;
+		proxy.pod.ifc = &_instance;
+	}
+
+private:
+	static DynamicValueWriteIntegerHelper _instance;
+};
+
+template<class TInteger>
+DynamicValueWriteIntegerHelper<TInteger> DynamicValueWriteIntegerHelper<TInteger>::_instance;
+
+template<class TClass, bool (TClass::*TWriteMethod)(const DynamicValue &dest)>
+struct DynamicValueWriteFuncHelper : public IDynamicValueWriteInterface {
+	bool write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const override {
+		return (static_cast<TClass *>(objectRef)->*TWriteMethod)(dest);
+	}
+	bool refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override {
+		return false;
+	}
+	bool refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override {
+		return false;
+	}
+
+	static void create(TClass *obj, DynamicValueWriteProxy &proxy) {
+		proxy.pod.ptrOrOffset = 0;
+		proxy.pod.objectRef = obj;
+		proxy.pod.ifc = &_instance;
+	}
+
+private:
+	static DynamicValueWriteFuncHelper _instance;
+};
+
+template<class TClass, bool (TClass::*TWriteMethod)(const DynamicValue &dest)>
+DynamicValueWriteFuncHelper<TClass, TWriteMethod> DynamicValueWriteFuncHelper<TClass, TWriteMethod>::_instance;
+
 struct MessengerSendSpec {
 	MessengerSendSpec();
 	bool load(const Data::Event &dataEvent, uint32 dataMessageFlags, const Data::InternalTypeTaggedValue &dataLocator, const Common::String &dataWithSource, const Common::String &dataWithString, uint32 dataDestination);
@@ -1271,8 +1415,8 @@ public:
 
 	virtual bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
 	virtual bool readAttributeIndexed(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib, const DynamicValue &index);
-	virtual bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
-	virtual bool writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib, const DynamicValue &index);
+	virtual bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib);
+	virtual bool writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib, const DynamicValue &index);
 
 protected:
 	// This is the static GUID stored in the data, it is not guaranteed
@@ -1756,8 +1900,19 @@ protected:
 class VariableModifier : public Modifier {
 public:
 	virtual bool isVariable() const;
-	virtual bool setValue(const DynamicValue &value) = 0;
-	virtual void getValue(DynamicValue &dest) const = 0;
+	virtual bool varSetValue(const DynamicValue &value) = 0;
+	virtual void varGetValue(DynamicValue &dest) const = 0;
+
+	virtual DynamicValueWriteProxy createWriteProxy();
+
+private:
+	struct WriteProxyInterface : public IDynamicValueWriteInterface {
+		bool write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const override;
+		bool refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override;
+		bool refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
+
+		static WriteProxyInterface _instance;
+	};
 };
 
 enum AssetType {


Commit: f36ac72be3919a36636b6051e444e91e8d03ef0c
    https://github.com/scummvm/scummvm/commit/f36ac72be3919a36636b6051e444e91e8d03ef0c
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: More script work

Changed paths:
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/miniscript.h
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 24f04e8d346..aeee0cd0fb9 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -488,7 +488,7 @@ MiniscriptInstructionOutcome Set::execute(MiniscriptThread *thread) const {
 		}
 
 		if (var != nullptr) {
-			if (!var->varSetValue(srcValue.value)) {
+			if (!var->varSetValue(thread, srcValue.value)) {
 				thread->error("Couldn't assign value to variable, probably wrong type");
 				return kMiniscriptInstructionOutcomeFailed;
 			}
@@ -752,6 +752,39 @@ MiniscriptInstructionOutcome OrderedCompareInstruction::execute(MiniscriptThread
 BuiltinFunc::BuiltinFunc(BuiltinFunctionID bfid) : _funcID(bfid) {
 }
 
+MiniscriptInstructionOutcome StrConcat::execute(MiniscriptThread *thread) const {
+	if (thread->getStackSize() < 2) {
+		thread->error("Stack underflow");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0, false);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	outcome = thread->dereferenceRValue(1, false);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	DynamicValue &rVal = thread->getStackValueFromTop(0).value;
+	DynamicValue &lValDest = thread->getStackValueFromTop(1).value;
+
+	if (rVal.getType() != DynamicValueTypes::kString) {
+		thread->error("String concat right side was not a string");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	if (lValDest.getType() != DynamicValueTypes::kString) {
+		thread->error("String concat left side was not a string");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	lValDest.setString(lValDest.getString() + rVal.getString());
+	thread->popValues(1);
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 MiniscriptInstructionOutcome PointCreate::execute(MiniscriptThread *thread) const {
 	if (thread->getStackSize() < 2) {
 		thread->error("Stack underflow");
@@ -1084,7 +1117,7 @@ MiniscriptInstructionOutcome PushGlobal::executeFindFilteredParent(MiniscriptThr
 		bool isMatch = false;
 		switch (_globalID) {
 		case kGlobalRefElement:
-			isMatch = obj->isElement();
+			isMatch = obj->isStructural();	// We don't classify the project, sections, and subsections as elements, but mTropolis does
 			break;
 		case kGlobalRefSection:
 			isMatch = obj->isSection();
@@ -1106,7 +1139,13 @@ MiniscriptInstructionOutcome PushGlobal::executeFindFilteredParent(MiniscriptThr
 		if (isMatch)
 			break;
 		else if (obj->isStructural()) {
-			ref = static_cast<Structural *>(obj.get())->getParent()->getSelfReference();
+			Structural *parent = static_cast<Structural *>(obj.get())->getParent();
+			if (parent)
+				ref = parent->getSelfReference();
+			else {
+				ref.reset();
+				break;
+			}
 		} else if (obj->isModifier()) {
 			ref = static_cast<Modifier *>(obj.get())->getParent();
 		} else {
@@ -1234,7 +1273,7 @@ MiniscriptInstructionOutcome MiniscriptThread::dereferenceRValue(size_t offset,
 			if (obj && obj->isModifier()) {
 				const Modifier *modifier = static_cast<const Modifier *>(obj.get());
 				if (modifier->isVariable()) {
-					static_cast<const VariableModifier *>(modifier)->varGetValue(stackValue.value);
+					static_cast<const VariableModifier *>(modifier)->varGetValue(this, stackValue.value);
 				}
 			}
 		} break;
@@ -1314,7 +1353,7 @@ MiniscriptInstructionOutcome MiniscriptThread::tryLoadVariable(MiniscriptStackVa
 		Common::SharedPtr<RuntimeObject> obj = stackValue.value.getObject().object.lock();
 		if (obj && obj->isModifier() && static_cast<Modifier *>(obj.get())->isVariable()) {
 			VariableModifier *varMod = static_cast<VariableModifier *>(obj.get());
-			varMod->varGetValue(stackValue.value);
+			varMod->varGetValue(this, stackValue.value);
 		}
 	}
 
diff --git a/engines/mtropolis/miniscript.h b/engines/mtropolis/miniscript.h
index 587526d11c9..e2c490f0317 100644
--- a/engines/mtropolis/miniscript.h
+++ b/engines/mtropolis/miniscript.h
@@ -227,7 +227,9 @@ namespace MiniscriptInstructions {
 	class Modulo : public UnimplementedInstruction {
 	};
 
-	class StrConcat : public UnimplementedInstruction {
+	class StrConcat : public MiniscriptInstruction {
+	private:
+		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
 	};
 
 	class PointCreate : public MiniscriptInstruction {
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 0b3ae1d1266..b5288451899 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -785,16 +785,16 @@ void CompoundVariableModifier::visitInternalReferences(IStructuralReferenceVisit
 
 bool CompoundVariableModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
 	Modifier *var = findChildByName(attrib);
-	if (!var || !var->isModifier())
+	if (!var || !var->isVariable())
 		return false;
 
-	static_cast<VariableModifier *>(var)->varGetValue(result);
+	static_cast<VariableModifier *>(var)->varGetValue(thread, result);
 	return true;
 }
 
 bool CompoundVariableModifier::readAttributeIndexed(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib, const DynamicValue &index) {
 	Modifier *var = findChildByName(attrib);
-	if (!var || !var->isModifier())
+	if (!var || !var->isVariable())
 		return false;
 
 	return var->readAttributeIndexed(thread, result, "value", index);
@@ -802,7 +802,7 @@ bool CompoundVariableModifier::readAttributeIndexed(MiniscriptThread *thread, Dy
 
 bool CompoundVariableModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
 	Modifier *var = findChildByName(attrib);
-	if (!var || !var->isModifier())
+	if (!var || !var->isVariable())
 		return false;
 
 	writeProxy = static_cast<VariableModifier *>(var)->createWriteProxy();
@@ -840,7 +840,7 @@ bool BooleanVariableModifier::load(ModifierLoaderContext &context, const Data::B
 	return true;
 }
 
-bool BooleanVariableModifier::varSetValue(const DynamicValue &value) {
+bool BooleanVariableModifier::varSetValue(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kBoolean)
 		_value = value.getBool();
 	else
@@ -849,7 +849,7 @@ bool BooleanVariableModifier::varSetValue(const DynamicValue &value) {
 	return true;
 }
 
-void BooleanVariableModifier::varGetValue(DynamicValue &dest) const {
+void BooleanVariableModifier::varGetValue(MiniscriptThread *thread, DynamicValue &dest) const {
 	dest.setBool(_value);
 }
 
@@ -866,7 +866,7 @@ bool IntegerVariableModifier::load(ModifierLoaderContext& context, const Data::I
 	return true;
 }
 
-bool IntegerVariableModifier::varSetValue(const DynamicValue &value) {
+bool IntegerVariableModifier::varSetValue(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kFloat)
 		_value = static_cast<int32>(floor(value.getFloat() + 0.5));
 	else if (value.getType() == DynamicValueTypes::kInteger)
@@ -877,7 +877,7 @@ bool IntegerVariableModifier::varSetValue(const DynamicValue &value) {
 	return true;
 }
 
-void IntegerVariableModifier::varGetValue(DynamicValue &dest) const {
+void IntegerVariableModifier::varGetValue(MiniscriptThread *thread, DynamicValue &dest) const {
 	dest.setInt(_value);
 }
 
@@ -895,7 +895,7 @@ bool IntegerRangeVariableModifier::load(ModifierLoaderContext& context, const Da
 	return true;
 }
 
-bool IntegerRangeVariableModifier::varSetValue(const DynamicValue &value) {
+bool IntegerRangeVariableModifier::varSetValue(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kIntegerRange)
 		_range = value.getIntRange();
 	else
@@ -904,7 +904,7 @@ bool IntegerRangeVariableModifier::varSetValue(const DynamicValue &value) {
 	return true;
 }
 
-void IntegerRangeVariableModifier::varGetValue(DynamicValue &dest) const {
+void IntegerRangeVariableModifier::varGetValue(MiniscriptThread *thread, DynamicValue &dest) const {
 	dest.setIntRange(_range);
 }
 
@@ -922,7 +922,7 @@ bool VectorVariableModifier::load(ModifierLoaderContext &context, const Data::Ve
 	return true;
 }
 
-bool VectorVariableModifier::varSetValue(const DynamicValue &value) {
+bool VectorVariableModifier::varSetValue(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kVector)
 		_vector = value.getVector();
 	else
@@ -931,7 +931,7 @@ bool VectorVariableModifier::varSetValue(const DynamicValue &value) {
 	return true;
 }
 
-void VectorVariableModifier::varGetValue(DynamicValue &dest) const {
+void VectorVariableModifier::varGetValue(MiniscriptThread *thread, DynamicValue &dest) const {
 	dest.setVector(_vector);
 }
 
@@ -949,7 +949,7 @@ bool PointVariableModifier::load(ModifierLoaderContext &context, const Data::Poi
 	return true;
 }
 
-bool PointVariableModifier::varSetValue(const DynamicValue &value) {
+bool PointVariableModifier::varSetValue(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kPoint)
 		_value = value.getPoint();
 	else
@@ -958,7 +958,7 @@ bool PointVariableModifier::varSetValue(const DynamicValue &value) {
 	return true;
 }
 
-void PointVariableModifier::varGetValue(DynamicValue &dest) const {
+void PointVariableModifier::varGetValue(MiniscriptThread *thread, DynamicValue &dest) const {
 	dest.setPoint(_value);
 }
 
@@ -975,7 +975,7 @@ bool FloatingPointVariableModifier::load(ModifierLoaderContext &context, const D
 	return true;
 }
 
-bool FloatingPointVariableModifier::varSetValue(const DynamicValue &value) {
+bool FloatingPointVariableModifier::varSetValue(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kInteger)
 		_value = value.getInt();
 	else if (value.getType() == DynamicValueTypes::kFloat)
@@ -986,7 +986,7 @@ bool FloatingPointVariableModifier::varSetValue(const DynamicValue &value) {
 	return true;
 }
 
-void FloatingPointVariableModifier::varGetValue(DynamicValue &dest) const {
+void FloatingPointVariableModifier::varGetValue(MiniscriptThread *thread, DynamicValue &dest) const {
 	dest.setFloat(_value);
 }
 
@@ -1003,7 +1003,7 @@ bool StringVariableModifier::load(ModifierLoaderContext &context, const Data::St
 	return true;
 }
 
-bool StringVariableModifier::varSetValue(const DynamicValue &value) {
+bool StringVariableModifier::varSetValue(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kString)
 		_value = value.getString();
 	else
@@ -1012,7 +1012,7 @@ bool StringVariableModifier::varSetValue(const DynamicValue &value) {
 	return true;
 }
 
-void StringVariableModifier::varGetValue(DynamicValue &dest) const {
+void StringVariableModifier::varGetValue(MiniscriptThread *thread, DynamicValue &dest) const {
 	dest.setString(_value);
 }
 
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index b89ed4861ea..f20fa22ab68 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -589,8 +589,8 @@ class BooleanVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::BooleanVariableModifier &data);
 
-	bool varSetValue(const DynamicValue &value) override;
-	void varGetValue(DynamicValue &dest) const override;
+	bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override;
+	void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Boolean Variable Modifier"; }
@@ -607,8 +607,8 @@ class IntegerVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::IntegerVariableModifier &data);
 
-	bool varSetValue(const DynamicValue &value) override;
-	void varGetValue(DynamicValue &dest) const override;
+	bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override;
+	void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Integer Variable Modifier"; }
@@ -625,8 +625,8 @@ class IntegerRangeVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::IntegerRangeVariableModifier &data);
 
-	bool varSetValue(const DynamicValue &value) override;
-	void varGetValue(DynamicValue &dest) const override;
+	bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override;
+	void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Integer Range Variable Modifier"; }
@@ -643,8 +643,8 @@ class VectorVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::VectorVariableModifier &data);
 
-	bool varSetValue(const DynamicValue &value) override;
-	void varGetValue(DynamicValue &dest) const override;
+	bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override;
+	void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Vector Variable Modifier"; }
@@ -661,8 +661,8 @@ class PointVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::PointVariableModifier &data);
 
-	bool varSetValue(const DynamicValue &value) override;
-	void varGetValue(DynamicValue &dest) const override;
+	bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override;
+	void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Point Variable Modifier"; }
@@ -679,8 +679,8 @@ class FloatingPointVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::FloatingPointVariableModifier &data);
 
-	bool varSetValue(const DynamicValue &value) override;
-	void varGetValue(DynamicValue &dest) const override;
+	bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override;
+	void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Floating Point Variable Modifier"; }
@@ -697,8 +697,8 @@ class StringVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::StringVariableModifier &data);
 
-	bool varSetValue(const DynamicValue &value) override;
-	void varGetValue(DynamicValue &dest) const override;
+	bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override;
+	void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "String Variable Modifier"; }
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 3faec701fb2..1b31c614591 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -181,8 +181,9 @@ bool ObjectReferenceVariableModifier::readAttribute(MiniscriptThread *thread, Dy
 			resolve();
 
 		if (_object.object.expired())
-			result.setObject(nullptr);
-		result.setObject(_object);
+			result.clear();
+		else
+			result.setObject(_object);
 		return true;
 	}
 
@@ -202,11 +203,21 @@ bool ObjectReferenceVariableModifier::writeRefAttribute(MiniscriptThread *thread
 	return VariableModifier::writeRefAttribute(thread, result, attrib);
 }
 
-bool ObjectReferenceVariableModifier::varSetValue(const DynamicValue &value) {
-	return false;
+bool ObjectReferenceVariableModifier::varSetValue(MiniscriptThread *thread, const DynamicValue &value) {
+	// Somewhat strangely, setting an object reference variable to something sets the path or object,
+	// but getting the variable returns the modifier
+	switch (value.getType()) {
+	case DynamicValueTypes::kNull:
+	case DynamicValueTypes::kObject:
+		return dynSetObject(value);
+	case DynamicValueTypes::kString:
+		return dynSetPath(value);
+	default:
+		return false;
+	}
 }
 
-void ObjectReferenceVariableModifier::varGetValue(DynamicValue &dest) const {
+void ObjectReferenceVariableModifier::varGetValue(MiniscriptThread *thread, DynamicValue &dest) const {
 	dest.setObject(this->getSelfReference());
 }
 
@@ -218,21 +229,200 @@ bool ObjectReferenceVariableModifier::dynSetPath(const DynamicValue &value) {
 	if (value.getType() != DynamicValueTypes::kString)
 		return false;
 
-	_isResolved = false;
 	_objectPath = value.getString();
 	_object.reset();
 
 	return true;
 }
-bool ObjectReferenceVariableModifier::dynSetObject(const DynamicValue& value) {
 
+bool ObjectReferenceVariableModifier::dynSetObject(const DynamicValue &value) {
+	if (value.getType() == DynamicValueTypes::kNull) {
+		_object.reset();
+		_objectPath.clear();
+		_fullPath.clear();
+
+		return true;
+	} else if (value.getType() == DynamicValueTypes::kObject) {
+		Common::SharedPtr<RuntimeObject> obj = value.getObject().object.lock();
+		if (!obj)
+			return dynSetObject(DynamicValue());
+
+		if (!computeObjectPath(obj.get(), _fullPath))
+			return dynSetObject(DynamicValue());
+
+		_objectPath = _fullPath;
+		_object.object = obj;
+
+		return true;
+	} else
+		return false;
 }
 
 void ObjectReferenceVariableModifier::resolve() {
-	if (_isResolved)
+	if (!_object.object.expired())
 		return;
 
-	_isResolved = true;
+	_fullPath.clear();
+	_object.reset();
+
+	if (_objectPath.size() == 0)
+		return;
+
+	if (_objectPath[0] == '/')
+		resolveAbsolutePath();
+	else if (_objectPath[0] == '.')
+		resolveRelativePath(this, _objectPath, 0);
+	else
+		warning("Object reference variable had an unknown path format");
+
+	if (!_object.object.expired()) {
+		if (!computeObjectPath(_object.object.lock().get(), _fullPath)) {
+			_object.reset();
+		}
+	}
+}
+
+void ObjectReferenceVariableModifier::resolveRelativePath(RuntimeObject *obj, const Common::String &path, size_t startPos) {
+	bool haveNextLevel = true;
+	size_t nextLevelPos = startPos;
+
+	while (haveNextLevel) {
+		startPos = nextLevelPos;
+		size_t endPos = path.find('/', startPos);
+		if (endPos == Common::String::npos) {
+			haveNextLevel = false;
+			endPos = path.size();
+		} else {
+			nextLevelPos = endPos + 1;
+		}
+
+		Common::String levelName = path.substr(startPos, endPos - startPos);
+
+		// This is technically more forgiving than mTropolis, which only allows ".." chains at the start of the path
+		// Adjust this if it turns out to be a problem...
+		if (levelName == "..") {
+			obj = getObjectParent(obj);
+			if (obj == nullptr)
+				return;
+		}
+
+		const Common::Array<Common::SharedPtr<Modifier> > *modifierChildren = nullptr;
+		const Common::Array<Common::SharedPtr<Structural> > *structuralChildren = nullptr;
+
+		if (obj->isStructural()) {
+			Structural *structural = static_cast<Structural *>(obj);
+			modifierChildren = &structural->getModifiers();
+			structuralChildren = &structural->getChildren();
+		} else if (obj->isModifier()) {
+			Modifier *modifier = static_cast<Modifier *>(obj);
+			IModifierContainer *childContainer = modifier->getChildContainer();
+			if (childContainer)
+				modifierChildren = &childContainer->getModifiers();
+		}
+
+		bool foundMatch = false;
+		if (modifierChildren) {
+			for (const Common::SharedPtr<Modifier> &modifier : *modifierChildren) {
+				if (caseInsensitiveEqual(levelName, modifier->getName())) {
+					foundMatch = true;
+					obj = modifier.get();
+					break;
+				}
+			}
+		}
+		if (structuralChildren && !foundMatch) {
+			for (const Common::SharedPtr<Structural> &structural : *structuralChildren) {
+				if (caseInsensitiveEqual(levelName, structural->getName())) {
+					foundMatch = true;
+					obj = structural.get();
+					break;
+				}
+			}
+		}
+
+		if (!foundMatch)
+			return;
+	}
+}
+
+void ObjectReferenceVariableModifier::resolveAbsolutePath() {
+	assert(_objectPath[0] == '/');
+
+	RuntimeObject *project = this;
+	for (;;) {
+		RuntimeObject *parent = getObjectParent(project);
+		if (!parent)
+			break;
+		project = parent;
+	}
+
+	if (!project->isProject())
+		return; // Some sort of detached object
+
+	Common::String projectPrefixes[2] = {
+		"/" + static_cast<Structural *>(project)->getName(),
+		"/<project>"};
+
+	size_t prefixEnd = 0;
+
+	bool foundPrefix = false;
+	for (const Common::String &prefix : projectPrefixes) {
+		if (_objectPath.size() >= prefix.size() && caseInsensitiveEqual(_objectPath.substr(0, prefix.size()), prefix)) {
+			prefixEnd = prefix.size();
+			foundPrefix = true;
+			break;
+		}
+	}
+
+	if (!foundPrefix)
+		return;
+
+	// If the object path is longer, then there must be a slash separator, otherwise this doesn't match the project
+	if (prefixEnd == _objectPath.size()) {
+		_object = ObjectReference(project->getSelfReference());
+		return;
+	}
+
+	if (_objectPath[prefixEnd] != '/')
+		return;
+
+	return resolveRelativePath(project, _objectPath, prefixEnd + 1);
+}
+
+bool ObjectReferenceVariableModifier::computeObjectPath(RuntimeObject *obj, Common::String &outPath) {
+	Common::String pathForThis = "/";
+
+	if (obj->isStructural()) {
+		Structural *structural = static_cast<Structural *>(obj);
+		pathForThis += structural->getName();
+	} else if (obj->isModifier()) {
+		Modifier *modifier = static_cast<Modifier *>(obj);
+		pathForThis += modifier->getName();
+	}
+
+	RuntimeObject *parent = getObjectParent(this);
+
+	if (parent) {
+		Common::String pathForParent;
+		if (!computeObjectPath(parent, pathForParent))
+			return false;
+
+		outPath = pathForParent + pathForThis;
+	} else
+		outPath = pathForThis;
+
+	return true;
+}
+
+RuntimeObject *ObjectReferenceVariableModifier::getObjectParent(RuntimeObject *obj) {
+	if (obj->isStructural()) {
+		Structural *structural = static_cast<Structural *>(obj);
+		return structural->getParent();
+	} else if (obj->isModifier()) {
+		Modifier *modifier = static_cast<Modifier *>(obj);
+		return modifier->getParent().lock().get();
+	}
+	return nullptr;
 }
 
 MidiModifier::MidiModifier() : _plugIn(nullptr) {
@@ -373,7 +563,7 @@ bool ListVariableModifier::load(const PlugInModifierLoaderContext &context, cons
 	return true;
 }
 
-bool ListVariableModifier::varSetValue(const DynamicValue &value) {
+bool ListVariableModifier::varSetValue(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kList)
 		_list = value.getList()->clone();
 	else
@@ -382,7 +572,7 @@ bool ListVariableModifier::varSetValue(const DynamicValue &value) {
 	return true;
 }
 
-void ListVariableModifier::varGetValue(DynamicValue &dest) const {
+void ListVariableModifier::varGetValue(MiniscriptThread *thread, DynamicValue &dest) const {
 	dest.setList(_list);
 }
 
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index c3b9ce7f1fb..da1eee953d5 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -99,8 +99,8 @@ public:
 	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
 	bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib);
 
-	bool varSetValue(const DynamicValue &value) override;
-	void varGetValue(DynamicValue &dest) const override;
+	bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override;
+	void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Object Reference Variable Modifier"; }
@@ -113,11 +113,17 @@ private:
 	bool dynSetObject(const DynamicValue &value);
 
 	void resolve();
+	void resolveRelativePath(RuntimeObject *obj, const Common::String &path, size_t startPos);
+	void resolveAbsolutePath();
+
+	bool computeObjectPath(RuntimeObject *obj, Common::String &outPath);
+	static RuntimeObject *getObjectParent(RuntimeObject *obj);
 
 	Event _setToSourceParentWhen;
 
 	mutable ObjectReference _object;
-	mutable Common::String _objectPath;
+	Common::String _objectPath;
+	Common::String _fullPath;
 };
 
 class MidiModifier : public Modifier {
@@ -183,8 +189,8 @@ public:
 
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::ListVariableModifier &data);
 
-	bool varSetValue(const DynamicValue &value) override;
-	void varGetValue(DynamicValue &dest) const override;
+	bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override;
+	void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "List Variable Modifier"; }
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 5d4d2a6b727..81c00fed80e 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -1424,6 +1424,34 @@ void DynamicValue::initFromOther(const DynamicValue &other) {
 	_type = other._type;
 }
 
+bool DynamicValueWriteStringHelper::write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const {
+	Common::String &dest = *static_cast<Common::String *>(objectRef);
+	switch (value.getType()) {
+	case DynamicValueTypes::kString:
+		dest = value.getString();
+		return true;
+	default:
+		return false;
+	}
+}
+
+bool DynamicValueWriteStringHelper::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const {
+	return false;
+}
+
+bool DynamicValueWriteStringHelper::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const {
+	return false;
+}
+
+void DynamicValueWriteStringHelper::create(Common::String *strValue, DynamicValueWriteProxy &proxy) {
+	proxy.pod.ptrOrOffset = 0;
+	proxy.pod.objectRef = strValue;
+	proxy.pod.ifc = &_instance;
+}
+
+DynamicValueWriteStringHelper DynamicValueWriteStringHelper::_instance;
+
+
 MessengerSendSpec::MessengerSendSpec() : destination(0), _linkType(kLinkTypeNotYetLinked) {
 }
 
@@ -1532,9 +1560,11 @@ void MessengerSendSpec::resolveDestination(Runtime *runtime, Modifier *sender, C
 		case kMessageDestElement:
 			resolveHierarchyStructuralDestination(runtime, sender, outStructuralDest, outModifierDest, isElementFilter);
 			break;
+		case kMessageDestModifiersParent:
+			resolveVariableObjectType(sender->getParent().lock().get(), outStructuralDest, outModifierDest);
+			break;
 		case kMessageDestChildren:
 		case kMessageDestElementsParent:
-		case kMessageDestModifiersParent:
 		case kMessageDestSubsection:
 		case kMessageDestSourcesParent:
 		case kMessageDestBehavior:
@@ -1549,6 +1579,22 @@ void MessengerSendSpec::resolveDestination(Runtime *runtime, Modifier *sender, C
 	}
 }
 
+void MessengerSendSpec::resolveVariableObjectType(RuntimeObject *obj, Common::WeakPtr<Structural> &outStructuralDest, Common::WeakPtr<Modifier> &outModifierDest) {
+	if (!obj) {
+		warning("Couldn't resolve mesenger destination");
+		return;
+	}
+
+	if (obj->isStructural())
+		outStructuralDest = obj->getSelfReference().staticCast<Structural>();
+	else if (obj->isModifier())
+		outModifierDest = obj->getSelfReference().staticCast<Modifier>();
+	else {
+		warning("Messenger destination was not a valid recipient type");
+		return;
+	}
+}
+
 void MessengerSendSpec::sendFromMessenger(Runtime *runtime, Modifier *sender) const {
 
 	Common::SharedPtr<MessageProperties> props(new MessageProperties(this->send, this->with, sender->getSelfReference()));
@@ -1873,6 +1919,24 @@ bool Structural::isStructural() const {
 	return true;
 }
 
+bool Structural::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "name") {
+		result.setString(_name);
+		return true;
+	}
+
+	return RuntimeObject::readAttribute(thread, result, attrib);
+}
+
+bool Structural::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
+	if (attrib == "name") {
+		DynamicValueWriteStringHelper::create(&_name, result);
+		return true;
+	}
+
+	return RuntimeObject::writeRefAttribute(thread, result, attrib);
+}
+
 const Common::Array<Common::SharedPtr<Structural> > &Structural::getChildren() const {
 	return _children;
 }
@@ -4201,6 +4265,16 @@ bool Subsection::load(const Data::SubsectionStructuralDef &data) {
 	return true;
 }
 
+bool Subsection::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "subsection") {
+		// Unclear why this is necessary
+		result.setObject(getSelfReference());
+		return true;
+	}
+
+	return Structural::readAttribute(thread, result, attrib);
+}
+
 bool Subsection::isSubsection() const {
 	return true;
 }
@@ -4469,7 +4543,7 @@ DynamicValueWriteProxy VariableModifier::createWriteProxy() {
 }
 
 bool VariableModifier::WriteProxyInterface::write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const {
-	return static_cast<VariableModifier *>(objectRef)->varSetValue(dest);
+	return static_cast<VariableModifier *>(objectRef)->varSetValue(thread, dest);
 }
 
 bool VariableModifier::WriteProxyInterface::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &dest, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const {
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index f4af3ea2d92..51ccf30e9a7 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -876,6 +876,17 @@ private:
 template<class TInteger>
 DynamicValueWriteIntegerHelper<TInteger> DynamicValueWriteIntegerHelper<TInteger>::_instance;
 
+struct DynamicValueWriteStringHelper : public IDynamicValueWriteInterface {
+	bool write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const override;
+	bool refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override;
+	bool refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
+
+	static void create(Common::String *strValue, DynamicValueWriteProxy &proxy);
+
+private:
+	static DynamicValueWriteStringHelper _instance;
+};
+
 template<class TClass, bool (TClass::*TWriteMethod)(const DynamicValue &dest)>
 struct DynamicValueWriteFuncHelper : public IDynamicValueWriteInterface {
 	bool write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const override {
@@ -910,6 +921,8 @@ struct MessengerSendSpec {
 	void visitInternalReferences(IStructuralReferenceVisitor *visitor);
 	void resolveDestination(Runtime *runtime, Modifier *sender, Common::WeakPtr<Structural> &outStructuralDest, Common::WeakPtr<Modifier> &outModifierDest) const;
 
+	static void resolveVariableObjectType(RuntimeObject *obj, Common::WeakPtr<Structural> &outStructuralDest, Common::WeakPtr<Modifier> &outModifierDest);
+
 	void sendFromMessenger(Runtime *runtime, Modifier *sender) const;
 
 	Event send;
@@ -1461,6 +1474,9 @@ public:
 
 	bool isStructural() const override;
 
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
+	bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) override;
+
 	const Common::Array<Common::SharedPtr<Structural> > &getChildren() const;
 	void addChild(const Common::SharedPtr<Structural> &child);
 	void removeAllChildren();
@@ -1758,6 +1774,8 @@ class Subsection : public Structural {
 public:
 	bool load(const Data::SubsectionStructuralDef &data);
 
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
+
 	ObjectLinkingScope *getSceneLoadMaterializeScope();
 
 	bool isSubsection() const override;
@@ -1900,8 +1918,8 @@ protected:
 class VariableModifier : public Modifier {
 public:
 	virtual bool isVariable() const;
-	virtual bool varSetValue(const DynamicValue &value) = 0;
-	virtual void varGetValue(DynamicValue &dest) const = 0;
+	virtual bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) = 0;
+	virtual void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const = 0;
 
 	virtual DynamicValueWriteProxy createWriteProxy();
 


Commit: d71d8e566c669a4f1b52fc10045669bc5140e66c
    https://github.com/scummvm/scummvm/commit/d71d8e566c669a4f1b52fc10045669bc5140e66c
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add arith ops, fix compound-in-compound crash

Changed paths:
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/miniscript.h
    engines/mtropolis/modifiers.cpp


diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index aeee0cd0fb9..c565cbbc755 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -557,6 +557,112 @@ MiniscriptInstructionOutcome Send::execute(MiniscriptThread *thread) const {
 	}
 }
 
+MiniscriptInstructionOutcome BinaryArithInstruction::execute(MiniscriptThread *thread) const {
+	if (thread->getStackSize() < 2) {
+		thread->error("Stack underflow");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0, false);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	outcome = thread->dereferenceRValue(1, false);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	DynamicValue &rs = thread->getStackValueFromTop(0).value;
+	DynamicValue &lsDest = thread->getStackValueFromTop(1).value;
+
+	double leftVal = 0.0;
+	switch (lsDest.getType()) {
+	case DynamicValueTypes::kInteger:
+		leftVal = lsDest.getInt();
+		break;
+	case DynamicValueTypes::kFloat:
+		leftVal = lsDest.getFloat();
+		break;
+	default:
+		thread->error("Invalid left-side type for binary arithmetic operator");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	double rightVal = 0.0;
+	switch (rs.getType()) {
+	case DynamicValueTypes::kInteger:
+		rightVal = rs.getInt();
+		break;
+	case DynamicValueTypes::kFloat:
+		rightVal = rs.getFloat();
+		break;
+	default:
+		thread->error("Invalid right-side type for binary arithmetic operator");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	double result = 0.0;
+	outcome = arithExecute(thread, result, leftVal, rightVal);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	lsDest.setFloat(result);
+
+	thread->popValues(1);
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome Add::arithExecute(MiniscriptThread *thread, double &result, double left, double right) const {
+	result = left + right;
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome Sub::arithExecute(MiniscriptThread *thread, double &result, double left, double right) const {
+	result = left - right;
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome Mul::arithExecute(MiniscriptThread *thread, double &result, double left, double right) const {
+	result = left * right;
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome Div::arithExecute(MiniscriptThread *thread, double &result, double left, double right) const {
+	if (right == 0.0) {
+		thread->error("Arithmetic error: Division by zero");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+	result = left / right;
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome Pow::arithExecute(MiniscriptThread *thread, double &result, double left, double right) const {
+	if (left < 0.0 && right != floor(right)) {
+		thread->error("Arithmetic error: Left side is negative but right side is not an integer");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+	result = pow(left, right);
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome DivInt::arithExecute(MiniscriptThread *thread, double &result, double left, double right) const {
+	if (right == 0.0) {
+		thread->error("Arithmetic error: Integer division by zero");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+	result = floor(left / right);
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome Modulo::arithExecute(MiniscriptThread *thread, double &result, double left, double right) const {
+	if (right == 0.0) {
+		thread->error("Arithmetic error: Modulo division by zero");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+	result = fmod(left, right);
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 MiniscriptInstructionOutcome UnorderedCompareInstruction::execute(MiniscriptThread *thread) const {
 	if (thread->getStackSize() < 2) {
 		thread->error("Stack underflow");
diff --git a/engines/mtropolis/miniscript.h b/engines/mtropolis/miniscript.h
index e2c490f0317..ee87264bd22 100644
--- a/engines/mtropolis/miniscript.h
+++ b/engines/mtropolis/miniscript.h
@@ -111,19 +111,47 @@ namespace MiniscriptInstructions {
 		MessageFlags _messageFlags;
 	};
 
-	class Add : public UnimplementedInstruction {
+	class BinaryArithInstruction : public MiniscriptInstruction {
+	private:
+		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
+
+	protected:
+		virtual MiniscriptInstructionOutcome arithExecute(MiniscriptThread *thread, double &result, double left, double right) const = 0;
 	};
 
-	class Sub : public UnimplementedInstruction {
+	class Add : public BinaryArithInstruction {
+	private:
+		MiniscriptInstructionOutcome arithExecute(MiniscriptThread *thread, double &result, double left, double right) const override;
 	};
 
-	class Mul : public UnimplementedInstruction {
+	class Sub : public BinaryArithInstruction {
+	private:
+		MiniscriptInstructionOutcome arithExecute(MiniscriptThread *thread, double &result, double left, double right) const override;
 	};
 
-	class Div : public UnimplementedInstruction {
+	class Mul : public BinaryArithInstruction {
+	private:
+		MiniscriptInstructionOutcome arithExecute(MiniscriptThread *thread, double &result, double left, double right) const override;
 	};
 
-	class Pow : public UnimplementedInstruction {
+	class Div : public BinaryArithInstruction {
+	private:
+		MiniscriptInstructionOutcome arithExecute(MiniscriptThread *thread, double &result, double left, double right) const override;
+	};
+
+	class Pow : public BinaryArithInstruction {
+	private:
+		MiniscriptInstructionOutcome arithExecute(MiniscriptThread *thread, double &result, double left, double right) const override;
+	};
+
+	class DivInt : public BinaryArithInstruction {
+	private:
+		MiniscriptInstructionOutcome arithExecute(MiniscriptThread *thread, double &result, double left, double right) const override;
+	};
+
+	class Modulo : public BinaryArithInstruction {
+	private:
+		MiniscriptInstructionOutcome arithExecute(MiniscriptThread *thread, double &result, double left, double right) const override;
 	};
 
 	class And : public MiniscriptInstruction {
@@ -221,12 +249,6 @@ namespace MiniscriptInstructions {
 		BuiltinFunctionID _funcID;
 	};
 
-	class DivInt : public UnimplementedInstruction {
-	};
-
-	class Modulo : public UnimplementedInstruction {
-	};
-
 	class StrConcat : public MiniscriptInstruction {
 	private:
 		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index b5288451899..f6d26746df6 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -785,11 +785,15 @@ void CompoundVariableModifier::visitInternalReferences(IStructuralReferenceVisit
 
 bool CompoundVariableModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
 	Modifier *var = findChildByName(attrib);
-	if (!var || !var->isVariable())
-		return false;
-
-	static_cast<VariableModifier *>(var)->varGetValue(thread, result);
-	return true;
+	if (var) {
+		if (var->isVariable()) {
+			static_cast<VariableModifier *>(var)->varGetValue(thread, result);
+		} else {
+			result.setObject(var->getSelfReference());
+		}
+		return true;
+	}
+	return Modifier::readAttribute(thread, result, attrib);
 }
 
 bool CompoundVariableModifier::readAttributeIndexed(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib, const DynamicValue &index) {


Commit: 367afa4b539f41de2cc018b58950c1340c9077b7
    https://github.com/scummvm/scummvm/commit/367afa4b539f41de2cc018b58950c1340c9077b7
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix debugger scroll bar not animating

Changed paths:
    engines/mtropolis/debug.cpp


diff --git a/engines/mtropolis/debug.cpp b/engines/mtropolis/debug.cpp
index 4569c6a8243..521ded89fa3 100644
--- a/engines/mtropolis/debug.cpp
+++ b/engines/mtropolis/debug.cpp
@@ -160,6 +160,8 @@ private:
 
 	Common::String _title;
 	bool _isDirty;
+	bool _forceRedrawChrome;
+
 	int _scrollOffset;
 
 	int _scrollBarHandleSize;
@@ -175,7 +177,7 @@ private:
 
 DebugToolWindowBase::DebugToolWindowBase(DebuggerTool tool, const Common::String &title, Debugger *debugger, const WindowParameters &windowParams)
 	: Window(windowParams), _debugger(debugger), _tool(tool), _title(title), _activeWidget(kToolWindowWidgetNone), _isMouseCaptured(false),
-	  _isDirty(true), _scrollOffset(0), _haveScrollBar(false), _havePreferredScrollOffset(false) {
+	  _isDirty(true), _scrollOffset(0), _haveScrollBar(false), _havePreferredScrollOffset(false), _forceRedrawChrome(false) {
 
 	refreshChrome();
 }
@@ -184,7 +186,9 @@ void DebugToolWindowBase::render() {
 	if (_isDirty) {
 		_isDirty = false;
 
-		bool needChromeUpdate = false;
+		bool needChromeUpdate = _forceRedrawChrome;
+		_forceRedrawChrome = false;
+
 		int oldWidth = 0;
 		int oldHeight = 0;
 		if (_toolSurface) {
@@ -342,6 +346,7 @@ void DebugToolWindowBase::onMouseDown(int32 x, int32 y, int mouseButton) {
 				}
 
 				setDirty();
+				_forceRedrawChrome = true;
 			} else {
 				_activeWidget = kToolWindowWidgetScrollEmpty;
 			}
@@ -387,6 +392,7 @@ void DebugToolWindowBase::onMouseMove(int32 x, int32 y) {
 			if (desiredOffset != _scrollBarHandleOffset) {
 				_scrollBarHandleOffset = desiredOffset;
 				_scrollOffset = _scrollBarHandleOffset * _maxScrollOffset / _scrollBarHandleMaxOffset;
+				_forceRedrawChrome = true;
 				setDirty();
 			}
 		}
@@ -413,6 +419,7 @@ void DebugToolWindowBase::onMouseUp(int32 x, int32 y, int mouseButton) {
 		}
 		if (_activeWidget == kToolWindowWidgetScrollHandle) {
 			setDirty();
+			_forceRedrawChrome = true;
 		}
 
 		_activeWidget = kToolWindowWidgetNone;


Commit: 5e9f44d731888a5d0bebbd15db6e0d1b984519b6
    https://github.com/scummvm/scummvm/commit/5e9f44d731888a5d0bebbd15db6e0d1b984519b6
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Annotate fields of STransCt (plug-in scene transition) modifier.

Changed paths:
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/miniscript.h
    engines/mtropolis/plugin/standard_data.cpp
    engines/mtropolis/plugin/standard_data.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index c565cbbc755..5925b0bdbc9 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -22,6 +22,7 @@
 #include "mtropolis/miniscript.h"
 #include "common/config-manager.h"
 
+#include "common/random.h"
 #include "common/memstream.h"
 
 namespace MTropolis {
@@ -858,6 +859,245 @@ MiniscriptInstructionOutcome OrderedCompareInstruction::execute(MiniscriptThread
 BuiltinFunc::BuiltinFunc(BuiltinFunctionID bfid) : _funcID(bfid) {
 }
 
+MiniscriptInstructionOutcome BuiltinFunc::execute(MiniscriptThread *thread) const {
+	size_t stackArgsNeeded = 1;
+	bool returnsValue = true;
+
+	if (thread->getStackSize() < stackArgsNeeded) {
+		thread->error("Stack underflow");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	for (size_t i = 0; i < stackArgsNeeded; i++) {
+		MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(i, false);
+		if (outcome != kMiniscriptInstructionOutcomeContinue)
+			return outcome;
+	}
+
+	DynamicValue staticDest;
+	DynamicValue *dest = nullptr;
+
+	if (returnsValue) {
+		if (stackArgsNeeded > 0)
+			dest = &thread->getStackValueFromTop(stackArgsNeeded - 1).value;
+		else
+			dest = &staticDest;
+	}
+
+	MiniscriptInstructionOutcome outcome = executeFunction(thread, dest);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	if (stackArgsNeeded > 0) {
+		size_t valuesToPop = stackArgsNeeded;
+		if (returnsValue)
+			valuesToPop--;
+
+		if (valuesToPop > 0)
+			thread->popValues(valuesToPop);
+	} else {
+		if (returnsValue)
+			thread->pushValue(staticDest);
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome BuiltinFunc::executeFunction(MiniscriptThread *thread, DynamicValue *returnValue) const {
+	switch (_funcID) {
+	case kSin:
+	case kCos:
+	case kRandom:
+	case kSqrt:
+	case kTan:
+	case kAbs:
+	case kSign:
+	case kArctangent:
+	case kExp:
+	case kLn:
+	case kLog:
+	case kCosH:
+	case kSinH:
+	case kTanH:
+	case kTrunc:
+	case kRound:
+		return executeSimpleNumericInstruction(thread, returnValue);
+	case kRect2Polar:
+		return executeRectToPolar(thread, returnValue);
+	case kPolar2Rect:
+		return executePolarToRect(thread, returnValue);
+	case kNum2Str:
+		return executeNum2Str(thread, returnValue);
+	case kStr2Num:
+		return executeStr2Num(thread, returnValue);
+	default:
+		thread->error("Unimplemented built-in function");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+}
+
+MiniscriptInstructionOutcome BuiltinFunc::executeSimpleNumericInstruction(MiniscriptThread *thread, DynamicValue *returnValue) const {
+	double result = 0.0;
+
+	double input = 0.0;
+	const DynamicValue &inputDynamicValue = thread->getStackValueFromTop(0).value;
+
+	switch (inputDynamicValue.getType()) {
+	case DynamicValueTypes::kInteger:
+		input = inputDynamicValue.getInt();
+		break;
+	case DynamicValueTypes::kFloat:
+		input = inputDynamicValue.getFloat();
+		break;
+	default:
+		thread->error("Invalid numeric function input type");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	switch (_funcID) {
+	case kSin:
+		result = sin(input * (M_PI / 180.0));
+		break;
+	case kCos:
+		result = cos(input * (M_PI / 180.0));
+		break;
+	case kRandom:
+		if (input < 1.5)
+			result = 0.0;
+		else {
+			uint rngMax = static_cast<uint>(floor(input + 0.5)) - 1;
+			result = thread->getRuntime()->getRandom()->getRandomNumber(rngMax);
+		}
+		break;
+	case kSqrt:
+		result = sqrt(input);
+		break;
+	case kTan:
+		result = tan(input * (M_PI / 180.0));
+		break;
+	case kAbs:
+		result = fabs(input);
+		break;
+	case kSign:
+		if (input < 0.0)
+			result = -1;
+		else if (input > 0.0)
+			result = 1;
+		else
+			result = 0;
+		break;
+	case kArctangent:
+		result = atan(input) * (180.0 / M_PI);
+		break;
+	case kExp:
+		result = exp(input);
+		break;
+	case kLn:
+		result = log(input);
+		break;
+	case kLog:
+		result = log10(input);
+		break;
+	case kCosH:
+		result = cosh(input * (M_PI / 180.0));
+		break;
+	case kSinH:
+		result = sinh(input * (M_PI / 180.0));
+		break;
+	case kTanH:
+		result = tanh(input * (M_PI / 180.0));
+		break;
+	case kTrunc:
+		result = trunc(input);
+		break;
+	case kRound:
+		result = round(input);
+		break;
+	default:
+		thread->error("Unimplemented numeric function");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	returnValue->setFloat(result);
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome BuiltinFunc::executeRectToPolar(MiniscriptThread *thread, DynamicValue *returnValue) const {
+	const DynamicValue &inputDynamicValue = thread->getStackValueFromTop(0).value;
+
+	if (inputDynamicValue.getType() != DynamicValueTypes::kPoint) {
+		thread->error("Polar to rect input must be a vector");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	const Point16 &pt = inputDynamicValue.getPoint();
+
+	double angle = atan2(pt.x, pt.y);
+	double magnitude = sqrt(pt.x * pt.x + pt.y * pt.y);
+
+	returnValue->setVector(AngleMagVector::create(angle, magnitude));
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome BuiltinFunc::executePolarToRect(MiniscriptThread *thread, DynamicValue *returnValue) const {
+	const DynamicValue &inputDynamicValue = thread->getStackValueFromTop(0).value;
+
+	if (inputDynamicValue.getType() != DynamicValueTypes::kVector) {
+		thread->error("Polar to rect input must be a vector");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	const AngleMagVector &vec = inputDynamicValue.getVector();
+
+	double x = cos(vec.angleRadians) * vec.magnitude;
+	double y = sin(vec.angleRadians) * vec.magnitude;
+
+	returnValue->setPoint(Point16::create(static_cast<int16>(round(x)), static_cast<int16>(round(y))));
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome BuiltinFunc::executeNum2Str(MiniscriptThread *thread, DynamicValue *returnValue) const {
+	Common::String result;
+
+	const DynamicValue &inputDynamicValue = thread->getStackValueFromTop(0).value;
+	switch (inputDynamicValue.getType()) {
+	case DynamicValueTypes::kInteger:
+		result.format("%i", static_cast<int>(inputDynamicValue.getInt()));
+		break;
+	case DynamicValueTypes::kFloat:
+		result.format("%g", static_cast<double>(inputDynamicValue.getFloat()));
+		break;
+	default:
+		thread->error("Invalid input value to num2str");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome BuiltinFunc::executeStr2Num(MiniscriptThread *thread, DynamicValue *returnValue) const {
+	double result = 0.0;
+
+	const DynamicValue &inputDynamicValue = thread->getStackValueFromTop(0).value;
+	if (inputDynamicValue.getType() != DynamicValueTypes::kString) {
+		thread->error("Invalid input value to str2num");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	const Common::String &str = inputDynamicValue.getString();
+	if (str.size() == 0 || !sscanf(str.c_str(), "%lf", &result)) {
+		thread->error("Couldn't parse number");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	returnValue->setFloat(result);
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 MiniscriptInstructionOutcome StrConcat::execute(MiniscriptThread *thread) const {
 	if (thread->getStackSize() < 2) {
 		thread->error("Stack underflow");
diff --git a/engines/mtropolis/miniscript.h b/engines/mtropolis/miniscript.h
index ee87264bd22..462bc271177 100644
--- a/engines/mtropolis/miniscript.h
+++ b/engines/mtropolis/miniscript.h
@@ -246,6 +246,15 @@ namespace MiniscriptInstructions {
 		explicit BuiltinFunc(BuiltinFunctionID bfid);
 
 	private:
+		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
+
+		MiniscriptInstructionOutcome executeFunction(MiniscriptThread *thread, DynamicValue *returnValue) const;
+		MiniscriptInstructionOutcome executeSimpleNumericInstruction(MiniscriptThread *thread, DynamicValue *returnValue) const;
+		MiniscriptInstructionOutcome executeRectToPolar(MiniscriptThread *thread, DynamicValue *returnValue) const;
+		MiniscriptInstructionOutcome executePolarToRect(MiniscriptThread *thread, DynamicValue *returnValue) const;
+		MiniscriptInstructionOutcome executeNum2Str(MiniscriptThread *thread, DynamicValue *returnValue) const;
+		MiniscriptInstructionOutcome executeStr2Num(MiniscriptThread *thread, DynamicValue *returnValue) const;
+
 		BuiltinFunctionID _funcID;
 	};
 
diff --git a/engines/mtropolis/plugin/standard_data.cpp b/engines/mtropolis/plugin/standard_data.cpp
index a70509800fc..c2b9c8ef2df 100644
--- a/engines/mtropolis/plugin/standard_data.cpp
+++ b/engines/mtropolis/plugin/standard_data.cpp
@@ -43,10 +43,9 @@ DataReadErrorCode STransCtModifier::load(PlugIn &plugIn, const PlugInModifier &p
 	if (prefix.plugInRevision != 0)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!reader.readU16(unknown1) || !unknown2.load(reader) || !reader.readU16(unknown3) || !unknown4.load(reader)
-		|| !reader.readU16(unknown5) || !reader.readU32(unknown6) || !reader.readU16(unknown7) || !reader.readU32(unknown8)
-		|| !reader.readU16(unknown9) || !reader.readU32(unknown10) || !reader.readU16(unknown11) || !reader.readU32(unknown12)
-		|| !reader.readU16(unknown13) || !reader.readU32(unknown14) || !reader.readU16(unknown15) || !reader.readBytes(unknown16))
+	if (!enableWhen.load(reader) || !disableWhen.load(reader) || !transitionType.load(reader) ||
+		!transitionDirection.load(reader) || !unknown1.load(reader) || !steps.load(reader) ||
+		!duration.load(reader) || !fullScreen.load(reader))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
diff --git a/engines/mtropolis/plugin/standard_data.h b/engines/mtropolis/plugin/standard_data.h
index 9863f492d86..00a76be3a2b 100644
--- a/engines/mtropolis/plugin/standard_data.h
+++ b/engines/mtropolis/plugin/standard_data.h
@@ -44,22 +44,14 @@ protected:
 };
 
 struct STransCtModifier : public PlugInModifierData {
-	uint16 unknown1;	// Type tag? (0x17)
-	Event unknown2;		// Probably "apply when"
-	uint16 unknown3;	// Type tag? (0x17)
-	Event unknown4;		// Probably "remove when"
-	uint16 unknown5;	// Type tag? (1)
-	uint32 unknown6;
-	uint16 unknown7;	// Type tag? (1)
-	uint32 unknown8;
-	uint16 unknown9;	// Type tag? (1)
-	uint32 unknown10;
-	uint16 unknown11;	// Type tag? (1)
-	uint32 unknown12;
-	uint16 unknown13;	// Type tag? (1)
-	uint32 unknown14;
-	uint16 unknown15;	// Type tag? (0x14)
-	uint8 unknown16[2];
+	PlugInTypeTaggedValue enableWhen;  // Event
+	PlugInTypeTaggedValue disableWhen;	// Event
+	PlugInTypeTaggedValue transitionType;	// int
+	PlugInTypeTaggedValue transitionDirection;	// int
+	PlugInTypeTaggedValue unknown1; // int, seems to always be 1
+	PlugInTypeTaggedValue steps;	// int, seems to always be 32
+	PlugInTypeTaggedValue duration;    // int, always observed as 60000
+	PlugInTypeTaggedValue fullScreen; // bool
 
 protected:
 	DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override;
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 81c00fed80e..b49847d0bed 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -30,6 +30,7 @@
 
 #include "common/debug.h"
 #include "common/file.h"
+#include "common/random.h"
 #include "common/substream.h"
 #include "common/system.h"
 
@@ -2483,6 +2484,8 @@ Runtime::SceneStackEntry::SceneStackEntry() {
 Runtime::Runtime(OSystem *system) : _nextRuntimeGUID(1), _realDisplayMode(kColorDepthModeInvalid), _fakeDisplayMode(kColorDepthModeInvalid),
 									_displayWidth(1024), _displayHeight(768), _realTimeBase(0), _playTimeBase(0), _sceneTransitionState(kSceneTransitionStateNotTransitioning),
 									_system(system), _lastFrameCursor(nullptr), _defaultCursor(new DefaultCursor()) {
+	_random.reset(new Common::RandomSource("mtropolis"));
+
 	_vthread.reset(new VThread());
 
 	for (int i = 0; i < kColorDepthModeCount; i++) {
@@ -3187,7 +3190,9 @@ void Runtime::onMouseUp(int32 x, int32 y, Actions::MouseButton mButton) {
 		_mouseFocusWindow.reset();
 }
 
-
+Common::RandomSource* Runtime::getRandom() const {
+	return _random.get();
+}
 
 void Runtime::ensureMainWindowExists() {
 	// Maybe there's a better spot for this
@@ -4319,10 +4324,12 @@ bool VisualElement::readAttribute(MiniscriptThread *thread, DynamicValue &result
 	if (attrib == "visible") {
 		result.setBool(_visible);
 		return true;
-	}
-	if (attrib == "direct") {
+	} else if (attrib == "direct") {
 		result.setBool(_directToScreen);
 		return true;
+	} else if (attrib == "position") {
+		result.setPoint(Point16::create(_rect.left, _rect.top));
+		return true;
 	}
 
 	return Element::readAttribute(thread, result, attrib);
@@ -4332,10 +4339,12 @@ bool VisualElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWrit
 	if (attrib == "visible") {
 		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetVisibility>::create(this, writeProxy);
 		return true;
-	}
-	if (attrib == "direct") {
+	} else if (attrib == "direct") {
 		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetDirect>::create(this, writeProxy);
 		return true;
+	} else if (attrib == "position") {
+		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetPosition>::create(this, writeProxy);
+		return true;
 	}
 
 	return Element::writeRefAttribute(thread, writeProxy, attrib);
@@ -4374,6 +4383,34 @@ bool VisualElement::scriptSetDirect(const DynamicValue &dest) {
 	return false;
 }
 
+bool VisualElement::scriptSetPosition(const DynamicValue &dest) {
+	if (dest.getType() == DynamicValueTypes::kPoint) {
+		const Point16 &destPoint = dest.getPoint();
+		int32 xDelta = destPoint.x - _rect.left;
+		int32 yDelta = destPoint.y - _rect.right;
+
+		offsetTranslate(xDelta, yDelta);
+
+		return true;
+	}
+	return false;
+}
+
+void VisualElement::offsetTranslate(int32 xDelta, int32 yDelta) {
+	_rect.left += xDelta;
+	_rect.right += xDelta;
+	_rect.top += yDelta;
+	_rect.bottom += yDelta;
+
+	for (const Common::SharedPtr<Structural> &child : _children) {
+		if (child->isElement()) {
+			Element *element = static_cast<Element *>(child.get());
+			if (element->isVisual())
+				static_cast<VisualElement *>(element)->offsetTranslate(xDelta, yDelta);
+		}
+	}
+}
+
 VThreadState VisualElement::changeVisibilityTask(const ChangeFlagTaskData &taskData) {
 	if (_visible != taskData.desiredFlag) {
 		_visible = taskData.desiredFlag;
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 51ccf30e9a7..22015f92336 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -39,6 +39,12 @@
 
 class OSystem;
 
+namespace Common {
+
+class RandomSource;
+
+} // End of namespace Common
+
 namespace Graphics {
 
 struct WinCursorGroup;
@@ -398,6 +404,13 @@ struct AngleMagVector {
 		return !((*this) == other);
 	}
 
+	inline static AngleMagVector create(double angleRadians, double magnitude) {
+		AngleMagVector result;
+		result.angleRadians = angleRadians;
+		result.magnitude = magnitude;
+		return result;
+	}
+
 	bool dynSetAngleDegrees(const DynamicValue &value);
 	void dynGetAngleDegrees(DynamicValue &value) const;
 
@@ -1267,6 +1280,8 @@ public:
 	void onMouseMove(int32 x, int32 y);
 	void onMouseUp(int32 x, int32 y, Actions::MouseButton mButton);
 
+	Common::RandomSource *getRandom() const;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	void debugSetEnabled(bool enabled);
 	void debugBreak();
@@ -1360,6 +1375,8 @@ private:
 
 	Common::SharedPtr<Graphics::MacFontManager> _macFontMan;
 
+	Common::SharedPtr<Common::RandomSource> _random;
+
 	uint32 _nextRuntimeGUID;
 
 	bool _displayModeSupported[kColorDepthModeCount];
@@ -1823,6 +1840,9 @@ protected:
 	bool loadCommon(const Common::String &name, uint32 guid, const Data::Rect &rect, uint32 elementFlags, uint16 layer, uint32 streamLocator, uint16 sectionID);
 
 	bool scriptSetDirect(const DynamicValue &dest);
+	bool scriptSetPosition(const DynamicValue &dest);
+
+	void offsetTranslate(int32 xDelta, int32 yDelta);
 
 	struct ChangeFlagTaskData {
 		bool desiredFlag;


Commit: d966b988b781d108736e785f37c1c8de9ecd68b0
    https://github.com/scummvm/scummvm/commit/d966b988b781d108736e785f37c1c8de9ecd68b0
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add width/height read attribs, fix up some error messages

Changed paths:
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/runtime.cpp


diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 5925b0bdbc9..bc2470ddf45 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -1261,13 +1261,13 @@ MiniscriptInstructionOutcome GetChild::execute(MiniscriptThread *thread) const {
 			if (indexableValueSlot.value.getType() == DynamicValueTypes::kObject) {
 				Common::SharedPtr<RuntimeObject> obj = indexableValueSlot.value.getObject().object.lock();
 				if (!obj) {
-					thread->error("Tried to read '" + attrib + "' to an invalid object reference");
+					thread->error("Tried to indirect '" + attrib + "' using an invalid object reference");
 					return kMiniscriptInstructionOutcomeFailed;
 				}
 
 				DynamicValueWriteProxy writeProxy;
 				if (!obj->writeRefAttribute(thread, writeProxy, attrib)) {
-					thread->error("Failed to read attribute '" + attrib + "'");
+					thread->error("Failed to get a writeable reference to attribute '" + attrib + "'");
 					return kMiniscriptInstructionOutcomeFailed;
 				}
 
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index b49847d0bed..b9445ade40e 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -4330,6 +4330,12 @@ bool VisualElement::readAttribute(MiniscriptThread *thread, DynamicValue &result
 	} else if (attrib == "position") {
 		result.setPoint(Point16::create(_rect.left, _rect.top));
 		return true;
+	} else if (attrib == "width") {
+		result.setInt(_rect.right - _rect.left);
+		return true;
+	} else if (attrib == "height") {
+		result.setInt(_rect.bottom - _rect.top);
+		return true;
 	}
 
 	return Element::readAttribute(thread, result, attrib);


Commit: 089335f531862f59cd668733fc1eb00787ed8cf5
    https://github.com/scummvm/scummvm/commit/089335f531862f59cd668733fc1eb00787ed8cf5
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Pause refactor, allow proxy writes to suspend execution and trigger VThread tasks

Changed paths:
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/miniscript.h
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index c4ba0abf86a..56ec5aaf276 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -52,7 +52,7 @@ void GraphicElement::render(Window *window) {
 }
 
 MovieElement::MovieElement()
-	: _cacheBitmap(false), _paused(false), _reversed(false), _haveFiredAtFirstCel(false), _haveFiredAtLastCel(false)
+	: _cacheBitmap(false), _reversed(false), _haveFiredAtFirstCel(false), _haveFiredAtLastCel(false)
 	, _loop(false), _alternate(false), _playEveryFrame(false), _assetID(0), _runtime(nullptr) {
 }
 
@@ -79,25 +79,6 @@ bool MovieElement::load(ElementLoaderContext &context, const Data::MovieElement
 	return true;
 }
 
-bool MovieElement::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
-	if (attrib == "paused") {
-		result.setBool(_paused);
-		return true;
-	}
-
-	return VisualElement::readAttribute(thread, result, attrib);
-}
-
-bool MovieElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
-
-	if (attrib == "paused") {
-		DynamicValueWriteFuncHelper<MovieElement, &MovieElement::scriptSetPaused>::create(this, writeProxy);
-		return true;
-	}
-	
-	return VisualElement::writeRefAttribute(thread, writeProxy, attrib);
-}
-
 VThreadState MovieElement::consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
 	if (Event::create(EventIDs::kPlay, 0).respondsTo(msg->getEvent())) {
 		StartPlayingTaskData *startPlayingTaskData = runtime->getVThread().pushTask("MovieElement::startPlayingTask", this, &MovieElement::startPlayingTask);
@@ -200,14 +181,6 @@ void MovieElement::onPostRender(Runtime *runtime, Project *project) {
 	}
 }
 
-bool MovieElement::scriptSetPaused(const DynamicValue& dest) {
-	if (dest.getType() == DynamicValueTypes::kBoolean) {
-		_paused = dest.getBool();
-		return true;
-	}
-	return false;
-}
-
 void MovieElement::onSegmentUnloaded(int segmentIndex) {
 	_videoDecoder.reset();
 }
@@ -252,7 +225,7 @@ bool ImageElement::readAttribute(MiniscriptThread *thread, DynamicValue &result,
 	return VisualElement::readAttribute(thread, result, attrib);
 }
 
-bool ImageElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
+MiniscriptInstructionOutcome ImageElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
 	return VisualElement::writeRefAttribute(thread, writeProxy, attrib);
 }
 
@@ -450,7 +423,7 @@ bool TextLabelElement::readAttribute(MiniscriptThread *thread, DynamicValue &res
 	return VisualElement::readAttribute(thread, result, attrib);
 }
 
-bool TextLabelElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
+MiniscriptInstructionOutcome TextLabelElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
 	return VisualElement::writeRefAttribute(thread, writeProxy, attrib);
 }
 
@@ -511,7 +484,7 @@ bool SoundElement::readAttribute(MiniscriptThread *thread, DynamicValue &result,
 	return NonVisualElement::readAttribute(thread, result, attrib);
 }
 
-bool SoundElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
+MiniscriptInstructionOutcome SoundElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
 	return NonVisualElement::writeRefAttribute(thread, writeProxy, attrib);
 }
 
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index 56bd82422db..9923c7d53bb 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -61,8 +61,6 @@ public:
 
 	bool load(ElementLoaderContext &context, const Data::MovieElement &data);
 
-	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
-	bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) override;
 	VThreadState consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
 
 	void activate() override;
@@ -77,8 +75,6 @@ public:
 #endif
 
 private:
-	bool scriptSetPaused(const DynamicValue &dest);
-
 	void onSegmentUnloaded(int segmentIndex) override;
 
 	struct StartPlayingTaskData {
@@ -88,7 +84,6 @@ private:
 	VThreadState startPlayingTask(const StartPlayingTaskData &taskData);
 
 	bool _cacheBitmap;
-	bool _paused;
 	bool _loop;
 	bool _alternate;
 	bool _playEveryFrame;
@@ -112,7 +107,7 @@ public:
 	bool load(ElementLoaderContext &context, const Data::ImageElement &data);
 
 	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
-	bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
 
 	void activate() override;
 	void deactivate() override;
@@ -141,7 +136,7 @@ public:
 	bool load(ElementLoaderContext &context, const Data::TextLabelElement &data);
 
 	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
-	bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
 
 	void activate() override;
 	void deactivate() override;
@@ -175,7 +170,7 @@ public:
 	bool load(ElementLoaderContext &context, const Data::SoundElement &data);
 
 	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
-	bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
 
 	void activate() override;
 	void deactivate() override;
@@ -186,7 +181,6 @@ public:
 #endif
 
 private:
-	bool _paused;
 	uint16 _leftVolume;
 	uint16 _rightVolume;
 	int16 _balance;
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index bc2470ddf45..5cb199a60f4 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -476,9 +476,10 @@ MiniscriptInstructionOutcome Set::execute(MiniscriptThread *thread) const {
 
 	if (target.value.getType() == DynamicValueTypes::kWriteProxy) {
 		const DynamicValueWriteProxyPOD &proxy = target.value.getWriteProxyPOD();
-		if (!proxy.ifc->write(thread, srcValue.value, proxy.objectRef, proxy.ptrOrOffset)) {
+		outcome = proxy.ifc->write(thread, srcValue.value, proxy.objectRef, proxy.ptrOrOffset);
+		if (outcome == kMiniscriptInstructionOutcomeFailed) {
 			thread->error("Failed to assign value");
-			return kMiniscriptInstructionOutcomeFailed;
+			return outcome;
 		}
 	} else {
 		VariableModifier *var = nullptr;
@@ -551,7 +552,7 @@ MiniscriptInstructionOutcome Send::execute(MiniscriptThread *thread) const {
 
 	if (_messageFlags.immediate) {
 		thread->getRuntime()->sendMessageOnVThread(dispatch);
-		return kMiniscriptInstructionOutcomeYieldToVThread;
+		return kMiniscriptInstructionOutcomeYieldToVThreadNoRetry;
 	} else {
 		thread->getRuntime()->queueMessage(dispatch);
 		return kMiniscriptInstructionOutcomeContinue;
@@ -1202,6 +1203,8 @@ MiniscriptInstructionOutcome GetChild::execute(MiniscriptThread *thread) const {
 
 	const Common::String &attrib = attribs[_attribute].name;
 
+	MiniscriptInstructionOutcome outcome = kMiniscriptInstructionOutcomeFailed;
+
 	if (_isIndexed) {
 		if (thread->getStackSize() < 2) {
 			thread->error("Stack underflow");
@@ -1209,7 +1212,7 @@ MiniscriptInstructionOutcome GetChild::execute(MiniscriptThread *thread) const {
 		}
 
 		// Convert index
-		MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0, false);
+		outcome = thread->dereferenceRValue(0, false);
 		if (outcome != kMiniscriptInstructionOutcomeContinue)
 			return outcome;
 
@@ -1225,16 +1228,20 @@ MiniscriptInstructionOutcome GetChild::execute(MiniscriptThread *thread) const {
 				}
 
 				DynamicValueWriteProxy proxy;
-				if (!obj->writeRefAttributeIndexed(thread, proxy, attrib, indexSlot.value)) {
-					thread->error("Failed to get a writeable reference to attribute '" + attrib + "'");
-					return kMiniscriptInstructionOutcomeFailed;
+				outcome = obj->writeRefAttributeIndexed(thread, proxy, attrib, indexSlot.value);
+				if (outcome == kMiniscriptInstructionOutcomeFailed) {
+					thread->error("Failed to get a writeable reference to indexed attribute '" + attrib + "'");
+					return outcome;
 				}
+
+				indexableValueSlot.value.setWriteProxy(proxy);
 			} else if (indexableValueSlot.value.getType() == DynamicValueTypes::kWriteProxy) {
 				DynamicValueWriteProxy proxy = indexableValueSlot.value.getWriteProxyTEMP();
 
-				if (!proxy.pod.ifc->refAttribIndexed(thread, proxy, proxy.pod.objectRef, proxy.pod.ptrOrOffset, attrib, indexSlot.value)) {
+				outcome = proxy.pod.ifc->refAttribIndexed(thread, proxy, proxy.pod.objectRef, proxy.pod.ptrOrOffset, attrib, indexSlot.value);
+				if (outcome == kMiniscriptInstructionOutcomeFailed) {
 					thread->error("Can't write to indexed attribute '" + attrib + "'");
-					return kMiniscriptInstructionOutcomeFailed;
+					return outcome;
 				}
 
 				indexableValueSlot.value.setWriteProxy(proxy);
@@ -1266,17 +1273,19 @@ MiniscriptInstructionOutcome GetChild::execute(MiniscriptThread *thread) const {
 				}
 
 				DynamicValueWriteProxy writeProxy;
-				if (!obj->writeRefAttribute(thread, writeProxy, attrib)) {
+				outcome = obj->writeRefAttribute(thread, writeProxy, attrib);
+				if (outcome == kMiniscriptInstructionOutcomeFailed) {
 					thread->error("Failed to get a writeable reference to attribute '" + attrib + "'");
-					return kMiniscriptInstructionOutcomeFailed;
+					return outcome;
 				}
 
 				indexableValueSlot.value.setWriteProxy(writeProxy);
 			} else if (indexableValueSlot.value.getType() == DynamicValueTypes::kWriteProxy) {
 				DynamicValueWriteProxy proxy = indexableValueSlot.value.getWriteProxyTEMP();
-				if (!proxy.pod.ifc->refAttrib(thread, proxy, proxy.pod.objectRef, proxy.pod.ptrOrOffset, attrib)) {
+				outcome = proxy.pod.ifc->refAttrib(thread, proxy, proxy.pod.objectRef, proxy.pod.ptrOrOffset, attrib);
+				if (outcome == kMiniscriptInstructionOutcomeFailed) {
 					thread->error("Can't write to attribute '" + attrib + "'");
-					return kMiniscriptInstructionOutcomeFailed;
+					return outcome;
 				}
 				indexableValueSlot.value.setWriteProxy(proxy);
 			} else {
@@ -1284,13 +1293,11 @@ MiniscriptInstructionOutcome GetChild::execute(MiniscriptThread *thread) const {
 				return kMiniscriptInstructionOutcomeFailed;
 			}
 		} else {
-			MiniscriptInstructionOutcome outcome = readRValueAttrib(thread, indexableValueSlot.value, attrib);
-			if (outcome != kMiniscriptInstructionOutcomeContinue)
-				return outcome;
+			outcome = readRValueAttrib(thread, indexableValueSlot.value, attrib);
 		}
 	}
 
-	return kMiniscriptInstructionOutcomeContinue;
+	return outcome;
 }
 
 MiniscriptInstructionOutcome GetChild::readRValueAttrib(MiniscriptThread *thread, DynamicValue &valueSrcDest, const Common::String &attrib) const {
@@ -1678,7 +1685,8 @@ VThreadState MiniscriptThread::resume(const ResumeTaskData &taskData) {
 	}
 
 	while (_currentInstruction < numInstrs && !_failed) {
-		MiniscriptInstruction *instr = instrs[_currentInstruction++];
+		size_t instrNum = _currentInstruction++;
+		MiniscriptInstruction *instr = instrs[instrNum];
 
 		MiniscriptInstructionOutcome outcome = instr->execute(this);
 		if (outcome == kMiniscriptInstructionOutcomeFailed) {
@@ -1687,7 +1695,12 @@ VThreadState MiniscriptThread::resume(const ResumeTaskData &taskData) {
 			return kVThreadReturn;
 		}
 
-		if (outcome == kMiniscriptInstructionOutcomeYieldToVThread)
+		if (outcome == kMiniscriptInstructionOutcomeYieldToVThreadAndRetry) {
+			_currentInstruction = instrNum;
+			return kVThreadReturn;
+		}
+
+		if (outcome == kMiniscriptInstructionOutcomeYieldToVThreadNoRetry)
 			return kVThreadReturn;
 	}
 
diff --git a/engines/mtropolis/miniscript.h b/engines/mtropolis/miniscript.h
index 462bc271177..145723d73cf 100644
--- a/engines/mtropolis/miniscript.h
+++ b/engines/mtropolis/miniscript.h
@@ -31,12 +31,6 @@ class MiniscriptThread;
 struct MiniscriptStackValue;
 struct IMiniscriptInstructionFactory;
 
-enum MiniscriptInstructionOutcome {
-	kMiniscriptInstructionOutcomeContinue,			// Continue executing next instruction
-	kMiniscriptInstructionOutcomeYieldToVThread,	// Instruction pushed a VThread task
-	kMiniscriptInstructionOutcomeFailed,			// Instruction errored
-};
-
 class MiniscriptInstruction {
 public:
 	virtual ~MiniscriptInstruction();
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index f6d26746df6..bfc118e329e 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -804,19 +804,19 @@ bool CompoundVariableModifier::readAttributeIndexed(MiniscriptThread *thread, Dy
 	return var->readAttributeIndexed(thread, result, "value", index);
 }
 
-bool CompoundVariableModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
+MiniscriptInstructionOutcome CompoundVariableModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
 	Modifier *var = findChildByName(attrib);
 	if (!var || !var->isVariable())
-		return false;
+		return kMiniscriptInstructionOutcomeFailed;
 
 	writeProxy = static_cast<VariableModifier *>(var)->createWriteProxy();
-	return true;
+	return kMiniscriptInstructionOutcomeContinue;
 }
 
-bool CompoundVariableModifier::writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib, const DynamicValue &index) {
+MiniscriptInstructionOutcome CompoundVariableModifier::writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib, const DynamicValue &index) {
 	Modifier *var = findChildByName(attrib);
 	if (!var || !var->isModifier())
-		return false;
+		return kMiniscriptInstructionOutcomeFailed;
 
 	return var->writeRefAttributeIndexed(thread, writeProxy, "value", index);
 }
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index f20fa22ab68..a33ad62c714 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -577,8 +577,8 @@ private:
 
 	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
 	bool readAttributeIndexed(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib, const DynamicValue &index) override;
-	bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) override;
-	bool writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib, const DynamicValue &index) override;
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) override;
+	MiniscriptInstructionOutcome writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib, const DynamicValue &index) override;
 
 	Modifier *findChildByName(const Common::String &name) const;
 
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 1b31c614591..704c2c2f704 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -190,14 +190,14 @@ bool ObjectReferenceVariableModifier::readAttribute(MiniscriptThread *thread, Dy
 	return VariableModifier::readAttribute(thread, result, attrib);
 }
 
-bool ObjectReferenceVariableModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
+MiniscriptInstructionOutcome ObjectReferenceVariableModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
 	if (attrib == "path") {
-		DynamicValueWriteFuncHelper<ObjectReferenceVariableModifier, &ObjectReferenceVariableModifier::dynSetPath>::create(this, result);
-		return true;
+		DynamicValueWriteFuncHelper<ObjectReferenceVariableModifier, &ObjectReferenceVariableModifier::scriptSetPath>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
 	}
 	if (attrib == "object") {
-		DynamicValueWriteFuncHelper<ObjectReferenceVariableModifier, &ObjectReferenceVariableModifier::dynSetObject>::create(this, result);
-		return true;
+		DynamicValueWriteFuncHelper<ObjectReferenceVariableModifier, &ObjectReferenceVariableModifier::scriptSetObject>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
 	}
 
 	return VariableModifier::writeRefAttribute(thread, result, attrib);
@@ -209,9 +209,9 @@ bool ObjectReferenceVariableModifier::varSetValue(MiniscriptThread *thread, cons
 	switch (value.getType()) {
 	case DynamicValueTypes::kNull:
 	case DynamicValueTypes::kObject:
-		return dynSetObject(value);
+		return scriptSetObject(thread, value);
 	case DynamicValueTypes::kString:
-		return dynSetPath(value);
+		return scriptSetPath(thread, value);
 	default:
 		return false;
 	}
@@ -225,37 +225,37 @@ Common::SharedPtr<Modifier> ObjectReferenceVariableModifier::shallowClone() cons
 	return Common::SharedPtr<Modifier>(new ObjectReferenceVariableModifier(*this));
 }
 
-bool ObjectReferenceVariableModifier::dynSetPath(const DynamicValue &value) {
+MiniscriptInstructionOutcome ObjectReferenceVariableModifier::scriptSetPath(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() != DynamicValueTypes::kString)
-		return false;
+		return kMiniscriptInstructionOutcomeFailed;
 
 	_objectPath = value.getString();
 	_object.reset();
 
-	return true;
+	return kMiniscriptInstructionOutcomeContinue;
 }
 
-bool ObjectReferenceVariableModifier::dynSetObject(const DynamicValue &value) {
+MiniscriptInstructionOutcome ObjectReferenceVariableModifier::scriptSetObject(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kNull) {
 		_object.reset();
 		_objectPath.clear();
 		_fullPath.clear();
 
-		return true;
+		return kMiniscriptInstructionOutcomeContinue;
 	} else if (value.getType() == DynamicValueTypes::kObject) {
 		Common::SharedPtr<RuntimeObject> obj = value.getObject().object.lock();
 		if (!obj)
-			return dynSetObject(DynamicValue());
+			return scriptSetObject(thread, DynamicValue());
 
 		if (!computeObjectPath(obj.get(), _fullPath))
-			return dynSetObject(DynamicValue());
+			return scriptSetObject(thread, DynamicValue());
 
 		_objectPath = _fullPath;
 		_object.object = obj;
 
-		return true;
+		return kMiniscriptInstructionOutcomeContinue;
 	} else
-		return false;
+		return kMiniscriptInstructionOutcomeFailed;
 }
 
 void ObjectReferenceVariableModifier::resolve() {
@@ -576,6 +576,26 @@ void ListVariableModifier::varGetValue(MiniscriptThread *thread, DynamicValue &d
 	dest.setList(_list);
 }
 
+bool ListVariableModifier::readAttributeIndexed(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib, const DynamicValue &index) {
+	if (attrib == "value") {
+		size_t realIndex = 0;
+		return _list->dynamicValueToIndex(realIndex, index) && _list->getAtIndex(realIndex, result);
+	}
+	return false;
+}
+
+MiniscriptInstructionOutcome ListVariableModifier::writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib, const DynamicValue &index) {
+	if (attrib == "value") {
+		size_t realIndex = 0;
+		if (!_list->dynamicValueToIndex(realIndex, index))
+			return kMiniscriptInstructionOutcomeFailed;
+
+		_list->createWriteProxyForIndex(realIndex, writeProxy);
+		writeProxy.containerList = _list;
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+	return kMiniscriptInstructionOutcomeFailed;
+}
 
 ListVariableModifier::ListVariableModifier(const ListVariableModifier &other) {
 	if (other._list)
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index da1eee953d5..f928b6a0261 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -97,7 +97,7 @@ public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::ObjectReferenceVariableModifier &data);
 
 	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
-	bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib);
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib);
 
 	bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override;
 	void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override;
@@ -109,8 +109,8 @@ public:
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
-	bool dynSetPath(const DynamicValue &value);
-	bool dynSetObject(const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetPath(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetObject(MiniscriptThread *thread, const DynamicValue &value);
 
 	void resolve();
 	void resolveRelativePath(RuntimeObject *obj, const Common::String &path, size_t startPos);
@@ -192,6 +192,10 @@ public:
 	bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override;
 	void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override;
 
+	bool readAttributeIndexed(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib, const DynamicValue &index) override;
+	MiniscriptInstructionOutcome writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib, const DynamicValue &index) override;
+
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "List Variable Modifier"; }
 #endif
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index b9445ade40e..325159ffcf1 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -24,6 +24,7 @@
 #include "mtropolis/vthread.h"
 #include "mtropolis/asset_factory.h"
 #include "mtropolis/element_factory.h"
+#include "mtropolis/miniscript.h"
 #include "mtropolis/modifier_factory.h"
 #include "mtropolis/modifiers.h"
 #include "mtropolis/render.h"
@@ -227,17 +228,17 @@ bool Point16::load(const Data::Point &point) {
 	return true;
 }
 
-bool Point16::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib) {
+MiniscriptInstructionOutcome Point16::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib) {
 	if (attrib == "x") {
 		DynamicValueWriteIntegerHelper<int16>::create(&x, proxy);
-		return true;
+		return kMiniscriptInstructionOutcomeContinue;
 	}
 	if (attrib == "y") {
 		DynamicValueWriteIntegerHelper<int16>::create(&y, proxy);
-		return true;
+		return kMiniscriptInstructionOutcomeContinue;
 	}
 
-	return false;
+	return kMiniscriptInstructionOutcomeFailed;
 }
 
 bool Rect16::load(const Data::Rect &rect) {
@@ -897,72 +898,79 @@ void DynamicList::initFromOther(const DynamicList &other) {
 	}
 }
 
-bool DynamicList::WriteProxyInterface::write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const {
-	return static_cast<DynamicList *>(objectRef)->setAtIndex(ptrOrOffset, value);
+MiniscriptInstructionOutcome DynamicList::WriteProxyInterface::write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const {
+	if (!static_cast<DynamicList *>(objectRef)->setAtIndex(ptrOrOffset, value))
+		return kMiniscriptInstructionOutcomeFailed;
+	return kMiniscriptInstructionOutcomeContinue;
 }
 
-bool DynamicList::WriteProxyInterface::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const {
+MiniscriptInstructionOutcome DynamicList::WriteProxyInterface::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const {
 	DynamicList *list = static_cast<DynamicList *>(objectRef);
+	bool succeeded = false;
 	switch (list->getType()) {
 	case DynamicValueTypes::kPoint:
 		list->expandToMinimumSize(ptrOrOffset + 1);
-		return list->getPoint()[ptrOrOffset].refAttrib(thread, proxy, attrib);
+		succeeded = list->getPoint()[ptrOrOffset].refAttrib(thread, proxy, attrib);
 		break;
 	case DynamicValueTypes::kIntegerRange:
 		list->expandToMinimumSize(ptrOrOffset + 1);
-		return list->getIntRange()[ptrOrOffset].refAttrib(thread, proxy, attrib);
+		succeeded = list->getIntRange()[ptrOrOffset].refAttrib(thread, proxy, attrib);
+		break;
 	case DynamicValueTypes::kVector:
 		list->expandToMinimumSize(ptrOrOffset + 1);
-		return list->getVector()[ptrOrOffset].refAttrib(thread, proxy, attrib);
+		succeeded = list->getVector()[ptrOrOffset].refAttrib(thread, proxy, attrib);
 	case DynamicValueTypes::kObject: {
 			if (list->getSize() <= ptrOrOffset)
-				return false;
+			return kMiniscriptInstructionOutcomeFailed;
 
 			Common::SharedPtr<RuntimeObject> obj = list->getObjectReference()[ptrOrOffset].object.lock();
 			proxy.containerList.reset();
-			if (!obj && !obj->writeRefAttribute(thread, proxy, attrib))
-				return false;
-
-			return true;
-		}
+			succeeded = (obj && obj->writeRefAttribute(thread, proxy, attrib));
+		} break;
 	default:
-		return false;
+		succeeded = false;
+		break;
 	}
+
+	if (succeeded)
+		return kMiniscriptInstructionOutcomeContinue;
+
+	return kMiniscriptInstructionOutcomeFailed;
 }
 
-bool DynamicList::WriteProxyInterface::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const {
+MiniscriptInstructionOutcome DynamicList::WriteProxyInterface::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const {
 	DynamicList *list = static_cast<DynamicList *>(objectRef);
 	switch (list->getType()) {
 	case DynamicValueTypes::kList: {
 			if (list->getSize() <= ptrOrOffset)
-				return false;
+				return kMiniscriptInstructionOutcomeFailed;
 
 			Common::SharedPtr<DynamicList> subList = list->getList()[ptrOrOffset];
 
 			size_t subIndex = 0;
 			if (!subList->dynamicValueToIndex(subIndex, index))
-				return false;
+				return kMiniscriptInstructionOutcomeFailed;
 
 			subList->createWriteProxyForIndex(subIndex, proxy);
 			proxy.containerList = subList;
-			return true;
+			return kMiniscriptInstructionOutcomeContinue;
 		}
 	case DynamicValueTypes::kObject: {
 			if (list->getSize() <= ptrOrOffset)
-				return false;
+				return kMiniscriptInstructionOutcomeFailed;
 
 			Common::SharedPtr<RuntimeObject> obj = list->getObjectReference()[ptrOrOffset].object.lock();
 			proxy.containerList.reset();
 			if (!obj && !obj->writeRefAttributeIndexed(thread, proxy, attrib, index))
-				return false;
+				return kMiniscriptInstructionOutcomeFailed;
 
-			return true;
+			return kMiniscriptInstructionOutcomeContinue;
 		}
 	default:
-		return false;
+		return kMiniscriptInstructionOutcomeFailed;
 	}
 
-	return false;
+	return kMiniscriptInstructionOutcomeFailed;
 }
 
 DynamicList::WriteProxyInterface DynamicList::WriteProxyInterface::_instance;
@@ -1425,23 +1433,23 @@ void DynamicValue::initFromOther(const DynamicValue &other) {
 	_type = other._type;
 }
 
-bool DynamicValueWriteStringHelper::write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const {
+MiniscriptInstructionOutcome DynamicValueWriteStringHelper::write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const {
 	Common::String &dest = *static_cast<Common::String *>(objectRef);
 	switch (value.getType()) {
 	case DynamicValueTypes::kString:
 		dest = value.getString();
-		return true;
+		return kMiniscriptInstructionOutcomeContinue;
 	default:
-		return false;
+		return kMiniscriptInstructionOutcomeFailed;
 	}
 }
 
-bool DynamicValueWriteStringHelper::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const {
-	return false;
+MiniscriptInstructionOutcome DynamicValueWriteStringHelper::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const {
+	return kMiniscriptInstructionOutcomeFailed;
 }
 
-bool DynamicValueWriteStringHelper::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const {
-	return false;
+MiniscriptInstructionOutcome DynamicValueWriteStringHelper::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const {
+	return kMiniscriptInstructionOutcomeFailed;
 }
 
 void DynamicValueWriteStringHelper::create(Common::String *strValue, DynamicValueWriteProxy &proxy) {
@@ -1686,7 +1694,7 @@ bool Event::load(const Data::Event &data) {
 	return true;
 }
 
-bool AngleMagVector::dynSetAngleDegrees(const DynamicValue &value) {
+MiniscriptInstructionOutcome AngleMagVector::scriptSetAngleDegrees(MiniscriptThread *thread, const DynamicValue &value) {
 	double degrees = 0.0;
 	switch (value.getType())
 	{
@@ -1697,30 +1705,31 @@ bool AngleMagVector::dynSetAngleDegrees(const DynamicValue &value) {
 		degrees = value.getFloat();
 		break;
 	default:
-		return false;
+		return kMiniscriptInstructionOutcomeFailed;
 	}
 
 	angleRadians = degrees * (M_PI / 180.0);
 
-	return true;
+	return kMiniscriptInstructionOutcomeContinue;
+	;
 }
 
-void AngleMagVector::dynGetAngleDegrees(DynamicValue &value) const {
+void AngleMagVector::scriptGetAngleDegrees(DynamicValue &value) const {
 	value.setFloat(angleRadians * (180.0 / M_PI));
 }
 
 
-bool AngleMagVector::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib) {
+MiniscriptInstructionOutcome AngleMagVector::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib) {
 	if (attrib == "angle") {
-		DynamicValueWriteFuncHelper<AngleMagVector, &AngleMagVector::dynSetAngleDegrees>::create(this, proxy);
-		return true;
+		DynamicValueWriteFuncHelper<AngleMagVector, &AngleMagVector::scriptSetAngleDegrees>::create(this, proxy);
+		return kMiniscriptInstructionOutcomeContinue;
 	}
 	if (attrib == "magnitude") {
 		DynamicValueWriteFloatHelper<double>::create(&magnitude, proxy);
-		return true;
+		return kMiniscriptInstructionOutcomeContinue;
 	}
 
-	return false;
+	return kMiniscriptInstructionOutcomeFailed;
 }
 
 void IPlugInModifierRegistrar::registerPlugInModifier(const char *name, const IPlugInModifierFactoryAndDataFactory *loaderFactory) {
@@ -1879,12 +1888,12 @@ bool RuntimeObject::readAttributeIndexed(MiniscriptThread *thread, DynamicValue
 	return false;
 }
 
-bool RuntimeObject::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
-	return false;
+MiniscriptInstructionOutcome RuntimeObject::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
+	return kMiniscriptInstructionOutcomeFailed;
 }
 
-bool RuntimeObject::writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib, const DynamicValue &index) {
-	return false;
+MiniscriptInstructionOutcome RuntimeObject::writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib, const DynamicValue &index) {
+	return kMiniscriptInstructionOutcomeFailed;
 }
 
 MessageProperties::MessageProperties(const Event &evt, const DynamicValue &value, const Common::WeakPtr<RuntimeObject> &source)
@@ -1903,7 +1912,7 @@ const Common::WeakPtr<RuntimeObject>& MessageProperties::getSource() const {
 	return _source;
 }
 
-Structural::Structural() : _parent(nullptr) {
+Structural::Structural() : _parent(nullptr), _paused(false) {
 }
 
 Structural::~Structural() {
@@ -1925,14 +1934,22 @@ bool Structural::readAttribute(MiniscriptThread *thread, DynamicValue &result, c
 		result.setString(_name);
 		return true;
 	}
+	if (attrib == "paused") {
+		result.setBool(_paused);
+		return true;
+	}
 
 	return RuntimeObject::readAttribute(thread, result, attrib);
 }
 
-bool Structural::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
+MiniscriptInstructionOutcome Structural::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
 	if (attrib == "name") {
 		DynamicValueWriteStringHelper::create(&_name, result);
-		return true;
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+	if (attrib == "paused") {
+		DynamicValueWriteFuncHelper<Structural, &Structural::scriptSetPaused>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
 	}
 
 	return RuntimeObject::writeRefAttribute(thread, result, attrib);
@@ -2093,6 +2110,9 @@ DebugInspector *Structural::debugCreateInspector() {
 void Structural::linkInternalReferences(ObjectLinkingScope *scope) {
 }
 
+void Structural::onPauseStateChanged() {
+}
+
 ObjectLinkingScope *Structural::getPersistentStructuralScope() {
 	return nullptr;
 }
@@ -2101,6 +2121,25 @@ ObjectLinkingScope* Structural::getPersistentModifierScope() {
 	return nullptr;
 }
 
+MiniscriptInstructionOutcome Structural::scriptSetPaused(MiniscriptThread *thread, const DynamicValue &value) {
+	if (value.getType() != DynamicValueTypes::kBoolean)
+		return kMiniscriptInstructionOutcomeFailed;
+
+	const bool targetValue = value.getBool();
+
+	if (targetValue == _paused)
+		return kMiniscriptInstructionOutcomeContinue;
+
+	_paused = targetValue;
+	onPauseStateChanged();
+
+	Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(targetValue ? EventIDs::kPause : EventIDs::kUnpause, 0), DynamicValue(), getSelfReference()));
+	Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+	thread->getRuntime()->sendMessageOnVThread(dispatch);
+
+	return kMiniscriptInstructionOutcomeYieldToVThreadNoRetry;
+}
+
 ObjectLinkingScope::ObjectLinkingScope() : _parent(nullptr) {
 }
 
@@ -4341,29 +4380,29 @@ bool VisualElement::readAttribute(MiniscriptThread *thread, DynamicValue &result
 	return Element::readAttribute(thread, result, attrib);
 }
 
-bool VisualElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
+MiniscriptInstructionOutcome VisualElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
 	if (attrib == "visible") {
 		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetVisibility>::create(this, writeProxy);
-		return true;
+		return kMiniscriptInstructionOutcomeContinue;
 	} else if (attrib == "direct") {
 		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetDirect>::create(this, writeProxy);
-		return true;
+		return kMiniscriptInstructionOutcomeContinue;
 	} else if (attrib == "position") {
 		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetPosition>::create(this, writeProxy);
-		return true;
+		return kMiniscriptInstructionOutcomeContinue;
 	}
 
 	return Element::writeRefAttribute(thread, writeProxy, attrib);
 }
 
-bool VisualElement::scriptSetVisibility(const DynamicValue& result) {
+MiniscriptInstructionOutcome VisualElement::scriptSetVisibility(MiniscriptThread *thread, const DynamicValue &result) {
 	// FIXME: Need to make this fire Show/Hide events!
 	if (result.getType() == DynamicValueTypes::kBoolean) {
 		_visible = result.getBool();
-		return true;
+		return kMiniscriptInstructionOutcomeContinue;
 	}
 
-	return false;
+	return kMiniscriptInstructionOutcomeFailed;
 }
 
 bool VisualElement::loadCommon(const Common::String &name, uint32 guid, const Data::Rect &rect, uint32 elementFlags, uint16 layer, uint32 streamLocator, uint16 sectionID) {
@@ -4381,15 +4420,15 @@ bool VisualElement::loadCommon(const Common::String &name, uint32 guid, const Da
 	return true;
 }
 
-bool VisualElement::scriptSetDirect(const DynamicValue &dest) {
+MiniscriptInstructionOutcome VisualElement::scriptSetDirect(MiniscriptThread *thread, const DynamicValue &dest) {
 	if (dest.getType() == DynamicValueTypes::kBoolean) {
 		_directToScreen = dest.getBool();
-		return true;
+		return kMiniscriptInstructionOutcomeContinue;
 	}
-	return false;
+	return kMiniscriptInstructionOutcomeFailed;
 }
 
-bool VisualElement::scriptSetPosition(const DynamicValue &dest) {
+MiniscriptInstructionOutcome VisualElement::scriptSetPosition(MiniscriptThread *thread, const DynamicValue &dest) {
 	if (dest.getType() == DynamicValueTypes::kPoint) {
 		const Point16 &destPoint = dest.getPoint();
 		int32 xDelta = destPoint.x - _rect.left;
@@ -4397,9 +4436,9 @@ bool VisualElement::scriptSetPosition(const DynamicValue &dest) {
 
 		offsetTranslate(xDelta, yDelta);
 
-		return true;
+		return kMiniscriptInstructionOutcomeContinue;
 	}
-	return false;
+	return kMiniscriptInstructionOutcomeFailed;
 }
 
 void VisualElement::offsetTranslate(int32 xDelta, int32 yDelta) {
@@ -4585,15 +4624,17 @@ DynamicValueWriteProxy VariableModifier::createWriteProxy() {
 	return proxy;
 }
 
-bool VariableModifier::WriteProxyInterface::write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const {
-	return static_cast<VariableModifier *>(objectRef)->varSetValue(thread, dest);
+MiniscriptInstructionOutcome VariableModifier::WriteProxyInterface::write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const {
+	if (!static_cast<VariableModifier *>(objectRef)->varSetValue(thread, dest))
+		return kMiniscriptInstructionOutcomeFailed;
+	return kMiniscriptInstructionOutcomeContinue;
 }
 
-bool VariableModifier::WriteProxyInterface::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &dest, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const {
+MiniscriptInstructionOutcome VariableModifier::WriteProxyInterface::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &dest, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const {
 	return static_cast<VariableModifier *>(objectRef)->writeRefAttribute(thread, dest, attrib);
 }
 
-bool VariableModifier::WriteProxyInterface::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &dest, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const {
+MiniscriptInstructionOutcome VariableModifier::WriteProxyInterface::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &dest, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const {
 	return static_cast<VariableModifier *>(objectRef)->writeRefAttributeIndexed(thread, dest, attrib, index);
 }
 
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 22015f92336..06693839be1 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -88,6 +88,13 @@ struct ModifierLoaderContext;
 struct PlugInModifierLoaderContext;
 template<typename TElement, typename TElementData> class ElementFactory;
 
+enum MiniscriptInstructionOutcome {
+	kMiniscriptInstructionOutcomeContinue,					// Continue executing next instruction
+	kMiniscriptInstructionOutcomeYieldToVThreadNoRetry,		// Instruction pushed a VThread task and should be retried when the task completes
+	kMiniscriptInstructionOutcomeYieldToVThreadAndRetry,	// Instruction pushed a VThread task and completed
+	kMiniscriptInstructionOutcomeFailed,					// Instruction errored
+};
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 class DebugPrimaryTaskList;
 #endif
@@ -272,7 +279,7 @@ struct Point16 {
 		return result;
 	}
 
-	bool refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib);
+	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib);
 };
 
 struct Rect16 {
@@ -293,6 +300,7 @@ struct Rect16 {
 
 	inline uint16 getWidth() const { return static_cast<uint16>(right - left); }
 	inline uint16 getHeight() const { return static_cast<uint16>(bottom - top); }
+
 	inline static Rect16 create(int16 left, int16 top, int16 right, int16 bottom) {
 		Rect16 result;
 		result.left = left;
@@ -411,10 +419,10 @@ struct AngleMagVector {
 		return result;
 	}
 
-	bool dynSetAngleDegrees(const DynamicValue &value);
-	void dynGetAngleDegrees(DynamicValue &value) const;
+	MiniscriptInstructionOutcome scriptSetAngleDegrees(MiniscriptThread *thread, const DynamicValue &value);
+	void scriptGetAngleDegrees(DynamicValue &value) const;
 
-	bool refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib);
+	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib);
 };
 
 struct ColorRGB8 {
@@ -437,15 +445,15 @@ struct DynamicValue;
 struct DynamicList;
 
 struct IDynamicValueReadInterface {
-	virtual bool read(MiniscriptThread *thread, DynamicValue &dest, const void *objectRef, uintptr_t ptrOrOffset) const = 0;
-	virtual bool readAttrib(MiniscriptThread *thread, DynamicValue &dest, const void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const = 0;
-	virtual bool readAttribIndexed(MiniscriptThread *thread, DynamicValue &dest, const void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const = 0;
+	virtual MiniscriptInstructionOutcome read(MiniscriptThread *thread, DynamicValue &dest, const void *objectRef, uintptr_t ptrOrOffset) const = 0;
+	virtual MiniscriptInstructionOutcome readAttrib(MiniscriptThread *thread, DynamicValue &dest, const void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const = 0;
+	virtual MiniscriptInstructionOutcome readAttribIndexed(MiniscriptThread *thread, DynamicValue &dest, const void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const = 0;
 };
 
 struct IDynamicValueWriteInterface {
-	virtual bool write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const = 0;
-	virtual bool refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const = 0;
-	virtual bool refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const = 0;
+	virtual MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const = 0;
+	virtual MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const = 0;
+	virtual MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const = 0;
 };
 
 struct DynamicValueReadProxyPOD {
@@ -715,9 +723,9 @@ struct DynamicList {
 
 private:
 	struct WriteProxyInterface : public IDynamicValueWriteInterface {
-		bool write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const override;
-		bool refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override;
-		bool refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
+		MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const override;
+		MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override;
+		MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
 
 		static WriteProxyInterface _instance;
 	};
@@ -821,24 +829,24 @@ private:
 
 template<class TFloat>
 struct DynamicValueWriteFloatHelper : public IDynamicValueWriteInterface {
-	bool write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const override {
+	MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const override {
 		TFloat &dest = *static_cast<TFloat *>(objectRef);
 		switch (value.getType()) {
 		case DynamicValueTypes::kFloat:
 			dest = static_cast<TFloat>(value.getFloat());
-			return true;
+			return kMiniscriptInstructionOutcomeContinue;
 		case DynamicValueTypes::kInteger:
 			dest = static_cast<TFloat>(value.getInt());
-			return true;
+			return kMiniscriptInstructionOutcomeContinue;
 		default:
-			return false;
+			return kMiniscriptInstructionOutcomeFailed;
 		}
 	}
-	bool refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override {
-		return false;
+	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override {
+		return kMiniscriptInstructionOutcomeFailed;
 	}
-	bool refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override {
-		return false;
+	MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override {
+		return kMiniscriptInstructionOutcomeFailed;
 	}
 
 	static void create(TFloat *floatValue, DynamicValueWriteProxy &proxy) {
@@ -856,24 +864,24 @@ DynamicValueWriteFloatHelper<TFloat> DynamicValueWriteFloatHelper<TFloat>::_inst
 
 template<class TInteger>
 struct DynamicValueWriteIntegerHelper : public IDynamicValueWriteInterface {
-	bool write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const override {
+	MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const override {
 		TInteger &dest = *static_cast<TInteger *>(objectRef);
 		switch (value.getType()) {
 		case DynamicValueTypes::kFloat:
 			dest = static_cast<TInteger>(floor(value.getFloat() + 0.5));
-			return true;
+			return kMiniscriptInstructionOutcomeContinue;
 		case DynamicValueTypes::kInteger:
 			dest = static_cast<TInteger>(value.getInt());
-			return true;
+			return kMiniscriptInstructionOutcomeContinue;
 		default:
-			return false;
+			return kMiniscriptInstructionOutcomeFailed;
 		}
 	}
-	bool refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override {
-		return false;
+	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override {
+		return kMiniscriptInstructionOutcomeFailed;
 	}
-	bool refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override {
-		return false;
+	MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override {
+		return kMiniscriptInstructionOutcomeFailed;
 	}
 
 	static void create(TInteger *intValue, DynamicValueWriteProxy &proxy) {
@@ -890,9 +898,9 @@ template<class TInteger>
 DynamicValueWriteIntegerHelper<TInteger> DynamicValueWriteIntegerHelper<TInteger>::_instance;
 
 struct DynamicValueWriteStringHelper : public IDynamicValueWriteInterface {
-	bool write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const override;
-	bool refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override;
-	bool refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
+	MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const override;
+	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override;
+	MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
 
 	static void create(Common::String *strValue, DynamicValueWriteProxy &proxy);
 
@@ -900,16 +908,16 @@ private:
 	static DynamicValueWriteStringHelper _instance;
 };
 
-template<class TClass, bool (TClass::*TWriteMethod)(const DynamicValue &dest)>
+template<class TClass, MiniscriptInstructionOutcome (TClass::*TWriteMethod)(MiniscriptThread *thread, const DynamicValue &dest)>
 struct DynamicValueWriteFuncHelper : public IDynamicValueWriteInterface {
-	bool write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const override {
-		return (static_cast<TClass *>(objectRef)->*TWriteMethod)(dest);
+	MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const override {
+		return (static_cast<TClass *>(objectRef)->*TWriteMethod)(thread, dest);
 	}
-	bool refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override {
-		return false;
+	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override {
+		return kMiniscriptInstructionOutcomeFailed;
 	}
-	bool refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override {
-		return false;
+	MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override {
+		return kMiniscriptInstructionOutcomeFailed;
 	}
 
 	static void create(TClass *obj, DynamicValueWriteProxy &proxy) {
@@ -922,7 +930,7 @@ private:
 	static DynamicValueWriteFuncHelper _instance;
 };
 
-template<class TClass, bool (TClass::*TWriteMethod)(const DynamicValue &dest)>
+template<class TClass, MiniscriptInstructionOutcome (TClass::*TWriteMethod)(MiniscriptThread *thread, const DynamicValue &dest)>
 DynamicValueWriteFuncHelper<TClass, TWriteMethod> DynamicValueWriteFuncHelper<TClass, TWriteMethod>::_instance;
 
 struct MessengerSendSpec {
@@ -1445,8 +1453,8 @@ public:
 
 	virtual bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
 	virtual bool readAttributeIndexed(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib, const DynamicValue &index);
-	virtual bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib);
-	virtual bool writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib, const DynamicValue &index);
+	virtual MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib);
+	virtual MiniscriptInstructionOutcome writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib, const DynamicValue &index);
 
 protected:
 	// This is the static GUID stored in the data, it is not guaranteed
@@ -1492,7 +1500,7 @@ public:
 	bool isStructural() const override;
 
 	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
-	bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) override;
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) override;
 
 	const Common::Array<Common::SharedPtr<Structural> > &getChildren() const;
 	void addChild(const Common::SharedPtr<Structural> &child);
@@ -1534,9 +1542,13 @@ protected:
 	virtual ObjectLinkingScope *getPersistentStructuralScope();
 	virtual ObjectLinkingScope *getPersistentModifierScope();
 
+	MiniscriptInstructionOutcome scriptSetPaused(MiniscriptThread *thread, const DynamicValue &value);
+
 	// If you override this, you must override visitInternalReferences too.
 	virtual void linkInternalReferences(ObjectLinkingScope *outerScope);
 
+	virtual void onPauseStateChanged();
+
 	Structural *_parent;
 	Common::Array<Common::SharedPtr<Structural> > _children;
 	Common::Array<Common::SharedPtr<Modifier> > _modifiers;
@@ -1544,6 +1556,11 @@ protected:
 
 	Common::Array<Common::SharedPtr<Asset> > _assets;
 
+	// "paused" attrib is available for ALL structural types, even when it doesn't do anything.
+	// Changing it does not affect modifiers on the object that play media, but does fire
+	// "Paused"/"Unpaused" events.
+	bool _paused;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	Common::SharedPtr<DebugInspector> _debugInspector;
 #endif
@@ -1829,18 +1846,17 @@ public:
 	bool isDirectToScreen() const;
 	uint16 getLayer() const;
 
-	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
-	bool writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
-
-	bool scriptSetVisibility(const DynamicValue &result);
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) override;
 
 	virtual void render(Window *window) = 0;
 
 protected:
 	bool loadCommon(const Common::String &name, uint32 guid, const Data::Rect &rect, uint32 elementFlags, uint16 layer, uint32 streamLocator, uint16 sectionID);
 
-	bool scriptSetDirect(const DynamicValue &dest);
-	bool scriptSetPosition(const DynamicValue &dest);
+	MiniscriptInstructionOutcome scriptSetDirect(MiniscriptThread *thread, const DynamicValue &dest);
+	MiniscriptInstructionOutcome scriptSetPosition(MiniscriptThread *thread, const DynamicValue &dest);
+	MiniscriptInstructionOutcome scriptSetVisibility(MiniscriptThread *thread, const DynamicValue &result);
 
 	void offsetTranslate(int32 xDelta, int32 yDelta);
 
@@ -1945,9 +1961,9 @@ public:
 
 private:
 	struct WriteProxyInterface : public IDynamicValueWriteInterface {
-		bool write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const override;
-		bool refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override;
-		bool refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
+		MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const override;
+		MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override;
+		MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
 
 		static WriteProxyInterface _instance;
 	};


Commit: b04eff6895082d9b317e9e1bd0701223645c9048
    https://github.com/scummvm/scummvm/commit/b04eff6895082d9b317e9e1bd0701223645c9048
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add keyboard messenger modifier stuff and partial Project Started support

Changed paths:
    engines/mtropolis/elements.cpp
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/render.cpp
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 56ec5aaf276..6d2eed097a7 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -156,7 +156,7 @@ void MovieElement::render(Window *window) {
 	if (videoSurface) {
 		Graphics::ManagedSurface *target = window->getSurface().get();
 		Common::Rect srcRect(0, 0, videoSurface->w, videoSurface->h);
-		Common::Rect destRect(_rect.left, _rect.top, _rect.right, _rect.bottom);
+		Common::Rect destRect(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y, _cachedAbsoluteOrigin.x + _rect.getWidth(), _cachedAbsoluteOrigin.y + _rect.getHeight());
 		target->blitFrom(*videoSurface, srcRect, destRect);
 	}
 }
@@ -397,7 +397,7 @@ void ImageElement::deactivate() {
 void ImageElement::render(Window *window) {
 	if (_imageSurface) {
 		Common::Rect srcRect(_imageSurface->w, _imageSurface->h);
-		Common::Rect destRect(_rect.left, _rect.top, _rect.right, _rect.bottom);
+		Common::Rect destRect(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y, _cachedAbsoluteOrigin.x + _rect.getWidth(), _cachedAbsoluteOrigin.y + _rect.getHeight());
 		window->getSurface()->blitFrom(*_imageSurface, srcRect, destRect);
 	}
 }
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 5cb199a60f4..6a185959c95 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -533,6 +533,15 @@ MiniscriptInstructionOutcome Send::execute(MiniscriptThread *thread) const {
 	Common::SharedPtr<RuntimeObject> obj = targetValue.getObject().object.lock();
 
 	if (!obj) {
+		// HACK: Obsidian triggers NAV_Restart on Project Started, which triggers "<init globals> on NAV_Restart"
+		// which sends PRG_Toggle_Status_Display to sharedScene.  Apparently, mTropolis will not trigger an error
+		// on sends to sharedScene at that point even though the destination is invalid.
+		// Maybe invalid sends aren't even an error?  I don't know.
+		if (!thread->getRuntime()->getActiveSharedScene().get()) {
+			thread->popValues(2);
+			return kMiniscriptInstructionOutcomeContinue;
+		}
+
 		thread->error("Invalid message destination (object reference is invalid)");
 		return kMiniscriptInstructionOutcomeFailed;
 	}
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index bfc118e329e..8b907469e65 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -640,6 +640,11 @@ Common::SharedPtr<Modifier> CollisionDetectionMessengerModifier::shallowClone()
 	return Common::SharedPtr<Modifier>(new CollisionDetectionMessengerModifier(*this));
 }
 
+KeyboardMessengerModifier::~KeyboardMessengerModifier() {
+	if (_signaller)
+		_signaller->removeReceiver(this);
+}
+
 bool KeyboardMessengerModifier::load(ModifierLoaderContext &context, const Data::KeyboardMessengerModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -673,7 +678,7 @@ bool KeyboardMessengerModifier::load(ModifierLoaderContext &context, const Data:
 		break;
 	default:
 		_keyCodeType = kMacRomanChar;
-		_macRomanChar = data.keycode;
+		memcpy(&_macRomanChar, &data.keycode, 1);
 		break;
 	}
 
@@ -683,8 +688,162 @@ bool KeyboardMessengerModifier::load(ModifierLoaderContext &context, const Data:
 	return true;
 }
 
+bool KeyboardMessengerModifier::respondsToEvent(const Event &evt) const {
+	if (Event::create(EventIDs::kParentEnabled, 0).respondsTo(evt) || Event::create(EventIDs::kParentDisabled, 0).respondsTo(evt))
+		return true;
+
+	return false;
+}
+
+VThreadState KeyboardMessengerModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+ 	if (Event::create(EventIDs::kParentEnabled, 0).respondsTo(msg->getEvent())) {
+		if (!_signaller)
+			_signaller = runtime->getProject()->notifyOnKeyboardEvent(this);
+	} else if (Event::create(EventIDs::kParentDisabled, 0).respondsTo(msg->getEvent())) {
+		if (_signaller) {
+			_signaller->removeReceiver(this);
+			_signaller.reset();
+		}
+	}
+
+	return kVThreadReturn;
+}
+
+
 Common::SharedPtr<Modifier> KeyboardMessengerModifier::shallowClone() const {
-	return Common::SharedPtr<Modifier>(new KeyboardMessengerModifier(*this));
+	Common::SharedPtr<KeyboardMessengerModifier> cloned(new KeyboardMessengerModifier(*this));
+
+	cloned->_signaller.reset();
+	return cloned;
+}
+
+void KeyboardMessengerModifier::onKeyboardEvent(Runtime *runtime, Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt) {
+	bool responds = false;
+	if (evtType == Common::EVENT_KEYDOWN) {
+		if (repeat)
+			responds = _onRepeat;
+		else
+			responds = _onDown;
+	} else if (evtType == Common::EVENT_KEYUP)
+		responds = _onUp;
+
+	if (!responds)
+		return;
+
+	if (_keyModCommand) {
+		if (runtime->getPlatform() == kProjectPlatformWindows) {
+			// Windows projects check "alt"
+			if ((keyEvt.flags & Common::KBD_ALT) == 0)
+				return;
+		} else if (runtime->getPlatform() == kProjectPlatformMacintosh) {
+			if ((keyEvt.flags & Common::KBD_META) == 0)
+				return;
+		}
+	}
+
+	if (_keyModControl) {
+		if ((keyEvt.flags & Common::KBD_CTRL) == 0)
+			return;
+	}
+
+	if (_keyModOption) {
+		if ((keyEvt.flags & Common::KBD_ALT) == 0)
+			return;
+	}
+
+	Common::String encodedChar;
+
+	Common::String resolvedCharStr;
+
+	KeyCodeType resolvedType = kAny;
+	switch (keyEvt.keycode) {
+	case Common::KEYCODE_HOME:
+		resolvedType = kHome;
+		break;
+	case Common::KEYCODE_KP_ENTER:
+		resolvedType = kEnter;
+		break;
+	case Common::KEYCODE_END:
+		resolvedType = kEnd;
+		break;
+	case Common::KEYCODE_HELP:
+		resolvedType = kHelp;
+		break;
+	case Common::KEYCODE_F1:
+		// Windows projects map F1 to "help"
+		if (runtime->getPlatform() == kProjectPlatformWindows)
+			resolvedType = kHelp;
+		break;
+	case Common::KEYCODE_BACKSPACE:
+		resolvedType = kBackspace;
+		break;
+	case Common::KEYCODE_TAB:
+		resolvedType = kTab;
+		break;
+	case Common::KEYCODE_PAGEUP:
+		resolvedType = kPageUp;
+		break;
+	case Common::KEYCODE_PAGEDOWN:
+		resolvedType = kPageDown;
+		break;
+	case Common::KEYCODE_RETURN:
+		resolvedType = kReturn;
+		break;
+	case Common::KEYCODE_ESCAPE:
+		resolvedType = kEscape;
+		break;
+	case Common::KEYCODE_LEFT:
+		resolvedType = kArrowLeft;
+		break;
+	case Common::KEYCODE_RIGHT:
+		resolvedType = kArrowRight;
+		break;
+	case Common::KEYCODE_UP:
+		resolvedType = kArrowUp;
+		break;
+	case Common::KEYCODE_DOWN:
+		resolvedType = kDelete;
+		break;
+	default: {
+			bool isQuestion = (keyEvt.ascii == '?');
+			uint32 uchar = keyEvt.ascii;
+			Common::U32String u(&uchar, 1);
+			resolvedCharStr = u.encode(Common::kMacRoman);
+
+			// STUPID HACK PLEASE FIX ME: ScummVM has no way of just telling us that the character mapping failed,
+			// we we have to check if it encoded "?"
+			if (resolvedCharStr.size() < 1 || (resolvedCharStr[0] == '?' && !isQuestion))
+				return;
+
+			resolvedType = kMacRomanChar;
+		} break;
+	}
+
+	if (_keyCodeType != kAny && resolvedType != _keyCodeType)
+		return;
+
+	if (_keyCodeType == kMacRomanChar && (resolvedCharStr.size() == 0 || resolvedCharStr[0] != _macRomanChar))
+		return;
+
+	Common::SharedPtr<MessageProperties> msgProps;
+	if (_sendSpec.with.getType() == DynamicValueTypes::kIncomingData) {
+		if (resolvedCharStr.size() != 1)
+			warning("Keyboard messenger is supposed to send the character code, but they key was a special key and we haven't implemented conversion of those keycodes");
+
+		DynamicValue charStr;
+		charStr.setString(resolvedCharStr);
+		_sendSpec.sendFromMessengerWithCustomData(runtime, this, charStr);
+	} else {
+		_sendSpec.sendFromMessenger(runtime, this);
+	}
+}
+
+void KeyboardMessengerModifier::visitInternalReferences(IStructuralReferenceVisitor *visitor) {
+	_sendSpec.visitInternalReferences(visitor);
+}
+
+void KeyboardMessengerModifier::linkInternalReferences(ObjectLinkingScope *scope) {
+	_sendSpec.linkInternalReferences(scope);
 }
 
 TextStyleModifier::StyleFlags::StyleFlags() : bold(false), italic(false), underline(false), outline(false), shadow(false), condensed(false), expanded(false) {
@@ -806,10 +965,16 @@ bool CompoundVariableModifier::readAttributeIndexed(MiniscriptThread *thread, Dy
 
 MiniscriptInstructionOutcome CompoundVariableModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
 	Modifier *var = findChildByName(attrib);
-	if (!var || !var->isVariable())
+	if (!var)
+		return kMiniscriptInstructionOutcomeFailed;
+
+	if (var->isVariable())
+		writeProxy = static_cast<VariableModifier *>(var)->createWriteProxy();
+	else if (var->isModifier())
+		DynamicValueWriteObjectHelper::create(var, writeProxy);
+	else
 		return kMiniscriptInstructionOutcomeFailed;
 
-	writeProxy = static_cast<VariableModifier *>(var)->createWriteProxy();
 	return kMiniscriptInstructionOutcomeContinue;
 }
 
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index a33ad62c714..b29fa6480c5 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -25,6 +25,8 @@
 #include "mtropolis/runtime.h"
 #include "mtropolis/data.h"
 
+#include "common/events.h"
+
 namespace MTropolis {
 
 struct ModifierLoaderContext;
@@ -415,10 +417,15 @@ private:
 	bool _sendToOnlyFirstCollidingElement;
 };
 
-class KeyboardMessengerModifier : public Modifier {
+class KeyboardMessengerModifier : public Modifier, public IKeyboardEventReceiver {
 public:
+	~KeyboardMessengerModifier();
+
 	bool load(ModifierLoaderContext &context, const Data::KeyboardMessengerModifier &data);
 
+	bool respondsToEvent(const Event &evt) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Keyboard Messenger Modifier"; }
 #endif
@@ -426,6 +433,11 @@ public:
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
+	void onKeyboardEvent(Runtime *runtime, Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt) override;
+
+	void visitInternalReferences(IStructuralReferenceVisitor *visitor) override;
+	void linkInternalReferences(ObjectLinkingScope *scope) override;
+
 	Event _send;
 
 	enum KeyCodeType {
@@ -455,9 +467,11 @@ private:
 	bool _keyModCommand : 1;
 	bool _keyModOption : 1;
 	KeyCodeType _keyCodeType;
-	uint8_t _macRomanChar;
+	char _macRomanChar;
 
 	MessengerSendSpec _sendSpec;
+
+	Common::SharedPtr<KeyboardEventSignaller> _signaller;
 };
 
 class TextStyleModifier : public Modifier {
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index 0c5afba9720..25bfd150449 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -254,6 +254,10 @@ void MTropolisEngine::handleEvents() {
 		case Common::EVENT_MOUSEMOVE:
 			_runtime->onMouseMove(evt.mouse.x, evt.mouse.y);
 			break;
+		case Common::EVENT_KEYDOWN:
+		case Common::EVENT_KEYUP:
+			_runtime->onKeyboardEvent(evt.type, evt.kbdRepeat, evt.kbd);
+			break;
 
 		default:
 			break;
@@ -280,7 +284,7 @@ Common::Error MTropolisEngine::run() {
 		_runtime->addVolume(4, "OBSIDIAN4", true);
 		_runtime->addVolume(5, "OBSIDIAN5", true);
 
-		Common::SharedPtr<ProjectDescription> desc(new ProjectDescription());
+		Common::SharedPtr<ProjectDescription> desc(new ProjectDescription(kProjectPlatformWindows));
 		desc->addSegment(0, "Obsidian Data 1.MPL");
 		desc->addSegment(1, "Obsidian Data 2.MPX");
 		desc->addSegment(2, "Obsidian Data 3.MPX");
@@ -327,7 +331,7 @@ Common::Error MTropolisEngine::run() {
 		_runtime->addVolume(4, "OBSIDIAN4", true);
 		_runtime->addVolume(5, "OBSIDIAN5", true);
 
-		Common::SharedPtr<ProjectDescription> desc(new ProjectDescription());
+		Common::SharedPtr<ProjectDescription> desc(new ProjectDescription(kProjectPlatformMacintosh));
 
 		for (int i = 0; i < 6; i++)
 			desc->addSegment(i, resources->getSegmentStream(i));
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 704c2c2f704..27e9aaca58b 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -486,7 +486,6 @@ VThreadState MidiModifier::consumeMessage(Runtime *runtime, const Common::Shared
 				_plugIn->getMidi()->playFile(&_embeddedFile->contents[0], _embeddedFile->contents.size());
 			}
 		}
-
 	}
 
 	return kVThreadReturn;
diff --git a/engines/mtropolis/render.cpp b/engines/mtropolis/render.cpp
index b64f3fd2b71..0d904eb2b25 100644
--- a/engines/mtropolis/render.cpp
+++ b/engines/mtropolis/render.cpp
@@ -39,6 +39,10 @@ struct OrderedDitherGenerator<TNumber, 1> {
 	static void generateOrderedDither(TNumber (&pattern)[1][1]);
 };
 
+struct RenderItem {
+	VisualElement *element;
+};
+
 template<class TNumber, int TResolution>
 void OrderedDitherGenerator<TNumber, TResolution>::generateOrderedDither(TNumber (&pattern)[TResolution][TResolution]) {
 	const int kHalfResolution = TResolution / 2;
@@ -169,63 +173,74 @@ uint32 resolveRGB(uint8 r, uint8 g, uint8 b, const Graphics::PixelFormat &fmt) {
 	return rPlaced | gPlaced | bPlaced | aPlaced;
 }
 
-static void recursiveCollectDrawElements(Structural *structural, Common::Array<VisualElement *> &normalBucket, Common::Array<VisualElement *> &directBucket) {
+static void recursiveCollectDrawElementsAndUpdateOrigins(const Point16 &parentOrigin, Structural *structural, Common::Array<RenderItem> &normalBucket, Common::Array<RenderItem> &directBucket) {
+	Point16 elementOrigin = parentOrigin;
 	if (structural->isElement()) {
 		Element *element = static_cast<Element *>(structural);
 		if (element->isVisual()) {
 			VisualElement *visualElement = static_cast<VisualElement *>(element);
+			const Rect16 &elementRect = visualElement->getRelativeRect();
+
+			elementOrigin.x += elementRect.left;
+			elementOrigin.y += elementRect.top;
+
+			visualElement->setCachedAbsoluteOrigin(Point16::create(elementOrigin.x, elementOrigin.y));
+
+			RenderItem item;
+			item.element = visualElement;
+
 			if (visualElement->isVisible()) {
 				if (visualElement->isDirectToScreen())
-					directBucket.push_back(visualElement);
+					directBucket.push_back(item);
 				else
-					normalBucket.push_back(visualElement);
+					normalBucket.push_back(item);
 			}
 		}
 	}
 
 	for (Common::Array<Common::SharedPtr<Structural> >::const_iterator it = structural->getChildren().begin(), itEnd = structural->getChildren().end(); it != itEnd; ++it) {
-		recursiveCollectDrawElements(it->get(), normalBucket, directBucket);
+		recursiveCollectDrawElementsAndUpdateOrigins(elementOrigin, it->get(), normalBucket, directBucket);
 	}
 }
 
-static bool visualElementLayerLess(VisualElement *a, VisualElement *b) {
-	return a->getLayer() < b->getLayer();
+static bool renderItemLess(const RenderItem &a, const RenderItem &b) {
+	return a.element->getLayer() < b.element->getLayer();
 }
 
-static void renderNormalElement(VisualElement *element, Window *mainWindow) {
-	element->render(mainWindow);
+static void renderNormalElement(const RenderItem &item, Window *mainWindow) {
+	item.element->render(mainWindow);
 }
 
-static void renderDirectElement(VisualElement *element, Window *mainWindow) {
-	renderNormalElement(element, mainWindow);	// Meh
+static void renderDirectElement(const RenderItem &item, Window *mainWindow) {
+	renderNormalElement(item, mainWindow);	// Meh
 }
 
 void renderProject(Runtime *runtime, Window *mainWindow) {
 	Common::Array<Structural *> scenes;
 	runtime->getScenesInRenderOrder(scenes);
 
-	Common::Array<VisualElement *> normalBucket;
-	Common::Array<VisualElement *> directBucket;
+	Common::Array<RenderItem> normalBucket;
+	Common::Array<RenderItem> directBucket;
 
 	for (Common::Array<Structural *>::const_iterator it = scenes.begin(), itEnd = scenes.end(); it != itEnd; ++it) {
 		size_t normalStart = normalBucket.size();
 		size_t directStart = directBucket.size();
 
-		recursiveCollectDrawElements(*it, normalBucket, directBucket);
+		recursiveCollectDrawElementsAndUpdateOrigins(Point16::create(0, 0), *it, normalBucket, directBucket);
 
 		size_t normalEnd = normalBucket.size();
 		size_t directEnd = directBucket.size();
 
 		if (normalEnd - normalStart > 1)
-			Common::sort(normalBucket.begin() + normalStart, normalBucket.end(), visualElementLayerLess);
+			Common::sort(normalBucket.begin() + normalStart, normalBucket.end(), renderItemLess);
 		if (directEnd - directStart > 1)
-			Common::sort(directBucket.begin() + directStart, directBucket.end(), visualElementLayerLess);
+			Common::sort(directBucket.begin() + directStart, directBucket.end(), renderItemLess);
 	}
 
-	for (Common::Array<VisualElement *>::const_iterator it = normalBucket.begin(), itEnd = normalBucket.end(); it != itEnd; ++it)
+	for (Common::Array<RenderItem>::const_iterator it = normalBucket.begin(), itEnd = normalBucket.end(); it != itEnd; ++it)
 		renderNormalElement(*it, mainWindow);
 
-	for (Common::Array<VisualElement *>::const_iterator it = directBucket.begin(), itEnd = directBucket.end(); it != itEnd; ++it)
+	for (Common::Array<RenderItem>::const_iterator it = directBucket.begin(), itEnd = directBucket.end(); it != itEnd; ++it)
 		renderDirectElement(*it, mainWindow);
 }
 
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 325159ffcf1..cc098ade4fd 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -42,6 +42,8 @@
 #include "graphics/maccursor.h"
 #include "graphics/macgui/macfontmanager.h"
 
+#include "audio/mixer.h"
+
 namespace MTropolis {
 
 class ModifierInnerScopeBuilder : public IStructuralReferenceVisitor {
@@ -1297,6 +1299,18 @@ void DynamicValue::setWriteProxy(const DynamicValueWriteProxy &writeProxy) {
 	_list = listRef;
 }
 
+bool DynamicValue::roundToInt(int32 &outInt) const {
+	if (_type == DynamicValueTypes::kInteger) {
+		outInt = _value.asInt;
+		return true;
+	} else if (_type == DynamicValueTypes::kFloat) {
+		outInt = static_cast<int32>(floor(_value.asFloat + 0.5));
+		return true;
+	}
+
+	return false;
+}
+
 void DynamicValue::setObject(const ObjectReference &value) {
 	if (_type != DynamicValueTypes::kObject)
 		clear();
@@ -1460,6 +1474,27 @@ void DynamicValueWriteStringHelper::create(Common::String *strValue, DynamicValu
 
 DynamicValueWriteStringHelper DynamicValueWriteStringHelper::_instance;
 
+MiniscriptInstructionOutcome DynamicValueWriteObjectHelper::write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const {
+	thread->error("Can't write to read-only object value");
+	return kMiniscriptInstructionOutcomeFailed;
+}
+
+MiniscriptInstructionOutcome DynamicValueWriteObjectHelper::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const {
+	return static_cast<RuntimeObject *>(objectRef)->writeRefAttribute(thread, proxy, attrib);
+}
+
+MiniscriptInstructionOutcome DynamicValueWriteObjectHelper::refAttribIndexed(MiniscriptThread* thread, DynamicValueWriteProxy& proxy, void* objectRef, uintptr_t ptrOrOffset, const Common::String& attrib, const DynamicValue& index) const {
+	return static_cast<RuntimeObject *>(objectRef)->writeRefAttributeIndexed(thread, proxy, attrib, index);
+}
+
+void DynamicValueWriteObjectHelper::create(RuntimeObject *obj, DynamicValueWriteProxy &proxy) {
+	proxy.containerList.reset();	// Object references are always anchored while threads are running, so don't need to preserve the container
+	proxy.pod.ifc = &DynamicValueWriteObjectHelper::_instance;
+	proxy.pod.objectRef = obj;
+	proxy.pod.ptrOrOffset = 0;
+}
+
+DynamicValueWriteObjectHelper DynamicValueWriteObjectHelper::_instance;
 
 MessengerSendSpec::MessengerSendSpec() : destination(0), _linkType(kLinkTypeNotYetLinked) {
 }
@@ -1585,6 +1620,8 @@ void MessengerSendSpec::resolveDestination(Runtime *runtime, Modifier *sender, C
 		default:
 			break;
 		}
+	} else if (_linkType == kLinkTypeNotYetLinked) {
+		error("Messenger wasn't linked, programmer probably forgot to call linkInternalReferences on the send spec");
 	}
 }
 
@@ -1605,8 +1642,11 @@ void MessengerSendSpec::resolveVariableObjectType(RuntimeObject *obj, Common::We
 }
 
 void MessengerSendSpec::sendFromMessenger(Runtime *runtime, Modifier *sender) const {
+	sendFromMessengerWithCustomData(runtime, sender, this->with);
+}
 
-	Common::SharedPtr<MessageProperties> props(new MessageProperties(this->send, this->with, sender->getSelfReference()));
+void MessengerSendSpec::sendFromMessengerWithCustomData(Runtime *runtime, Modifier *sender, const DynamicValue &data) const {
+	Common::SharedPtr<MessageProperties> props(new MessageProperties(this->send, data, sender->getSelfReference()));
 
 	Common::WeakPtr<Modifier> modifierDestRef;
 	Common::WeakPtr<Structural> structuralDestRef;
@@ -1765,7 +1805,7 @@ void CursorGraphicCollection::addMacCursor(uint32 cursorID, const Common::Shared
 }
 
 
-ProjectDescription::ProjectDescription() : _language(Common::EN_ANY) {
+ProjectDescription::ProjectDescription(ProjectPlatform platform) : _language(Common::EN_ANY), _platform(platform) {
 }
 
 ProjectDescription::~ProjectDescription() {
@@ -1820,6 +1860,10 @@ const Common::Language &ProjectDescription::getLanguage() const {
 	return _language;
 }
 
+ProjectPlatform ProjectDescription::getPlatform() const {
+	return _platform;
+}
+
 const Common::Array<Common::SharedPtr<Modifier> >& SimpleModifierContainer::getModifiers() const {
 	return _modifiers;
 }
@@ -1912,6 +1956,166 @@ const Common::WeakPtr<RuntimeObject>& MessageProperties::getSource() const {
 	return _source;
 }
 
+SystemInterface::SystemInterface() : _masterVolume(kFullVolume) {
+}
+
+bool SystemInterface::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "mastervolume") {
+		result.setInt(_masterVolume);
+		return true;
+	} else if (attrib == "monitorbitdepth") {
+		int bitDepth = displayModeToBitDepth(thread->getRuntime()->getFakeColorDepth());
+		if (bitDepth <= 0)
+			return false;
+
+		result.setInt(bitDepth);
+		return true;
+	} else if (attrib == "volumeismounted") {
+		int volID = 0;
+		bool isMounted = false;
+		bool hasVolume = thread->getRuntime()->getVolumeState(_volumeName.c_str(), volID, isMounted);
+
+		result.setBool(hasVolume && isMounted);
+		return true;
+	}
+
+	return RuntimeObject::readAttribute(thread, result, attrib);
+}
+
+bool SystemInterface::readAttributeIndexed(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib, const DynamicValue &index) {
+	if (attrib == "supportsbitdepth") {
+		int32 asInteger = 0;
+		if (!index.roundToInt(asInteger))
+			return false;
+
+		bool supported = false;
+		ColorDepthMode mode = bitDepthToDisplayMode(asInteger);
+
+		if (mode != kColorDepthModeInvalid)
+			supported = thread->getRuntime()->isDisplayModeSupported(mode);
+
+		result.setBool(supported);
+		return true;
+	}
+
+	return RuntimeObject::readAttribute(thread, result, attrib);
+}
+
+MiniscriptInstructionOutcome SystemInterface::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
+	if (attrib == "ejectcd") {
+		DynamicValueWriteFuncHelper<SystemInterface, &SystemInterface::setEjectCD>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "gamemode") {
+		DynamicValueWriteFuncHelper<SystemInterface, &SystemInterface::setGameMode>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "mastervolume") {
+		DynamicValueWriteFuncHelper<SystemInterface, &SystemInterface::setMasterVolume>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "monitorbitdepth") {
+		DynamicValueWriteFuncHelper<SystemInterface, &SystemInterface::setMonitorBitDepth>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "volumename") {
+		DynamicValueWriteFuncHelper<SystemInterface, &SystemInterface::setVolumeName>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	return RuntimeObject::writeRefAttribute(thread, result, attrib);
+}
+
+int32 SystemInterface::displayModeToBitDepth(ColorDepthMode displayMode) {
+	switch (displayMode) {
+	case kColorDepthMode1Bit:
+		return 1;
+	case kColorDepthMode2Bit:
+		return 2;
+	case kColorDepthMode4Bit:
+		return 4;
+	case kColorDepthMode8Bit:
+		return 8;
+	case kColorDepthMode16Bit:
+		return 16;
+	case kColorDepthMode32Bit:
+		return 32;
+	default:
+		return 0;
+	}
+}
+
+ColorDepthMode SystemInterface::bitDepthToDisplayMode(int32 bits) {
+	switch (bits) {
+	case 1:
+		return kColorDepthMode1Bit;
+	case 2:
+		return kColorDepthMode2Bit;
+	case 4:
+		return kColorDepthMode4Bit;
+	case 8:
+		return kColorDepthMode8Bit;
+	case 16:
+		return kColorDepthMode16Bit;
+	case 32:
+		return kColorDepthMode32Bit;
+	default:
+		return kColorDepthModeInvalid;
+	}
+}
+
+MiniscriptInstructionOutcome SystemInterface::setEjectCD(MiniscriptThread *thread, const DynamicValue &value) {
+	if (value.getType() != DynamicValueTypes::kBoolean)
+		return kMiniscriptInstructionOutcomeFailed;
+
+	// If set to true, supposed to eject the CD.
+	// Maybe we could dismount one of the runtime volumes here if we really wanted to, but ironically,
+	// while volumeIsMounted supports multiple CD drives at once, ejectCD doesn't, so... do nothing?
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome SystemInterface::setGameMode(MiniscriptThread *thread, const DynamicValue &value) {
+	if (value.getType() != DynamicValueTypes::kBoolean)
+		return kMiniscriptInstructionOutcomeFailed;
+
+	// Nothing to do here
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome SystemInterface::setMasterVolume(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger))
+		return kMiniscriptInstructionOutcomeFailed;
+
+	if (asInteger < 0)
+		asInteger = 0;
+	else if (asInteger > kFullVolume)
+		asInteger = kFullVolume;
+
+	thread->getRuntime()->setVolume(static_cast<double>(asInteger) / kFullVolume);
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome SystemInterface::setMonitorBitDepth(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger))
+		return kMiniscriptInstructionOutcomeFailed;
+
+	const ColorDepthMode depthMode = SystemInterface::bitDepthToDisplayMode(asInteger);
+	if (depthMode != kColorDepthModeInvalid) {
+		thread->getRuntime()->switchDisplayMode(thread->getRuntime()->getRealColorDepth(), depthMode);
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome SystemInterface::setVolumeName(MiniscriptThread *thread, const DynamicValue &value) {
+	if (value.getType() != DynamicValueTypes::kString)
+		return kMiniscriptInstructionOutcomeFailed;
+
+	_volumeName = value.getString();
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 Structural::Structural() : _parent(nullptr), _paused(false) {
 }
 
@@ -1933,10 +2137,22 @@ bool Structural::readAttribute(MiniscriptThread *thread, DynamicValue &result, c
 	if (attrib == "name") {
 		result.setString(_name);
 		return true;
-	}
-	if (attrib == "paused") {
+	} else if (attrib == "paused") {
 		result.setBool(_paused);
 		return true;
+	} else if (attrib == "this") {
+		// Yes, "this" is an attribute
+		result.setObject(thread->getModifier()->getSelfReference());
+		return true;
+	} else if (attrib == "wm" || attrib == "worldmanager") {
+		result.setObject(thread->getRuntime()->getWorldManagerInterface()->getSelfReference());
+		return true;
+	} else if (attrib == "assetmanager") {
+		result.setObject(thread->getRuntime()->getAssetManagerInterface()->getSelfReference());
+		return true;
+	} else if (attrib == "system") {
+		result.setObject(thread->getRuntime()->getSystemInterface()->getSelfReference());
+		return true;
 	}
 
 	return RuntimeObject::readAttribute(thread, result, attrib);
@@ -1946,10 +2162,22 @@ MiniscriptInstructionOutcome Structural::writeRefAttribute(MiniscriptThread *thr
 	if (attrib == "name") {
 		DynamicValueWriteStringHelper::create(&_name, result);
 		return kMiniscriptInstructionOutcomeContinue;
-	}
-	if (attrib == "paused") {
+	} else if (attrib == "paused") {
 		DynamicValueWriteFuncHelper<Structural, &Structural::scriptSetPaused>::create(this, result);
 		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "this") {
+		// Yes, "this" is an attribute
+		DynamicValueWriteObjectHelper::create(thread->getModifier(), result);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "wm" || attrib == "worldmanager") {
+		DynamicValueWriteObjectHelper::create(thread->getRuntime()->getWorldManagerInterface(), result);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "assetmanager") {
+		DynamicValueWriteObjectHelper::create(thread->getRuntime()->getAssetManagerInterface(), result);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "system") {
+		DynamicValueWriteObjectHelper::create(thread->getRuntime()->getSystemInterface(), result);
+		return kMiniscriptInstructionOutcomeContinue;
 	}
 
 	return RuntimeObject::writeRefAttribute(thread, result, attrib);
@@ -2522,7 +2750,7 @@ Runtime::SceneStackEntry::SceneStackEntry() {
 
 Runtime::Runtime(OSystem *system) : _nextRuntimeGUID(1), _realDisplayMode(kColorDepthModeInvalid), _fakeDisplayMode(kColorDepthModeInvalid),
 									_displayWidth(1024), _displayHeight(768), _realTimeBase(0), _playTimeBase(0), _sceneTransitionState(kSceneTransitionStateNotTransitioning),
-									_system(system), _lastFrameCursor(nullptr), _defaultCursor(new DefaultCursor()) {
+									_system(system), _lastFrameCursor(nullptr), _defaultCursor(new DefaultCursor()), _platform(kProjectPlatformUnknown) {
 	_random.reset(new Common::RandomSource("mtropolis"));
 
 	_vthread.reset(new VThread());
@@ -2538,6 +2766,15 @@ Runtime::Runtime(OSystem *system) : _nextRuntimeGUID(1), _realDisplayMode(kColor
 
 	for (int i = 0; i < Actions::kMouseButtonCount; i++)
 		_mouseFocusFlags[Actions::kMouseButtonCount] = false;
+	
+	_worldManagerInterface.reset(new WorldManagerInterface());
+	_worldManagerInterface->setSelfReference(_worldManagerInterface);
+
+	_assetManagerInterface.reset(new AssetManagerInterface());
+	_assetManagerInterface->setSelfReference(_assetManagerInterface);
+
+	_systemInterface.reset(new SystemInterface());
+	_systemInterface->setSelfReference(_systemInterface);
 }
 
 bool Runtime::runFrame() {
@@ -2551,7 +2788,7 @@ bool Runtime::runFrame() {
 
 	for (;;) {
 #ifdef MTROPOLIS_DEBUG_ENABLE
-		if (_debugger->isPaused())
+		if (_debugger && _debugger->isPaused())
 			break;
 #endif
 
@@ -2601,6 +2838,10 @@ bool Runtime::runFrame() {
 				error("Project has no subsections");
 			}
 
+			Common::SharedPtr<MessageProperties> psProps(new MessageProperties(Event::create(EventIDs::kProjectStarted, 0), DynamicValue(), _project->getSelfReference()));
+			Common::SharedPtr<MessageDispatch> psDispatch(new MessageDispatch(psProps, _project.get(), false, true, false));
+			queueMessage(psDispatch);
+
 			_pendingSceneTransitions.push_back(HighLevelSceneTransition(firstSubsection->getChildren()[1], HighLevelSceneTransition::kTypeChangeToScene, false, false));
 			continue;
 		}
@@ -3049,9 +3290,8 @@ void Runtime::executeSharedScenePostSceneChangeActions() {
 }
 
 void Runtime::recursiveDeactivateStructural(Structural *structural) {
-	const Common::Array<Common::SharedPtr<Structural> > &children = structural->getChildren();
-	for (Common::Array<Common::SharedPtr<Structural> >::const_iterator it = children.begin(), itEnd = children.end(); it != itEnd; ++it) {
-		recursiveDeactivateStructural(it->get());
+	for (const Common::SharedPtr<Structural> &child : structural->getChildren()) {
+		recursiveDeactivateStructural(child.get());
 	}
 
 	structural->deactivate();
@@ -3060,9 +3300,8 @@ void Runtime::recursiveDeactivateStructural(Structural *structural) {
 void Runtime::recursiveActivateStructural(Structural *structural) {
 	structural->activate();
 
-	const Common::Array<Common::SharedPtr<Structural> > &children = structural->getChildren();
-	for (Common::Array<Common::SharedPtr<Structural> >::const_iterator it = children.begin(), itEnd = children.end(); it != itEnd; ++it) {
-		recursiveActivateStructural(it->get());
+	for (const Common::SharedPtr<Structural> &child : structural->getChildren()) {
+		recursiveActivateStructural(child.get());
 	}
 }
 
@@ -3180,6 +3419,18 @@ Common::SharedPtr<Window> Runtime::findTopWindow(int32 x, int32 y) const {
 	return bestWindow;
 }
 
+void Runtime::setVolume(double volume) {
+	Audio::Mixer *mixer = _system->getMixer();
+	mixer->setVolumeForSoundType(Audio::Mixer::kMusicSoundType, static_cast<int>(volume * Audio::Mixer::kMaxMixerVolume));
+	mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType, static_cast<int>(volume * Audio::Mixer::kMaxMixerVolume));
+	mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType, static_cast<int>(volume * Audio::Mixer::kMaxMixerVolume));
+	mixer->setVolumeForSoundType(Audio::Mixer::kSpeechSoundType, static_cast<int>(volume * Audio::Mixer::kMaxMixerVolume));
+}
+
+ProjectPlatform Runtime::getPlatform() const {
+	return _platform;
+}
+
 void Runtime::onMouseDown(int32 x, int32 y, Actions::MouseButton mButton) {
 	Common::SharedPtr<Window> focusWindow = _mouseFocusWindow.lock();
 	if (!focusWindow) {
@@ -3229,10 +3480,28 @@ void Runtime::onMouseUp(int32 x, int32 y, Actions::MouseButton mButton) {
 		_mouseFocusWindow.reset();
 }
 
+void Runtime::onKeyboardEvent(const Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt) {
+	if (_project)
+		_project->onKeyboardEvent(this, evtType, repeat, keyEvt);
+}
+
 Common::RandomSource* Runtime::getRandom() const {
 	return _random.get();
 }
 
+WorldManagerInterface *Runtime::getWorldManagerInterface() const {
+	return _worldManagerInterface.get();
+}
+
+AssetManagerInterface *Runtime::getAssetManagerInterface() const {
+	return _assetManagerInterface.get();
+}
+
+SystemInterface *Runtime::getSystemInterface() const {
+	return _systemInterface.get();
+}
+
+
 void Runtime::ensureMainWindowExists() {
 	// Maybe there's a better spot for this
 	if (_mainWindow.expired() && _project) {
@@ -3284,6 +3553,18 @@ void Runtime::addVolume(int volumeID, const char *name, bool isMounted) {
 	_volumes.push_back(volume);
 }
 
+bool Runtime::getVolumeState(const Common::String &name, int &outVolumeID, bool &outIsMounted) const {
+	for (const VolumeState &volume : _volumes) {
+		if (caseInsensitiveEqual(volume.name, name)) {
+			outVolumeID = volume.volumeID;
+			outIsMounted = volume.isMounted;
+			return true;
+		}
+	}
+
+	return false;
+}
+
 void Runtime::addSceneStateTransition(const HighLevelSceneTransition &transition) {
 	_pendingSceneTransitions.push_back(transition);
 }
@@ -3327,6 +3608,10 @@ void Runtime::setupDisplayMode(ColorDepthMode displayMode, const Graphics::Pixel
 	_displayModePixelFormats[displayMode] = pixelFormat;
 }
 
+bool Runtime::isDisplayModeSupported(ColorDepthMode displayMode) const {
+	return _displayModeSupported[displayMode];
+}
+
 bool Runtime::switchDisplayMode(ColorDepthMode realDisplayMode, ColorDepthMode fakeDisplayMode) {
 	_fakeDisplayMode = fakeDisplayMode;
 
@@ -3539,12 +3824,39 @@ void SegmentUnloadSignaller::removeReceiver(ISegmentUnloadSignalReceiver *receiv
 	}
 }
 
+KeyboardEventSignaller::KeyboardEventSignaller() {
+}
+
+KeyboardEventSignaller::~KeyboardEventSignaller() {
+}
+
+void KeyboardEventSignaller::onKeyboardEvent(Runtime *runtime, Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt) {
+	const size_t numReceivers = _receivers.size();
+	for (size_t i = 0; i < numReceivers; i++) {
+		_receivers[i]->onKeyboardEvent(runtime, evtType, repeat, keyEvt);
+	}
+}
+
+void KeyboardEventSignaller::addReceiver(IKeyboardEventReceiver *receiver) {
+	_receivers.push_back(receiver);
+}
+
+void KeyboardEventSignaller::removeReceiver(IKeyboardEventReceiver *receiver) {
+	for (size_t i = 0; i < _receivers.size(); i++) {
+		if (_receivers[i] == receiver) {
+			_receivers.remove_at(i);
+			break;
+		}
+	}
+}
+
 Project::Segment::Segment() : weakStream(nullptr) {
 }
 
 Project::Project(Runtime *runtime)
 	: _runtime(runtime), _projectFormat(Data::kProjectFormatUnknown), _isBigEndian(false),
-	  _haveGlobalObjectInfo(false), _haveProjectStructuralDef(false), _postRenderSignaller(new PostRenderSignaller()) {
+	  _haveGlobalObjectInfo(false), _haveProjectStructuralDef(false), _postRenderSignaller(new PostRenderSignaller()),
+	  _keyboardEventSignaller(new KeyboardEventSignaller()) {
 }
 
 Project::~Project() {
@@ -3833,6 +4145,15 @@ Common::SharedPtr<PostRenderSignaller> Project::notifyOnPostRender(IPostRenderSi
 	return _postRenderSignaller;
 }
 
+void Project::onKeyboardEvent(Runtime *runtime, const Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt) {
+	_keyboardEventSignaller->onKeyboardEvent(runtime, evtType, repeat, keyEvt);
+}
+
+Common::SharedPtr<KeyboardEventSignaller> Project::notifyOnKeyboardEvent(IKeyboardEventReceiver *receiver) {
+	_keyboardEventSignaller->addReceiver(receiver);
+	return _keyboardEventSignaller;
+}
+
 void Project::loadBootStream(size_t streamIndex) {
 	const StreamDesc &streamDesc = _streams[streamIndex];
 
@@ -4343,6 +4664,9 @@ uint32 Element::getStreamLocator() const {
 	return _streamLocator;
 }
 
+VisualElement::VisualElement() : _rect(Rect16::create(0, 0, 0, 0)), _cachedAbsoluteOrigin(Point16::create(0, 0)) {
+}
+
 bool VisualElement::isVisual() const {
 	return true;
 }
@@ -4395,6 +4719,18 @@ MiniscriptInstructionOutcome VisualElement::writeRefAttribute(MiniscriptThread *
 	return Element::writeRefAttribute(thread, writeProxy, attrib);
 }
 
+const Rect16 &VisualElement::getRelativeRect() const {
+	return _rect;
+}
+
+const Point16 &VisualElement::getCachedAbsoluteOrigin() const {
+	return _cachedAbsoluteOrigin;
+}
+
+void VisualElement::setCachedAbsoluteOrigin(const Point16 &absOrigin) {
+	_cachedAbsoluteOrigin = absOrigin;
+}
+
 MiniscriptInstructionOutcome VisualElement::scriptSetVisibility(MiniscriptThread *thread, const DynamicValue &result) {
 	// FIXME: Need to make this fire Show/Hide events!
 	if (result.getType() == DynamicValueTypes::kBoolean) {
@@ -4434,24 +4770,30 @@ MiniscriptInstructionOutcome VisualElement::scriptSetPosition(MiniscriptThread *
 		int32 xDelta = destPoint.x - _rect.left;
 		int32 yDelta = destPoint.y - _rect.right;
 
-		offsetTranslate(xDelta, yDelta);
+		if (xDelta != 0 || yDelta != 0)
+			offsetTranslate(xDelta, yDelta, false);
 
 		return kMiniscriptInstructionOutcomeContinue;
 	}
 	return kMiniscriptInstructionOutcomeFailed;
 }
 
-void VisualElement::offsetTranslate(int32 xDelta, int32 yDelta) {
-	_rect.left += xDelta;
-	_rect.right += xDelta;
-	_rect.top += yDelta;
-	_rect.bottom += yDelta;
+void VisualElement::offsetTranslate(int32 xDelta, int32 yDelta, bool cachedOriginOnly) {
+	if (!cachedOriginOnly) {
+		_rect.left += xDelta;
+		_rect.right += xDelta;
+		_rect.top += yDelta;
+		_rect.bottom += yDelta;
+	}
+
+	_cachedAbsoluteOrigin.x += xDelta;
+	_cachedAbsoluteOrigin.y += yDelta;
 
 	for (const Common::SharedPtr<Structural> &child : _children) {
 		if (child->isElement()) {
 			Element *element = static_cast<Element *>(child.get());
 			if (element->isVisual())
-				static_cast<VisualElement *>(element)->offsetTranslate(xDelta, yDelta);
+				static_cast<VisualElement *>(element)->offsetTranslate(xDelta, yDelta, true);
 		}
 	}
 }
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 06693839be1..5c2b789a690 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -23,6 +23,7 @@
 #define MTROPOLIS_RUNTIME_H
 
 #include "common/array.h"
+#include "common/events.h"
 #include "common/language.h"
 #include "common/platform.h"
 #include "common/ptr.h"
@@ -59,10 +60,9 @@ struct Surface;
 namespace MTropolis {
 
 class Asset;
+class AssetManagerInterface;
 class CursorGraphic;
 class CursorGraphicCollection;
-struct DynamicValueReadProxy;
-struct DynamicValueWriteProxy;
 class Element;
 class MessageDispatch;
 class MiniscriptThread;
@@ -74,9 +74,13 @@ class PlugIn;
 class Project;
 class Runtime;
 class Structural;
+class SystemInterface;
 class VisualElement;
 class Window;
+class WorldManagerInterface;
 struct DynamicValue;
+struct DynamicValueReadProxy;
+struct DynamicValueWriteProxy;
 struct IMessageConsumer;
 struct IModifierContainer;
 struct IModifierFactory;
@@ -787,6 +791,8 @@ struct DynamicValue {
 	void setReadProxy(const DynamicValueReadProxy &readProxy);
 	void setWriteProxy(const DynamicValueWriteProxy &writeProxy);
 
+	bool roundToInt(int32 &outInt) const;
+
 	DynamicValue &operator=(const DynamicValue &other);
 
 	bool operator==(const DynamicValue &other) const;
@@ -933,6 +939,17 @@ private:
 template<class TClass, MiniscriptInstructionOutcome (TClass::*TWriteMethod)(MiniscriptThread *thread, const DynamicValue &dest)>
 DynamicValueWriteFuncHelper<TClass, TWriteMethod> DynamicValueWriteFuncHelper<TClass, TWriteMethod>::_instance;
 
+struct DynamicValueWriteObjectHelper : public IDynamicValueWriteInterface {
+	MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const override;
+	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override;
+	MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
+
+	static void create(RuntimeObject *obj, DynamicValueWriteProxy &proxy);
+
+private:
+	static DynamicValueWriteObjectHelper _instance;
+};
+
 struct MessengerSendSpec {
 	MessengerSendSpec();
 	bool load(const Data::Event &dataEvent, uint32 dataMessageFlags, const Data::InternalTypeTaggedValue &dataLocator, const Common::String &dataWithSource, const Common::String &dataWithString, uint32 dataDestination);
@@ -945,6 +962,7 @@ struct MessengerSendSpec {
 	static void resolveVariableObjectType(RuntimeObject *obj, Common::WeakPtr<Structural> &outStructuralDest, Common::WeakPtr<Modifier> &outModifierDest);
 
 	void sendFromMessenger(Runtime *runtime, Modifier *sender) const;
+	void sendFromMessengerWithCustomData(Runtime *runtime, Modifier *sender, const DynamicValue &data) const;
 
 	Event send;
 	MessageFlags messageFlags;
@@ -1037,9 +1055,16 @@ private:
 	Common::Array<Common::SharedPtr<Graphics::MacCursor> > _macCursors;
 };
 
+enum ProjectPlatform {
+	kProjectPlatformUnknown,
+
+	kProjectPlatformWindows,
+	kProjectPlatformMacintosh,
+};
+
 class ProjectDescription {
 public:
-	ProjectDescription();
+	explicit ProjectDescription(ProjectPlatform platform);
 	~ProjectDescription();
 
 	void addSegment(int volumeID, const char *filePath);
@@ -1057,12 +1082,15 @@ public:
 	void setLanguage(const Common::Language &language);
 	const Common::Language &getLanguage() const;
 
+	ProjectPlatform getPlatform() const;
+
 private:
 	Common::Array<SegmentDescription> _segments;
 	Common::Array<Common::SharedPtr<PlugIn> > _plugIns;
 	Common::SharedPtr<ProjectResources> _resources;
 	Common::SharedPtr<CursorGraphicCollection> _cursorGraphics;
 	Common::Language _language;
+	ProjectPlatform _platform;
 };
 
 struct VolumeState {
@@ -1233,6 +1261,8 @@ public:
 	void queueProject(const Common::SharedPtr<ProjectDescription> &desc);
 
 	void addVolume(int volumeID, const char *name, bool isMounted);
+	bool getVolumeState(const Common::String &name, int &outVolumeID, bool &outIsMounted) const;
+
 	void addSceneStateTransition(const HighLevelSceneTransition &transition);
 
 	Project *getProject() const;
@@ -1248,9 +1278,12 @@ public:
 	// Sets up a supported display mode
 	void setupDisplayMode(ColorDepthMode displayMode, const Graphics::PixelFormat &pixelFormat);
 
+	bool isDisplayModeSupported(ColorDepthMode displayMode) const;
+
 	// Switches to a specified display mode.  Returns true if the mode was actually changed.  If so, all windows will need
 	// to be recreated.
 	bool switchDisplayMode(ColorDepthMode realDisplayMode, ColorDepthMode fakeDisplayMode);
+
 	void setDisplayResolution(uint16 width, uint16 height);
 	void getDisplayResolution(uint16 &outWidth, uint16 &outHeight) const;
 
@@ -1284,11 +1317,19 @@ public:
 
 	Common::SharedPtr<Window> findTopWindow(int32 x, int32 y) const;
 
+	void setVolume(double volume);
+
+	ProjectPlatform getPlatform() const;
+
 	void onMouseDown(int32 x, int32 y, Actions::MouseButton mButton);
 	void onMouseMove(int32 x, int32 y);
 	void onMouseUp(int32 x, int32 y, Actions::MouseButton mButton);
+	void onKeyboardEvent(const Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt);
 
 	Common::RandomSource *getRandom() const;
+	WorldManagerInterface *getWorldManagerInterface() const;
+	AssetManagerInterface *getAssetManagerInterface() const;
+	SystemInterface *getSystemInterface() const;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	void debugSetEnabled(bool enabled);
@@ -1409,6 +1450,12 @@ private:
 	Common::WeakPtr<Window> _mouseFocusWindow;
 	bool _mouseFocusFlags[Actions::kMouseButtonCount];
 
+	ProjectPlatform _platform;
+
+	Common::SharedPtr<SystemInterface> _systemInterface;
+	Common::SharedPtr<WorldManagerInterface> _worldManagerInterface;
+	Common::SharedPtr<AssetManagerInterface> _assetManagerInterface;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	Common::SharedPtr<Debugger> _debugger;
 #endif
@@ -1492,6 +1539,36 @@ struct IMessageConsumer {
 	virtual VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) = 0;
 };
 
+class WorldManagerInterface : public RuntimeObject {
+};
+
+class AssetManagerInterface : public RuntimeObject {
+};
+
+class SystemInterface : public RuntimeObject {
+public:
+	const int kFullVolume = 7;
+
+	SystemInterface();
+
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
+	bool readAttributeIndexed(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib, const DynamicValue &index) override;
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) override;
+
+private:
+	static ColorDepthMode bitDepthToDisplayMode(int32 bitDepth);
+	static int32 displayModeToBitDepth(ColorDepthMode displayMode);
+
+	MiniscriptInstructionOutcome setEjectCD(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome setGameMode(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome setMasterVolume(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome setMonitorBitDepth(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome setVolumeName(MiniscriptThread *thread, const DynamicValue &value);
+
+	Common::String _volumeName;
+	int _masterVolume;
+};
+
 class Structural : public RuntimeObject, public IModifierContainer, public IMessageConsumer, public IDebuggable {
 public:
 	Structural();
@@ -1660,6 +1737,24 @@ private:
 	Common::Array<ISegmentUnloadSignalReceiver *> _receivers;
 };
 
+struct IKeyboardEventReceiver {
+	virtual void onKeyboardEvent(Runtime *runtime, Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt) = 0;
+};
+
+class KeyboardEventSignaller {
+public:
+	KeyboardEventSignaller();
+	~KeyboardEventSignaller();
+
+	void onKeyboardEvent(Runtime *runtime, Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt);
+	void addReceiver(IKeyboardEventReceiver *receiver);
+	void removeReceiver(IKeyboardEventReceiver *receiver);
+
+private:
+	Common::Array<IKeyboardEventReceiver *> _receivers;
+	Common::SharedPtr<KeyboardEventSignaller> _signaller;
+};
+
 class Project : public Structural {
 public:
 	explicit Project(Runtime *runtime);
@@ -1685,6 +1780,9 @@ public:
 	void onPostRender();
 	Common::SharedPtr<PostRenderSignaller> notifyOnPostRender(IPostRenderSignalReceiver *receiver);
 
+	void onKeyboardEvent(Runtime *runtime, const Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt);
+	Common::SharedPtr<KeyboardEventSignaller> notifyOnKeyboardEvent(IKeyboardEventReceiver *receiver);
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Project"; }
 #endif
@@ -1782,6 +1880,7 @@ private:
 	ObjectLinkingScope _modifierScope;
 
 	Common::SharedPtr<PostRenderSignaller> _postRenderSignaller;
+	Common::SharedPtr<KeyboardEventSignaller> _keyboardEventSignaller;
 
 	Runtime *_runtime;
 };
@@ -1840,6 +1939,8 @@ protected:
 
 class VisualElement : public Element {
 public:
+	VisualElement();
+
 	bool isVisual() const override;
 
 	bool isVisible() const;
@@ -1849,6 +1950,13 @@ public:
 	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
 	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) override;
 
+	const Rect16 &getRelativeRect() const;
+
+	// The cached absolute origin is from the last time the element was rendered.
+	// Do not rely on it mid-frame.
+	const Point16 &getCachedAbsoluteOrigin() const;
+	void setCachedAbsoluteOrigin(const Point16 &absOrigin);
+
 	virtual void render(Window *window) = 0;
 
 protected:
@@ -1858,7 +1966,7 @@ protected:
 	MiniscriptInstructionOutcome scriptSetPosition(MiniscriptThread *thread, const DynamicValue &dest);
 	MiniscriptInstructionOutcome scriptSetVisibility(MiniscriptThread *thread, const DynamicValue &result);
 
-	void offsetTranslate(int32 xDelta, int32 yDelta);
+	void offsetTranslate(int32 xDelta, int32 yDelta, bool cachedOriginOnly);
 
 	struct ChangeFlagTaskData {
 		bool desiredFlag;
@@ -1870,6 +1978,7 @@ protected:
 	bool _directToScreen;
 	bool _visible;
 	Rect16 _rect;
+	Point16 _cachedAbsoluteOrigin;
 	uint16 _layer;
 };
 
@@ -1941,6 +2050,8 @@ protected:
 	// If you override this, you must override visitInternalReferences too.
 	virtual void linkInternalReferences(ObjectLinkingScope *scope);
 
+	Structural *findStructuralOwner() const;
+
 	Common::String _name;
 	ModifierFlags _modifierFlags;
 	Common::WeakPtr<RuntimeObject> _parent;


Commit: 7b48322af5ebf23d0760ae38dbe08ce736049347
    https://github.com/scummvm/scummvm/commit/7b48322af5ebf23d0760ae38dbe08ce736049347
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Get transition to game menu queue working (fails though)

Changed paths:
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/miniscript.h
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 6a185959c95..641fddca403 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -1393,6 +1393,77 @@ PushValue::PushValue(DataType dataType, const void *value, bool isLValue)
 	}
 }
 
+MiniscriptInstructionOutcome ListCreate::execute(MiniscriptThread *thread) const {
+	if (thread->getStackSize() < 2) {
+		thread->error("Stack underflow");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0, false);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	outcome = thread->dereferenceRValue(1, false);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	MiniscriptStackValue &rs = thread->getStackValueFromTop(0);
+	MiniscriptStackValue &lsDest = thread->getStackValueFromTop(1);
+
+	Common::SharedPtr<DynamicList> list(new DynamicList());
+	if (!list->setAtIndex(1, rs.value)) {
+		thread->error("Failed to set value 2 of list");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+	if (!list->setAtIndex(0, lsDest.value)) {
+		thread->error("Failed to set value 1 of list");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	lsDest.value.setList(list);
+	thread->popValues(1);
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome ListAppend::execute(MiniscriptThread *thread) const {
+	if (thread->getStackSize() < 2) {
+		thread->error("Stack underflow");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0, false);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	outcome = thread->dereferenceRValue(1, false);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	MiniscriptStackValue &rs = thread->getStackValueFromTop(0);
+	MiniscriptStackValue &lsDest = thread->getStackValueFromTop(1);
+
+	if (lsDest.value.getType() != DynamicValueTypes::kList) {
+		thread->error("Expected list on left side of list_append");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	Common::SharedPtr<DynamicList> listRef = lsDest.value.getList();
+	if (listRef.refCount() != 2) {
+		listRef = listRef->clone();
+		lsDest.value.setList(listRef);
+	}
+
+	if (!listRef->setAtIndex(listRef->getSize(), rs.value)) {
+		thread->error("Failed to expand list");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	thread->popValues(1);
+
+	return kMiniscriptInstructionOutcomeFailed;
+}
+
 MiniscriptInstructionOutcome PushValue::execute(MiniscriptThread *thread) const {
 	DynamicValue value;
 
@@ -1578,7 +1649,7 @@ void MiniscriptThread::error(const Common::String &message) {
 	if (_runtime->debugGetDebugger())
 		_runtime->debugGetDebugger()->notify(kDebugSeverityError, Common::String("Miniscript error: " + message));
 #endif
-	warning("Miniscript error: %s", message.c_str());
+	warning("Miniscript error in (%x '%s'): %s", _modifier->getStaticGUID(), _modifier->getName().c_str(), message.c_str());
 
 	// This should be redundant
 	_failed = true;
diff --git a/engines/mtropolis/miniscript.h b/engines/mtropolis/miniscript.h
index 145723d73cf..c65f6816d05 100644
--- a/engines/mtropolis/miniscript.h
+++ b/engines/mtropolis/miniscript.h
@@ -282,10 +282,14 @@ namespace MiniscriptInstructions {
 		bool _isIndexed;
 	};
 
-	class ListAppend : public UnimplementedInstruction {
+	class ListAppend : public MiniscriptInstruction {
+	private:
+		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
 	};
 
-	class ListCreate : public UnimplementedInstruction {
+	class ListCreate : public MiniscriptInstruction {
+	private:
+		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
 	};
 
 	class PushValue : public MiniscriptInstruction {
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 27e9aaca58b..836c5f6fbbe 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -343,6 +343,8 @@ void ObjectReferenceVariableModifier::resolveRelativePath(RuntimeObject *obj, co
 		if (!foundMatch)
 			return;
 	}
+
+	_object.object = obj->getSelfReference();
 }
 
 void ObjectReferenceVariableModifier::resolveAbsolutePath() {
@@ -400,7 +402,7 @@ bool ObjectReferenceVariableModifier::computeObjectPath(RuntimeObject *obj, Comm
 		pathForThis += modifier->getName();
 	}
 
-	RuntimeObject *parent = getObjectParent(this);
+	RuntimeObject *parent = getObjectParent(obj);
 
 	if (parent) {
 		Common::String pathForParent;
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index f928b6a0261..147ca450c0c 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -116,7 +116,7 @@ private:
 	void resolveRelativePath(RuntimeObject *obj, const Common::String &path, size_t startPos);
 	void resolveAbsolutePath();
 
-	bool computeObjectPath(RuntimeObject *obj, Common::String &outPath);
+	static bool computeObjectPath(RuntimeObject *obj, Common::String &outPath);
 	static RuntimeObject *getObjectParent(RuntimeObject *obj);
 
 	Event _setToSourceParentWhen;
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index cc098ade4fd..4cc245c337a 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -1956,6 +1956,58 @@ const Common::WeakPtr<RuntimeObject>& MessageProperties::getSource() const {
 	return _source;
 }
 
+WorldManagerInterface::WorldManagerInterface() {
+}
+
+bool WorldManagerInterface::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "currentscene") {
+		Common::SharedPtr<RuntimeObject> mainScene = thread->getRuntime()->getActiveMainScene();
+		if (mainScene)
+			result.setObject(mainScene->getSelfReference());
+		else
+			result.clear();
+		return true;
+	}
+
+	return RuntimeObject::readAttribute(thread, result, attrib);
+}
+
+MiniscriptInstructionOutcome WorldManagerInterface::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
+	if (attrib == "currentscene") {
+		DynamicValueWriteFuncHelper<WorldManagerInterface, &WorldManagerInterface::setCurrentScene>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	return RuntimeObject::writeRefAttribute(thread, result, attrib);
+}
+
+MiniscriptInstructionOutcome WorldManagerInterface::setCurrentScene(MiniscriptThread *thread, const DynamicValue &value) {
+	if (value.getType() != DynamicValueTypes::kObject)
+		return kMiniscriptInstructionOutcomeFailed;
+
+	Common::SharedPtr<RuntimeObject> sceneObj = value.getObject().object.lock();
+	if (!sceneObj) {
+		thread->error("Failed to get scene reference");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	if (!sceneObj->isStructural()) {
+		thread->error("Tried to change to a non-structural object as a scene");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	Structural *scene = static_cast<Structural *>(sceneObj.get());
+	Structural *subsection = scene->getParent();
+	if (!subsection->isSubsection()) {
+		thread->error("Tried to change to a non-scene as a scene");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	thread->getRuntime()->addSceneStateTransition(HighLevelSceneTransition(scene->getSelfReference().lock().staticCast<Structural>(), HighLevelSceneTransition::kTypeChangeToScene, false, false));
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 SystemInterface::SystemInterface() : _masterVolume(kFullVolume) {
 }
 
@@ -2153,6 +2205,13 @@ bool Structural::readAttribute(MiniscriptThread *thread, DynamicValue &result, c
 	} else if (attrib == "system") {
 		result.setObject(thread->getRuntime()->getSystemInterface()->getSelfReference());
 		return true;
+	} else if (attrib == "parent") {
+		Structural *parent = getParent();
+		if (parent)
+			result.setObject(parent->getSelfReference());
+		else
+			result.clear();
+		return true;
 	}
 
 	return RuntimeObject::readAttribute(thread, result, attrib);
@@ -2178,6 +2237,15 @@ MiniscriptInstructionOutcome Structural::writeRefAttribute(MiniscriptThread *thr
 	} else if (attrib == "system") {
 		DynamicValueWriteObjectHelper::create(thread->getRuntime()->getSystemInterface(), result);
 		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "parent") {
+		// NOTE: Re-parenting objects is allowed by mTropolis but we don't currently support that.
+		Structural *parent = getParent();
+		if (parent) {
+			DynamicValueWriteObjectHelper::create(getParent(), result);
+			return kMiniscriptInstructionOutcomeContinue;
+		} else {
+			return kMiniscriptInstructionOutcomeFailed;
+		}
 	}
 
 	return RuntimeObject::writeRefAttribute(thread, result, attrib);
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 5c2b789a690..89694082674 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -1540,6 +1540,14 @@ struct IMessageConsumer {
 };
 
 class WorldManagerInterface : public RuntimeObject {
+public:
+	WorldManagerInterface();
+
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) override;
+
+private:
+	MiniscriptInstructionOutcome setCurrentScene(MiniscriptThread *thread, const DynamicValue &value);
 };
 
 class AssetManagerInterface : public RuntimeObject {


Commit: ea7eaa4a158bb8917eb78b4724eb84bf4fe038bc
    https://github.com/scummvm/scummvm/commit/ea7eaa4a158bb8917eb78b4724eb84bf4fe038bc
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add mToon assets and elements and fix some things

Changed paths:
    engines/mtropolis/asset_factory.cpp
    engines/mtropolis/assets.cpp
    engines/mtropolis/assets.h
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/element_factory.cpp
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/miniscript.h
    engines/mtropolis/modifier_factory.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/asset_factory.cpp b/engines/mtropolis/asset_factory.cpp
index 95b3e9c8655..1441be568e9 100644
--- a/engines/mtropolis/asset_factory.cpp
+++ b/engines/mtropolis/asset_factory.cpp
@@ -65,6 +65,8 @@ IAssetFactory *getAssetFactoryForDataObjectType(const Data::DataObjectTypes::Dat
 		return AssetFactory<MovieAsset, Data::MovieAsset>::getInstance();
 	case Data::DataObjectTypes::kImageAsset:
 		return AssetFactory<ImageAsset, Data::ImageAsset>::getInstance();
+	case Data::DataObjectTypes::kMToonAsset:
+		return AssetFactory<MToonAsset, Data::MToonAsset>::getInstance();
 	case Data::DataObjectTypes::kTextAsset:
 		return AssetFactory<TextAsset, Data::TextAsset>::getInstance();
 
diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index 4fbb4e02695..b2180417a5e 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -194,6 +194,62 @@ ImageAsset::ImageFormat ImageAsset::getImageFormat() const {
 	return _imageFormat;
 }
 
+bool MToonAsset::load(AssetLoaderContext &context, const Data::MToonAsset &data) {
+	if (data.haveMacPart)
+		_imageFormat = kImageFormatMac;
+	else if (data.haveWinPart)
+		_imageFormat = kImageFormatWindows;
+	else
+		return false;
+
+	_frameDataPosition = data.frameDataPosition;
+	_sizeOfFrameData = data.sizeOfFrameData;
+
+	if (!_rect.load(data.rect))
+		return false;
+
+	_bitsPerPixel = data.bitsPerPixel;
+	_codecID = data.codecID;
+
+	_frames.resize(data.frames.size());
+	for (size_t i = 0; i < _frames.size(); i++) {
+		if (!_frames[i].load(context, data.frames[i]))
+			return false;
+	}
+
+	_frameRanges.resize(data.frameRangesPart.frameRanges.size());
+	for (size_t i = 0; i < _frameRanges.size(); i++) {
+		if (!_frameRanges[i].load(context, data.frameRangesPart.frameRanges[i]))
+			return false;
+	}
+
+	_codecData = data.codecData;
+}
+
+AssetType MToonAsset::getAssetType() const {
+	return kAssetTypeMToon;
+}
+
+bool MToonAsset::FrameDef::load(AssetLoaderContext &context, const Data::MToonAsset::FrameDef &data) {
+	compressedSize = data.compressedSize;
+	dataOffset = data.dataOffset;
+	decompressedBytesPerRow = data.decompressedBytesPerRow;
+	decompressedSize = data.decompressedSize;
+	isKeyFrame = (data.keyframeFlag != 0);
+	if (!rect.load(data.rect1))
+		return false;
+
+	return true;
+}
+
+bool MToonAsset::FrameRangeDef::load(AssetLoaderContext &context, const Data::MToonAsset::FrameRangeDef &data) {
+	name = data.name;
+	startFrame = data.startFrame;
+	endFrame = data.endFrame;
+
+	return true;
+}
+
 bool TextAsset::load(AssetLoaderContext &context, const Data::TextAsset &data) {
 	_assetID = data.assetID;
 
diff --git a/engines/mtropolis/assets.h b/engines/mtropolis/assets.h
index 0ed30d2aff5..ff8ce24fe5d 100644
--- a/engines/mtropolis/assets.h
+++ b/engines/mtropolis/assets.h
@@ -105,6 +105,8 @@ public:
 	size_t getStreamIndex() const;
 	ImageFormat getImageFormat() const;
 
+	const Common::SharedPtr<Graphics::Surface> &loadContent();
+
 private:
 	Rect16 _rect;
 	ColorDepthMode _colorDepth;
@@ -112,6 +114,54 @@ private:
 	uint32 _size;
 	size_t _streamIndex;
 	ImageFormat _imageFormat;
+
+	Common::SharedPtr<Graphics::Surface> _surface;
+};
+
+
+struct MToonAsset : public Asset {
+public:
+	bool load(AssetLoaderContext &context, const Data::MToonAsset &data);
+	AssetType getAssetType() const override;
+
+	enum ImageFormat {
+		kImageFormatMac,
+		kImageFormatWindows,
+	};
+
+	struct FrameDef {
+		Rect16 rect;
+		uint32 dataOffset;
+		uint32 compressedSize;
+		uint32 decompressedSize;
+		uint16 decompressedBytesPerRow;
+		bool isKeyFrame;
+
+		bool load(AssetLoaderContext &context, const Data::MToonAsset::FrameDef &data);
+	};
+
+	struct FrameRangeDef {
+		uint32 startFrame;
+		uint32 endFrame;
+
+		Common::String name;
+
+		bool load(AssetLoaderContext &context, const Data::MToonAsset::FrameRangeDef &data);
+	};
+
+private:
+	ImageFormat _imageFormat;
+
+	uint32 _frameDataPosition;
+	uint32 _sizeOfFrameData;
+
+	Rect16 _rect;
+	uint16 _bitsPerPixel;
+	uint32 _codecID;
+
+	Common::Array<FrameDef> _frames;
+	Common::Array<FrameRangeDef> _frameRanges;
+	Common::Array<uint8> _codecData;
 };
 
 class TextAsset : public Asset {
diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index 8fd33eb4c2f..359311e1e70 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -853,7 +853,7 @@ DataReadErrorCode MToonElement::load(DataReader &reader) {
 	if (!reader.readU32(structuralFlags) || !reader.readU32(sizeIncludingTag) || !reader.readU32(guid)
 			|| !reader.readU16(lengthOfName) || !reader.readU32(elementFlags) || !reader.readU16(layer)
 			|| !reader.readU32(animationFlags) || !reader.readBytes(unknown4) || !reader.readU16(sectionID)
-			|| !rect1.load(reader) || !rect2.load(reader) || !reader.readU32(unknown5)
+			|| !rect1.load(reader) || !rect2.load(reader) || !reader.readU32(assetID)
 			|| !reader.readU32(rateTimes10000) || !reader.readU32(streamLocator) || !reader.readU32(unknown6)
 			|| !reader.readTerminatedStr(name, lengthOfName))
 		return kDataReadErrorReadFailed;
@@ -1032,6 +1032,20 @@ DataReadErrorCode MiniscriptModifier::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode SaveAndRestoreModifier::load(DataReader &reader) {
+	if (_revision != 0x3e9)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !saveWhen.load(reader) || !restoreWhen.load(reader)
+		|| !saveOrRestoreValue.load(reader) || !reader.readBytes(unknown5) || !reader.readU8(lengthOfFilePath)
+		|| !reader.readU8(lengthOfFileName) || !reader.readU8(lengthOfVariableName) || !reader.readU8(lengthOfVariableString)
+		|| !reader.readNonTerminatedStr(varName, lengthOfVariableName) || !reader.readNonTerminatedStr(varString, lengthOfVariableString)
+		|| !reader.readNonTerminatedStr(filePath, lengthOfFilePath) || !reader.readNonTerminatedStr(fileName, lengthOfFileName))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode MessengerModifier::load(DataReader &reader) {
 	if (_revision != 0x3ea)
 		return kDataReadErrorUnsupportedRevision;
@@ -1629,6 +1643,95 @@ DataReadErrorCode ImageAsset::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode MToonAsset::load(DataReader &reader) {
+	if (_revision != 1)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!reader.readU32(marker) || !reader.readBytes(unknown1) || !reader.readU32(assetID))
+		return kDataReadErrorReadFailed;
+
+	haveMacPart = false;
+	haveWinPart = false;
+
+	if (reader.getProjectFormat() == kProjectFormatMacintosh) {
+		haveMacPart = true;
+
+		if (!reader.readBytes(platform.mac.unknown10))
+			return kDataReadErrorReadFailed;
+	} else if (reader.getProjectFormat() == kProjectFormatWindows) {
+		haveWinPart = true;
+
+		if (!reader.readBytes(platform.win.unknown11))
+			return kDataReadErrorReadFailed;
+	} else
+		return kDataReadErrorUnrecognized;
+
+	if (!reader.readU32(frameDataPosition) || !reader.readU32(sizeOfFrameData) || !reader.readU32(mtoonHeader[0])
+		|| !reader.readU32(mtoonHeader[1]) || !reader.readU16(version) || !reader.readBytes(unknown2)
+		|| !reader.readU32(encodingFlags) || !rect.load(reader) || !reader.readU16(numFrames)
+		|| !reader.readBytes(unknown3) || !reader.readU16(bitsPerPixel) || !reader.readU32(codecID)
+		|| !reader.readBytes(unknown4_1) || !reader.readU32(codecDataSize) || !reader.readBytes(unknown4_2))
+		return kDataReadErrorReadFailed;
+
+	if (mtoonHeader[0] != 0 || mtoonHeader[1] != 0x546f6f6e)
+		return kDataReadErrorUnrecognized;
+
+	if (numFrames > 0) {
+		frames.resize(numFrames);
+		for (size_t i = 0; i < numFrames; i++) {
+			FrameDef &frame = frames[i];
+
+			if (!reader.readBytes(frame.unknown12) || !frame.rect1.load(reader) || !reader.readU32(frame.dataOffset)
+				|| !reader.readBytes(frame.unknown13) || !reader.readU32(frame.compressedSize) || !reader.readU8(frame.unknown14)
+				|| !reader.readU8(frame.keyframeFlag) || !reader.readU8(frame.platformBit) || !reader.readU8(frame.unknown15)
+				|| !frame.rect2.load(reader) || !reader.readU32(frame.hdpiFixed) || !reader.readU32(frame.vdpiFixed)
+				|| !reader.readU16(frame.bitsPerPixel) || !reader.readU32(frame.unknown16) || !reader.readU16(frame.decompressedBytesPerRow))
+				return kDataReadErrorReadFailed;
+
+			if (reader.getProjectFormat() == kProjectFormatMacintosh) {
+				if (!reader.readBytes(frame.platform.mac.unknown17))
+					return kDataReadErrorReadFailed;
+			} else if (reader.getProjectFormat() == kProjectFormatWindows) {
+				if (!reader.readBytes(frame.platform.win.unknown18))
+					return kDataReadErrorReadFailed;
+			} else
+				return kDataReadErrorUnrecognized;
+
+			if (!reader.readU32(frame.decompressedSize))
+				return kDataReadErrorReadFailed;
+		}
+	}
+
+	if (codecDataSize > 0) {
+		codecData.resize(codecDataSize);
+		if (!reader.read(&codecData[0], codecDataSize))
+			return kDataReadErrorReadFailed;
+	}
+
+	if (encodingFlags & kEncodingFlag_HasRanges) {
+		if (!reader.readU32(frameRangesPart.tag) || !reader.readU32(frameRangesPart.sizeIncludingTag) || !reader.readU32(frameRangesPart.numFrameRanges))
+			return kDataReadErrorReadFailed;
+
+		if (frameRangesPart.tag != 1)
+			return kDataReadErrorUnrecognized;
+
+		if (frameRangesPart.numFrameRanges > 0) {
+			frameRangesPart.frameRanges.resize(frameRangesPart.numFrameRanges);
+			for (size_t i = 0; i < frameRangesPart.numFrameRanges; i++) {
+				FrameRangeDef &frameRange = frameRangesPart.frameRanges[i];
+
+				if (!reader.readU32(frameRange.startFrame) || !reader.readU32(frameRange.endFrame) || !reader.readU8(frameRange.lengthOfName) || !reader.readU8(frameRange.unknown14))
+					return kDataReadErrorReadFailed;
+
+				if (!reader.readTerminatedStr(frameRange.name, frameRange.lengthOfName))
+					return kDataReadErrorReadFailed;
+			}
+		}
+	}
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode TextAsset::load(DataReader &reader) {
 	if (_revision != 3)
 		return kDataReadErrorReadFailed;
@@ -1775,6 +1878,9 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 	case DataObjectTypes::kMiniscriptModifier:
 		dataObject = new MiniscriptModifier();
 		break;
+	case DataObjectTypes::kSaveAndRestoreModifier:
+		dataObject = new SaveAndRestoreModifier();
+		break;
 	case DataObjectTypes::kMessengerModifier:
 		dataObject = new MessengerModifier();
 		break;
@@ -1871,7 +1977,7 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 		break;
 
 	case DataObjectTypes::kMToonAsset:
-		//dataObject = new MToonAsset();
+		dataObject = new MToonAsset();
 		break;
 
 	case DataObjectTypes::kTextAsset:
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index a42f1e145c8..f0916d22c9e 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -122,7 +122,7 @@ enum DataObjectType {
 	kCursorModifierV1                    = 0x3ca,	// NYI - Obsolete version
 	kGradientModifier                    = 0x4b0,	// NYI
 	kColorTableModifier                  = 0x4c4,	// NYI
-	kSaveAndRestoreModifier              = 0x4d8,	// NYI
+	kSaveAndRestoreModifier              = 0x4d8,
 
 	kCompoundVariableModifier            = 0x2c7,
 	kBooleanVariableModifier             = 0x321,
@@ -272,7 +272,7 @@ struct Label {
 //
 // InternalTypeTaggedValue is used by internal modifiers for messenger payloads and set modifiers
 // and seems to match Miniscript ops too.
-// InternalTypeTaggedValue is always 44 bytes in size and stores string data elsewhere in the containing structure.
+// InternalTypeTaggedValue is always 46 bytes in size and stores string data elsewhere in the containing structure.
 //
 // PlugInTypeTaggedValue is used by plug-ins and is fully self-contained.
 //
@@ -394,7 +394,7 @@ struct ProjectLabelMap : public DataObject {
 
 		Common::String name;
 
-		uint32_t numChildren;
+		uint32 numChildren;
 		LabelTree *children;
 	};
 
@@ -407,7 +407,7 @@ struct ProjectLabelMap : public DataObject {
 		uint32 unknown2;
 		Common::String name;
 
-		uint32_t numChildren;
+		uint32 numChildren;
 		LabelTree *tree;
 	};
 
@@ -678,7 +678,7 @@ struct MToonElement : public StructuralDef {
 	uint16 sectionID;
 	Rect rect1;
 	Rect rect2;
-	uint32 unknown5;
+	uint32 assetID;
 	uint32 rateTimes10000;
 	uint32 streamLocator;
 	uint32 unknown6;
@@ -814,7 +814,6 @@ struct TypicalModifierHeader {
 };
 
 struct MiniscriptModifier : public DataObject {
-
 	TypicalModifierHeader modHeader;
 	Event enableWhen;
 	uint8 unknown6[11];
@@ -826,6 +825,30 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+
+struct SaveAndRestoreModifier : public DataObject {
+	TypicalModifierHeader modHeader;
+	uint8 unknown1[4];
+	Event saveWhen;
+	Event restoreWhen;
+	InternalTypeTaggedValue saveOrRestoreValue;
+
+	uint8 unknown5[8];
+
+	uint8 lengthOfFilePath;
+	uint8 lengthOfFileName;
+	uint8 lengthOfVariableName;
+	uint8 lengthOfVariableString;
+
+	Common::String varName;
+	Common::String varString;
+	Common::String filePath;
+	Common::String fileName;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 enum MessageFlags {
 	kMessageFlagNoRelay = 0x20000000,
 	kMessageFlagNoCascade = 0x40000000,
@@ -936,15 +959,15 @@ struct DragMotionModifier : public DataObject {
 	Event disableWhen;
 
 	struct WinPart {
-		uint8_t unknown2;
-		uint8_t constrainHorizontal;
-		uint8_t constrainVertical;
-		uint8_t constrainToParent;
+		uint8 unknown2;
+		uint8 constrainHorizontal;
+		uint8 constrainVertical;
+		uint8 constrainToParent;
 	};
 
 	struct MacPart {
-		uint8_t flags;
-		uint8_t unknown3;
+		uint8 flags;
+		uint8 unknown3;
 
 		enum Flags {
 			kConstrainToParent = 0x10,
@@ -963,7 +986,7 @@ struct DragMotionModifier : public DataObject {
 	bool haveMacPart;
 	bool haveWinPart;
 	Rect constraintMargin;
-	uint16_t unknown1;
+	uint16 unknown1;
 
 protected:
 	DataReadErrorCode load(DataReader &reader) override;
@@ -1205,7 +1228,7 @@ struct TextStyleModifier : public DataObject {
 	uint16 unknown3;
 	Event applyWhen;
 	Event removeWhen;
-	uint16_t lengthOfFontFamilyName;
+	uint16 lengthOfFontFamilyName;
 
 	Common::String fontFamilyName;
 
@@ -1497,7 +1520,7 @@ struct ImageAsset : public DataObject {
 
 	uint32 persistFlags;
 	uint32 unknown1;
-	uint8_t unknown2[4];
+	uint8 unknown2[4];
 	uint32 assetID;
 	uint32 unknown3;
 
@@ -1520,6 +1543,112 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct MToonAsset : public DataObject {
+	struct MacPart {
+		uint8 unknown10[88];
+	};
+
+	struct WinPart {
+		uint8 unknown11[54];
+	};
+
+	struct FrameDef {
+		struct MacPart {
+			uint8 unknown17[4];
+		};
+
+		struct WinPart {
+			uint8 unknown18[2];
+		};
+
+		union PlatformUnion {
+			MacPart mac;
+			WinPart win;
+		};
+
+		uint8 unknown12[4];
+		Rect rect1;
+		uint32 dataOffset;
+		uint8 unknown13[2];
+		uint32 compressedSize;
+		uint8 unknown14;
+		uint8 keyframeFlag;
+		uint8 platformBit;
+		uint8 unknown15;
+		Rect rect2;
+		uint32 hdpiFixed;
+		uint32 vdpiFixed;
+		uint16 bitsPerPixel;
+		uint32 unknown16;
+		uint16 decompressedBytesPerRow;
+		uint32 decompressedSize;
+
+		PlatformUnion platform;
+	};
+
+	struct FrameRangeDef {
+		uint32 startFrame;
+		uint32 endFrame;
+		uint8 lengthOfName;
+		uint8 unknown14;
+
+		Common::String name; // Null terminated
+	};
+
+	enum {
+		kEncodingFlag_TemporalCompression = 0x80,
+		kEncodingFlag_HasRanges = 0x20000000,
+		kEncodingFlag_Trimming = 0x08,
+	};
+
+	uint32 marker;
+	uint8 unknown1[8];
+	uint32 assetID;
+
+	bool haveMacPart;
+	bool haveWinPart;
+
+	union PlatformUnion {
+		MacPart mac;
+		WinPart win;
+	} platform;
+
+	uint32 frameDataPosition;
+	uint32 sizeOfFrameData;
+
+	// mToon data
+	uint32 mtoonHeader[2];
+	uint16 version;
+	uint8 unknown2[4];
+	uint32 encodingFlags;
+	Rect rect;
+
+	uint16 numFrames;
+	uint8 unknown3[14];
+	uint16 bitsPerPixel;
+	uint32 codecID;
+	uint8 unknown4_1[8];
+	uint32 codecDataSize;
+	uint8 unknown4_2[4];
+
+	Common::Array<FrameDef> frames;
+
+	Common::Array<uint8> codecData;
+
+	struct FrameRangePart {
+		uint32 tag;
+		uint32 sizeIncludingTag;
+
+		uint32 numFrameRanges;
+		Common::Array<FrameRangeDef> frameRanges;
+	};
+
+	FrameRangePart frameRangesPart;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 struct TextAsset : public DataObject {
 	struct MacFormattingSpan {
 		uint8 unknown9[2];
diff --git a/engines/mtropolis/element_factory.cpp b/engines/mtropolis/element_factory.cpp
index c829cbc965d..c70afeaca9c 100644
--- a/engines/mtropolis/element_factory.cpp
+++ b/engines/mtropolis/element_factory.cpp
@@ -66,6 +66,8 @@ IElementFactory *getElementFactoryForDataObjectType(const Data::DataObjectTypes:
 		return ElementFactory<MovieElement, Data::MovieElement>::getInstance();
 	case Data::DataObjectTypes::kImageElement:
 		return ElementFactory<ImageElement, Data::ImageElement>::getInstance();
+	case Data::DataObjectTypes::kMToonElement:
+		return ElementFactory<MToonElement, Data::MToonElement>::getInstance();
 	case Data::DataObjectTypes::kTextLabelElement:
 		return ElementFactory<TextLabelElement, Data::TextLabelElement>::getInstance();
 	case Data::DataObjectTypes::kSoundElement:
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 6d2eed097a7..71814f2f50f 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -284,7 +284,7 @@ void ImageElement::activate() {
 		pixelFmt = Graphics::PixelFormat::createFormatCLUT8();
 		break;
 	case kColorDepthMode16Bit:
-		bytesPerRow = width * 2;
+		bytesPerRow = (width * 2 + 3) / 4 * 4;
 		pixelFmt = Graphics::createPixelFormat<1555>();
 		break;
 	case kColorDepthMode32Bit:
@@ -402,6 +402,43 @@ void ImageElement::render(Window *window) {
 	}
 }
 
+MToonElement::MToonElement() {
+}
+
+MToonElement::~MToonElement() {
+}
+
+bool MToonElement::load(ElementLoaderContext &context, const Data::MToonElement &data) {
+	if (!loadCommon(data.name, data.guid, data.rect1, data.elementFlags, data.layer, data.streamLocator, data.sectionID))
+		return false;
+
+	_cacheBitmap = ((data.elementFlags & Data::ElementFlags::kCacheBitmap) != 0);
+	_paused = ((data.elementFlags & Data::ElementFlags::kPaused) != 0);
+	_loop = ((data.animationFlags & Data::AnimationFlags::kLoop) != 0);
+	_maintainRate = ((data.elementFlags & Data::AnimationFlags::kPlayEveryFrame) == 0);	// NOTE: Inverted intentionally
+	_assetID = data.assetID;
+	_runtime = context.runtime;
+	_rateTimes10000 = data.rateTimes10000;
+
+	return true;
+}
+
+void MToonElement::activate() {
+}
+
+void MToonElement::deactivate() {
+	_renderSurface.reset();
+}
+
+void MToonElement::render(Window *window) {
+	if (_renderSurface) {
+		Common::Rect srcRect(_renderSurface->w, _renderSurface->h);
+		Common::Rect destRect(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y, _cachedAbsoluteOrigin.x + _rect.getWidth(), _cachedAbsoluteOrigin.y + _rect.getHeight());
+		window->getSurface()->blitFrom(*_renderSurface, srcRect, destRect);
+	}
+}
+
+
 TextLabelElement::TextLabelElement() : _needsRender(false), _isBitmap(false) {
 }
 
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index 9923c7d53bb..dde51a4a7f1 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -128,6 +128,37 @@ private:
 	Runtime *_runtime;
 };
 
+class MToonElement : public VisualElement {
+public:
+	MToonElement();
+	~MToonElement();
+
+	bool load(ElementLoaderContext &context, const Data::MToonElement &data);
+
+	void activate() override;
+	void deactivate() override;
+
+	void render(Window *window) override;
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "mToon Element"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusPartial; }
+#endif
+
+private:
+	bool _cacheBitmap;
+	bool _loop;
+
+	// If set, then carry over residual frame time and display at the desired rate.  If not set, reset residual each frame for smoother animation.
+	bool _maintainRate;
+
+	uint32 _assetID;
+	Runtime *_runtime;
+	uint32 _rateTimes10000;
+
+	Common::SharedPtr<Graphics::Surface> _renderSurface;
+};
+
 class TextLabelElement : public VisualElement {
 public:
 	TextLabelElement();
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 641fddca403..291cd0d6c56 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -56,6 +56,23 @@ void MiniscriptReferences::linkInternalReferences(ObjectLinkingScope *scope) {
 	}
 }
 
+void MiniscriptReferences::visitInternalReferences(IStructuralReferenceVisitor *visitor) {
+	for (LocalRef &ref : _localRefs) {
+		Common::SharedPtr<RuntimeObject> obj = ref.resolution.lock();
+		if (obj) {
+			if (obj->isModifier()) {
+				Common::WeakPtr<Modifier> mod = obj.staticCast<Modifier>();
+				visitor->visitWeakModifierRef(mod);
+				ref.resolution = mod;
+			} else if (obj->isStructural()) {
+				Common::WeakPtr<Structural> struc = obj.staticCast<Structural>();
+				visitor->visitWeakStructuralRef(struc);
+				ref.resolution = struc;
+			}
+		}
+	}
+}
+
 Common::WeakPtr<RuntimeObject> MiniscriptReferences::getRefByIndex(uint index) const {
 	if (index >= _localRefs.size())
 		return Common::WeakPtr<RuntimeObject>();
@@ -1075,16 +1092,18 @@ MiniscriptInstructionOutcome BuiltinFunc::executeNum2Str(MiniscriptThread *threa
 	const DynamicValue &inputDynamicValue = thread->getStackValueFromTop(0).value;
 	switch (inputDynamicValue.getType()) {
 	case DynamicValueTypes::kInteger:
-		result.format("%i", static_cast<int>(inputDynamicValue.getInt()));
+		result = Common::String::format("%i", static_cast<int>(inputDynamicValue.getInt()));
 		break;
 	case DynamicValueTypes::kFloat:
-		result.format("%g", static_cast<double>(inputDynamicValue.getFloat()));
+		result = Common::String::format("%g", static_cast<double>(inputDynamicValue.getFloat()));
 		break;
 	default:
 		thread->error("Invalid input value to num2str");
 		return kMiniscriptInstructionOutcomeFailed;
 	}
 
+	returnValue->setString(result);
+
 	return kMiniscriptInstructionOutcomeContinue;
 }
 
diff --git a/engines/mtropolis/miniscript.h b/engines/mtropolis/miniscript.h
index c65f6816d05..aab8e32eb6d 100644
--- a/engines/mtropolis/miniscript.h
+++ b/engines/mtropolis/miniscript.h
@@ -49,6 +49,7 @@ public:
 	explicit MiniscriptReferences(const Common::Array<LocalRef> &localRefs);
 
 	void linkInternalReferences(ObjectLinkingScope *scope);
+	void visitInternalReferences(IStructuralReferenceVisitor *visitor);
 
 	Common::WeakPtr<RuntimeObject> getRefByIndex(uint index) const;
 
diff --git a/engines/mtropolis/modifier_factory.cpp b/engines/mtropolis/modifier_factory.cpp
index 7f1d0340f96..48934f8107c 100644
--- a/engines/mtropolis/modifier_factory.cpp
+++ b/engines/mtropolis/modifier_factory.cpp
@@ -67,6 +67,8 @@ IModifierFactory *getModifierFactoryForDataObjectType(const Data::DataObjectType
 		return ModifierFactory<BehaviorModifier, Data::BehaviorModifier>::getInstance();
 	case Data::DataObjectTypes::kMiniscriptModifier:
 		return ModifierFactory<MiniscriptModifier, Data::MiniscriptModifier>::getInstance();
+	case Data::DataObjectTypes::kSaveAndRestoreModifier:
+		return ModifierFactory<SaveAndRestoreModifier, Data::SaveAndRestoreModifier>::getInstance();
 	case Data::DataObjectTypes::kAliasModifier:
 		return ModifierFactory<AliasModifier, Data::AliasModifier>::getInstance();
 	case Data::DataObjectTypes::kChangeSceneModifier:
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 8b907469e65..627ffc802cd 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -132,6 +132,10 @@ Common::SharedPtr<Modifier> BehaviorModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new BehaviorModifier(*this));
 }
 
+void BehaviorModifier::linkInternalReferences(ObjectLinkingScope *scope) {
+	Modifier::linkInternalReferences(scope);
+}
+
 void BehaviorModifier::visitInternalReferences(IStructuralReferenceVisitor* visitor) {
 	for (Common::Array<Common::SharedPtr<Modifier> >::iterator it = _children.begin(), itEnd = _children.end(); it != itEnd; ++it) {
 		visitor->visitChildModifierRef(*it);
@@ -176,6 +180,49 @@ void MiniscriptModifier::linkInternalReferences(ObjectLinkingScope* scope) {
 	_references->linkInternalReferences(scope);
 }
 
+void MiniscriptModifier::visitInternalReferences(IStructuralReferenceVisitor *visitor) {
+	_references->visitInternalReferences(visitor);
+}
+
+bool SaveAndRestoreModifier::load(ModifierLoaderContext &context, const Data::SaveAndRestoreModifier &data) {
+	if (!loadTypicalHeader(data.modHeader))
+		return false;
+
+	if (!_saveWhen.load(data.saveWhen) || !_restoreWhen.load(data.restoreWhen))
+		return false;
+
+	if (!_saveOrRestoreValue.load(data.saveOrRestoreValue, data.varName, data.varString))
+		return false;
+
+	_filePath = data.filePath;
+	_fileName = data.fileName;
+
+	return true;
+}
+
+bool SaveAndRestoreModifier::respondsToEvent(const Event &evt) const {
+	if (_saveWhen.respondsTo(evt) || _restoreWhen.respondsTo(evt))
+		return true;
+
+	return false;
+}
+
+VThreadState SaveAndRestoreModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	if (_saveWhen.respondsTo(msg->getEvent())) {
+		error("Saves not implemented yet");
+		return kVThreadReturn;
+	} else if (_restoreWhen.respondsTo(msg->getEvent())) {
+		error("Restores not implemented yet");
+		return kVThreadReturn;
+	}
+
+	return kVThreadError;
+}
+
+Common::SharedPtr<Modifier> SaveAndRestoreModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new SaveAndRestoreModifier(*this));
+}
+
 bool MessengerModifier::load(ModifierLoaderContext &context, const Data::MessengerModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index b29fa6480c5..edec3cc85c0 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -71,6 +71,7 @@ private:
 	VThreadState propagateTask(const PropagateTaskData &taskData);
 
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	void linkInternalReferences(ObjectLinkingScope *scope) override;
 	void visitInternalReferences(IStructuralReferenceVisitor *visitor) override;
 
 	Common::Array<Common::SharedPtr<Modifier> > _children;
@@ -96,6 +97,7 @@ public:
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 	void linkInternalReferences(ObjectLinkingScope *scope) override;
+	void visitInternalReferences(IStructuralReferenceVisitor *visitor) override;
 
 	Event _enableWhen;
 
@@ -103,6 +105,30 @@ private:
 	Common::SharedPtr<MiniscriptReferences> _references;
 };
 
+class SaveAndRestoreModifier : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::SaveAndRestoreModifier &data);
+
+	bool respondsToEvent(const Event &evt) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Save And Restore Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusPartial; }
+#endif
+
+private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
+	Event _saveWhen;
+	Event _restoreWhen;
+
+	DynamicValue _saveOrRestoreValue;
+
+	Common::String _filePath;
+	Common::String _fileName;
+};
+
 class MessengerModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::MessengerModifier &data);
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 4cc245c337a..a6f2cb8b796 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -3391,7 +3391,7 @@ void Runtime::loadScene(const Common::SharedPtr<Structural>& scene) {
 	scene->materializeDescendents(this, subsection->getSceneLoadMaterializeScope());
 	debug(1, "Scene materialized OK");
 	recursiveActivateStructural(scene.get());
-	debug(1, "Scene added to renderer OK");
+	debug(1, "Structural elements activated OK");
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	if (_debugger) {
@@ -4767,6 +4767,9 @@ bool VisualElement::readAttribute(MiniscriptThread *thread, DynamicValue &result
 	} else if (attrib == "height") {
 		result.setInt(_rect.bottom - _rect.top);
 		return true;
+	} else if (attrib == "globalposition") {
+		result.setPoint(getGlobalPosition());
+		return true;
 	}
 
 	return Element::readAttribute(thread, result, attrib);
@@ -4791,6 +4794,21 @@ const Rect16 &VisualElement::getRelativeRect() const {
 	return _rect;
 }
 
+Point16 VisualElement::getGlobalPosition() const {
+	Point16 pos = Point16::create(0, 0);
+	if (_parent && _parent->isElement()) {
+		Element *element = static_cast<Element *>(_parent);
+		if (element->isVisual()) {
+			pos = static_cast<VisualElement *>(element)->getGlobalPosition();
+		}
+	}
+
+	pos.x += _rect.left;
+	pos.y += _rect.top;
+
+	return pos;
+}
+
 const Point16 &VisualElement::getCachedAbsoluteOrigin() const {
 	return _cachedAbsoluteOrigin;
 }
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 89694082674..32780db9673 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -1958,6 +1958,7 @@ public:
 	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
 	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) override;
 
+	Point16 getGlobalPosition() const;
 	const Rect16 &getRelativeRect() const;
 
 	// The cached absolute origin is from the last time the element was rendered.
@@ -2055,7 +2056,7 @@ protected:
 	bool loadTypicalHeader(const Data::TypicalModifierHeader &typicalHeader);
 
 	// Links any references contained in the object, resolving static GUIDs to runtime object references.
-	// If you override this, you must override visitInternalReferences too.
+	// If you override this, you should override visitInternalReferences too
 	virtual void linkInternalReferences(ObjectLinkingScope *scope);
 
 	Structural *findStructuralOwner() const;
@@ -2096,6 +2097,7 @@ enum AssetType {
 	kAssetTypeColorTable,
 	kAssetTypeImage,
 	kAssetTypeText,
+	kAssetTypeMToon,
 };
 
 class Asset {


Commit: 256b2f9ef57839da5cb6f72234b7c6680f346154
    https://github.com/scummvm/scummvm/commit/256b2f9ef57839da5cb6f72234b7c6680f346154
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Improve image optimization

Changed paths:
    engines/mtropolis/assets.cpp
    engines/mtropolis/assets.h
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/render.cpp
    engines/mtropolis/render.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index b2180417a5e..f1316747144 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -125,6 +125,51 @@ size_t MovieAsset::getStreamIndex() const {
 	return _streamIndex;
 }
 
+
+CachedImage::CachedImage() : _colorDepth(kColorDepthModeInvalid), _isOptimized(false) {
+}
+
+void CachedImage::resetSurface(ColorDepthMode colorDepth, const Common::SharedPtr<Graphics::Surface> &surface) {
+	_optimizedSurface.reset();
+	_isOptimized = false;
+
+	_colorDepth = colorDepth;
+	_surface = surface;
+}
+
+const Common::SharedPtr<Graphics::Surface> &CachedImage::optimize(Runtime *runtime) {
+	ColorDepthMode renderDepth = runtime->getRealColorDepth();
+	const Graphics::PixelFormat &renderFmt = runtime->getRenderPixelFormat();
+
+	if (renderDepth != _colorDepth) {
+		size_t w = _surface->w;
+		size_t h = _surface->h;
+
+		if (renderDepth == kColorDepthMode16Bit && _colorDepth == kColorDepthMode32Bit) {
+			_optimizedSurface.reset(new Graphics::Surface());
+			_optimizedSurface->create(w, h, renderFmt);
+			Render::convert32To16(*_optimizedSurface, *_surface);
+		} else if (renderDepth == kColorDepthMode32Bit && _colorDepth == kColorDepthMode16Bit) {
+			_optimizedSurface.reset(new Graphics::Surface());
+			_optimizedSurface->create(w, h, renderFmt);
+			Render::convert16To32(*_optimizedSurface, *_surface);
+		} else {
+			_optimizedSurface = _surface;	// Can't optimize
+		}
+	} else {
+		_surface->convertToInPlace(renderFmt, nullptr);
+		_optimizedSurface = _surface;
+	}
+
+	return _optimizedSurface;
+}
+
+ImageAsset::ImageAsset() {
+}
+
+ImageAsset::~ImageAsset() {
+}
+
 bool ImageAsset::load(AssetLoaderContext &context, const Data::ImageAsset &data) {
 	_assetID = data.assetID;
 	if (!_rect.load(data.rect1))
@@ -194,6 +239,158 @@ ImageAsset::ImageFormat ImageAsset::getImageFormat() const {
 	return _imageFormat;
 }
 
+const Common::SharedPtr<CachedImage> &ImageAsset::loadAndCacheImage(Runtime *runtime) {
+	if (_imageCache)
+		return _imageCache;
+
+	ColorDepthMode renderDepth = runtime->getRealColorDepth();
+
+	size_t streamIndex = getStreamIndex();
+	int segmentIndex = runtime->getProject()->getSegmentForStreamIndex(streamIndex);
+	runtime->getProject()->openSegmentStream(segmentIndex);
+	Common::SeekableReadStream *stream = runtime->getProject()->getStreamForSegment(segmentIndex);
+
+	if (!stream || !stream->seek(getFilePosition())) {
+		warning("Image element failed to load");
+		return _imageCache;
+	}
+
+	size_t bytesPerRow = 0;
+
+	Rect16 imageRect = getRect();
+	int width = imageRect.right - imageRect.left;
+	int height = imageRect.bottom - imageRect.top;
+
+	if (width <= 0 || height < 0) {
+		warning("Image asset has invalid size");
+		return _imageCache;
+	}
+
+	Graphics::PixelFormat pixelFmt;
+	switch (getColorDepth()) {
+	case kColorDepthMode1Bit:
+		bytesPerRow = (width + 7) / 8;
+		pixelFmt = Graphics::PixelFormat::createFormatCLUT8();
+		break;
+	case kColorDepthMode2Bit:
+		bytesPerRow = (width + 3) / 4;
+		pixelFmt = Graphics::PixelFormat::createFormatCLUT8();
+		break;
+	case kColorDepthMode4Bit:
+		bytesPerRow = (width + 1) / 2;
+		pixelFmt = Graphics::PixelFormat::createFormatCLUT8();
+		break;
+	case kColorDepthMode8Bit:
+		bytesPerRow = width;
+		pixelFmt = Graphics::PixelFormat::createFormatCLUT8();
+		break;
+	case kColorDepthMode16Bit:
+		bytesPerRow = (width * 2 + 3) / 4 * 4;
+		pixelFmt = Graphics::createPixelFormat<1555>();
+		break;
+	case kColorDepthMode32Bit:
+		bytesPerRow = width * 4;
+		pixelFmt = Graphics::createPixelFormat<8888>();
+		break;
+	default:
+		warning("Image asset has an unrecognizable pixel format");
+		return _imageCache;
+	}
+
+	Common::Array<uint8> rowBuffer;
+	rowBuffer.resize(bytesPerRow);
+
+	ImageAsset::ImageFormat imageFormat = getImageFormat();
+	bool bottomUp = (imageFormat == ImageAsset::kImageFormatWindows);
+	bool isBigEndian = (imageFormat == ImageAsset::kImageFormatMac);
+
+	Common::SharedPtr<Graphics::Surface> imageSurface;
+	imageSurface.reset(new Graphics::Surface());
+	imageSurface->create(width, height, pixelFmt);
+
+	for (int inRow = 0; inRow < height; inRow++) {
+		int outRow = bottomUp ? (height - 1 - inRow) : inRow;
+
+		stream->read(&rowBuffer[0], bytesPerRow);
+		const uint8 *inRowBytes = &rowBuffer[0];
+
+		void *outBase = imageSurface->getBasePtr(0, outRow);
+
+		switch (getColorDepth()) {
+		case kColorDepthMode1Bit: {
+				for (int x = 0; x < width; x++) {
+					int bit = (inRowBytes[x / 8] >> (7 - (x % 8))) & 1;
+					static_cast<uint8 *>(outBase)[x] = bit;
+				}
+			} break;
+		case kColorDepthMode2Bit: {
+				for (int x = 0; x < width; x++) {
+					int bit = (inRowBytes[x / 4] >> (3 - (x % 4))) & 3;
+					static_cast<uint8 *>(outBase)[x] = bit;
+				}
+			} break;
+		case kColorDepthMode4Bit: {
+				for (int x = 0; x < width; x++) {
+					int bit = (inRowBytes[x / 2] >> (1 - (x % 2))) & 15;
+					static_cast<uint8 *>(outBase)[x] = bit;
+				}
+			} break;
+		case kColorDepthMode8Bit:
+			memcpy(outBase, inRowBytes, width);
+			break;
+		case kColorDepthMode16Bit: {
+				if (isBigEndian) {
+					for (int x = 0; x < width; x++) {
+						uint16 packedPixel = inRowBytes[x * 2 + 1] + (inRowBytes[x * 2 + 0] << 8);
+						int r = ((packedPixel >> 10) & 0x1f);
+						int g = ((packedPixel >> 5) & 0x1f);
+						int b = (packedPixel & 0x1f);
+
+						uint16 repacked = (1 << pixelFmt.aShift) | (r << pixelFmt.rShift) | (g << pixelFmt.gShift) | (b << pixelFmt.bShift);
+						static_cast<uint16 *>(outBase)[x] = repacked;
+					}
+				} else {
+					for (int x = 0; x < width; x++) {
+						uint16 packedPixel = inRowBytes[x * 2 + 0] + (inRowBytes[x * 2 + 1] << 8);
+						int r = ((packedPixel >> 10) & 0x1f);
+						int g = ((packedPixel >> 5) & 0x1f);
+						int b = (packedPixel & 0x1f);
+
+						uint16 repacked = (1 << pixelFmt.aShift) | (r << pixelFmt.rShift) | (g << pixelFmt.gShift) | (b << pixelFmt.bShift);
+						static_cast<uint16 *>(outBase)[x] = repacked;
+					}
+				}
+			} break;
+		case kColorDepthMode32Bit: {
+				if (imageFormat == ImageAsset::kImageFormatMac) {
+					for (int x = 0; x < width; x++) {
+						uint8 r = inRowBytes[x * 4 + 0];
+						uint8 g = inRowBytes[x * 4 + 1];
+						uint8 b = inRowBytes[x * 4 + 2];
+						uint32 repacked = (255 << pixelFmt.aShift) | (r << pixelFmt.rShift) | (g << pixelFmt.gShift) | (b << pixelFmt.bShift);
+						static_cast<uint32 *>(outBase)[x] = repacked;
+					}
+				} else if (imageFormat == ImageAsset::kImageFormatWindows) {
+					for (int x = 0; x < width; x++) {
+						uint8 r = inRowBytes[x * 4 + 2];
+						uint8 g = inRowBytes[x * 4 + 1];
+						uint8 b = inRowBytes[x * 4 + 0];
+						uint32 repacked = (255 << pixelFmt.aShift) | (r << pixelFmt.rShift) | (g << pixelFmt.gShift) | (b << pixelFmt.bShift);
+						static_cast<uint32 *>(outBase)[x] = repacked;
+					}
+				}
+			} break;
+		default:
+			break;
+		}
+	}
+
+	_imageCache.reset(new CachedImage());
+	_imageCache->resetSurface(renderDepth, imageSurface);
+
+	return _imageCache;
+}
+
 bool MToonAsset::load(AssetLoaderContext &context, const Data::MToonAsset &data) {
 	if (data.haveMacPart)
 		_imageFormat = kImageFormatMac;
@@ -224,6 +421,8 @@ bool MToonAsset::load(AssetLoaderContext &context, const Data::MToonAsset &data)
 	}
 
 	_codecData = data.codecData;
+
+	return true;
 }
 
 AssetType MToonAsset::getAssetType() const {
diff --git a/engines/mtropolis/assets.h b/engines/mtropolis/assets.h
index ff8ce24fe5d..90b600bd59f 100644
--- a/engines/mtropolis/assets.h
+++ b/engines/mtropolis/assets.h
@@ -88,8 +88,27 @@ private:
 	size_t _streamIndex;
 };
 
+class CachedImage {
+public:
+	CachedImage();
+
+	const Common::SharedPtr<Graphics::Surface> &optimize(Runtime *runtime);
+
+	void resetSurface(ColorDepthMode colorDepth, const Common::SharedPtr<Graphics::Surface> &surface);
+
+private:
+	Common::SharedPtr<Graphics::Surface> _surface;
+	Common::SharedPtr<Graphics::Surface> _optimizedSurface;
+
+	ColorDepthMode _colorDepth;
+	bool _isOptimized;
+};
+
 class ImageAsset : public Asset {
 public:
+	ImageAsset();
+	~ImageAsset();
+
 	bool load(AssetLoaderContext &context, const Data::ImageAsset &data);
 	AssetType getAssetType() const override;
 
@@ -105,7 +124,7 @@ public:
 	size_t getStreamIndex() const;
 	ImageFormat getImageFormat() const;
 
-	const Common::SharedPtr<Graphics::Surface> &loadContent();
+	const Common::SharedPtr<CachedImage> &loadAndCacheImage(Runtime *runtime);
 
 private:
 	Rect16 _rect;
@@ -115,7 +134,7 @@ private:
 	size_t _streamIndex;
 	ImageFormat _imageFormat;
 
-	Common::SharedPtr<Graphics::Surface> _surface;
+	Common::SharedPtr<CachedImage> _imageCache;
 };
 
 
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 71814f2f50f..8d8571682d7 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -203,7 +203,6 @@ VThreadState MovieElement::startPlayingTask(const StartPlayingTaskData &taskData
 	return kVThreadReturn;
 }
 
-
 ImageElement::ImageElement() : _cacheBitmap(false), _runtime(nullptr) {
 }
 
@@ -243,162 +242,19 @@ void ImageElement::activate() {
 		return;
 	}
 
-	ImageAsset *imageAsset = static_cast<ImageAsset *>(asset.get());
-	size_t streamIndex = imageAsset->getStreamIndex();
-	int segmentIndex = project->getSegmentForStreamIndex(streamIndex);
-	project->openSegmentStream(segmentIndex);
-	Common::SeekableReadStream *stream = project->getStreamForSegment(segmentIndex);
-
-	if (!stream->seek(imageAsset->getFilePosition())) {
-		warning("Image element failed to load");
-		return;
-	}
-
-	size_t bytesPerRow = 0;
-
-	Rect16 imageRect = imageAsset->getRect();
-	int width = imageRect.right - imageRect.left;
-	int height = imageRect.bottom - imageRect.top;
-
-	if (width <= 0 || height < 0) {
-		warning("Image asset has invalid size");
-		return;
-	}
-
-	Graphics::PixelFormat pixelFmt;
-	switch (imageAsset->getColorDepth()) {
-	case kColorDepthMode1Bit:
-		bytesPerRow = (width + 7) / 8;
-		pixelFmt = Graphics::PixelFormat::createFormatCLUT8();
-		break;
-	case kColorDepthMode2Bit:
-		bytesPerRow = (width + 3) / 4;
-		pixelFmt = Graphics::PixelFormat::createFormatCLUT8();
-		break;
-	case kColorDepthMode4Bit:
-		bytesPerRow = (width + 1) / 2;
-		pixelFmt = Graphics::PixelFormat::createFormatCLUT8();
-		break;
-	case kColorDepthMode8Bit:
-		bytesPerRow = width;
-		pixelFmt = Graphics::PixelFormat::createFormatCLUT8();
-		break;
-	case kColorDepthMode16Bit:
-		bytesPerRow = (width * 2 + 3) / 4 * 4;
-		pixelFmt = Graphics::createPixelFormat<1555>();
-		break;
-	case kColorDepthMode32Bit:
-		bytesPerRow = width * 4;
-		pixelFmt = Graphics::createPixelFormat<8888>();
-		break;
-	default:
-		warning("Image asset has an unrecognizable pixel format");
-		return;
-	}
-
-	// If this is the same mode as the render target, then copy the exact mode
-	// so blits go faster
-	if (imageAsset->getColorDepth() == _runtime->getRealColorDepth()) {
-		pixelFmt = _runtime->getRenderPixelFormat();
-	}
-
-	Common::Array<uint8> rowBuffer;
-	rowBuffer.resize(bytesPerRow);
-
-	ImageAsset::ImageFormat imageFormat = imageAsset->getImageFormat();
-	bool bottomUp = (imageFormat == ImageAsset::kImageFormatWindows);
-	bool isBigEndian = (imageFormat == ImageAsset::kImageFormatMac);
-
-	_imageSurface.reset(new Graphics::Surface());
-	_imageSurface->create(width, height, pixelFmt);
-
-	for (int inRow = 0; inRow < height; inRow++) {
-		int outRow = bottomUp ? (height - 1 - inRow) : inRow;
-
-		stream->read(&rowBuffer[0], bytesPerRow);
-		const uint8 *inRowBytes = &rowBuffer[0];
-
-		void *outBase = _imageSurface->getBasePtr(0, outRow);
-
-		switch (imageAsset->getColorDepth()) {
-		case kColorDepthMode1Bit: {
-				for (int x = 0; x < width; x++) {
-					int bit = (inRowBytes[x / 8] >> (7 - (x % 8))) & 1;
-					static_cast<uint8 *>(outBase)[x] = bit;
-				}
-			} break;
-		case kColorDepthMode2Bit: {
-				for (int x = 0; x < width; x++) {
-					int bit = (inRowBytes[x / 4] >> (3 - (x % 4))) & 3;
-					static_cast<uint8 *>(outBase)[x] = bit;
-				}
-			} break;
-		case kColorDepthMode4Bit: {
-				for (int x = 0; x < width; x++) {
-					int bit = (inRowBytes[x / 2] >> (1 - (x % 2))) & 15;
-					static_cast<uint8 *>(outBase)[x] = bit;
-				}
-			} break;
-		case kColorDepthMode8Bit:
-			memcpy(outBase, inRowBytes, width);
-			break;
-		case kColorDepthMode16Bit: {
-				if (isBigEndian) {
-					for (int x = 0; x < width; x++) {
-						uint16 packedPixel = inRowBytes[x * 2 + 1] + (inRowBytes[x * 2 + 0] << 8);
-						int r = ((packedPixel >> 10) & 0x1f);
-						int g = ((packedPixel >> 5) & 0x1f);
-						int b = (packedPixel & 0x1f);
-
-						uint16 repacked = (1 << pixelFmt.aShift) | (r << pixelFmt.rShift) | (g << pixelFmt.gShift) | (b << pixelFmt.bShift);
-						static_cast<uint16 *>(outBase)[x] = repacked;
-					}
-				} else {
-					for (int x = 0; x < width; x++) {
-						uint16 packedPixel = inRowBytes[x * 2 + 0] + (inRowBytes[x * 2 + 1] << 8);
-						int r = ((packedPixel >> 10) & 0x1f);
-						int g = ((packedPixel >> 5) & 0x1f);
-						int b = (packedPixel & 0x1f);
-
-						uint16 repacked = (1 << pixelFmt.aShift) | (r << pixelFmt.rShift) | (g << pixelFmt.gShift) | (b << pixelFmt.bShift);
-						static_cast<uint16 *>(outBase)[x] = repacked;
-					}
-				}
-			} break;
-		case kColorDepthMode32Bit: {
-				if (imageFormat == ImageAsset::kImageFormatMac) {
-					for (int x = 0; x < width; x++) {
-						uint8 r = inRowBytes[x * 4 + 0];
-						uint8 g = inRowBytes[x * 4 + 1];
-						uint8 b = inRowBytes[x * 4 + 2];
-						uint32 repacked = (255 << pixelFmt.aShift) | (r << pixelFmt.rShift) | (g << pixelFmt.gShift) | (b << pixelFmt.bShift);
-						static_cast<uint32 *>(outBase)[x] = repacked;
-					}
-				} else if (imageFormat == ImageAsset::kImageFormatWindows) {
-					for (int x = 0; x < width; x++) {
-						uint8 r = inRowBytes[x * 4 + 2];
-						uint8 g = inRowBytes[x * 4 + 1];
-						uint8 b = inRowBytes[x * 4 + 0];
-						uint32 repacked = (255 << pixelFmt.aShift) | (r << pixelFmt.rShift) | (g << pixelFmt.gShift) | (b << pixelFmt.bShift);
-						static_cast<uint32 *>(outBase)[x] = repacked;
-					}
-				}
-			} break;
-		default:
-			break;
-		}
-	}
+	_cachedImage = static_cast<ImageAsset *>(asset.get())->loadAndCacheImage(_runtime);
 }
 
 void ImageElement::deactivate() {
-	_imageSurface.reset();
+	_cachedImage.reset();
 }
 
 void ImageElement::render(Window *window) {
-	if (_imageSurface) {
-		Common::Rect srcRect(_imageSurface->w, _imageSurface->h);
+	if (_cachedImage) {
+		Common::SharedPtr<Graphics::Surface> optimized = _cachedImage->optimize(_runtime);
+		Common::Rect srcRect(optimized->w, optimized->h);
 		Common::Rect destRect(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y, _cachedAbsoluteOrigin.x + _rect.getWidth(), _cachedAbsoluteOrigin.y + _rect.getHeight());
-		window->getSurface()->blitFrom(*_imageSurface, srcRect, destRect);
+		window->getSurface()->blitFrom(*optimized, srcRect, destRect);
 	}
 }
 
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index dde51a4a7f1..ed7f77747f4 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -34,6 +34,7 @@ class VideoDecoder;
 
 namespace MTropolis {
 
+class CachedImage;
 struct ElementLoaderContext;
 
 class GraphicElement : public VisualElement {
@@ -123,7 +124,7 @@ private:
 	bool _cacheBitmap;
 	uint32 _assetID;
 
-	Common::SharedPtr<Graphics::Surface> _imageSurface;
+	Common::SharedPtr<CachedImage> _cachedImage;
 
 	Runtime *_runtime;
 };
diff --git a/engines/mtropolis/render.cpp b/engines/mtropolis/render.cpp
index 0d904eb2b25..9ff270a88b1 100644
--- a/engines/mtropolis/render.cpp
+++ b/engines/mtropolis/render.cpp
@@ -46,9 +46,9 @@ struct RenderItem {
 template<class TNumber, int TResolution>
 void OrderedDitherGenerator<TNumber, TResolution>::generateOrderedDither(TNumber (&pattern)[TResolution][TResolution]) {
 	const int kHalfResolution = TResolution / 2;
-	byte halfRes[kHalfResolution][kHalfResolution];
+	TNumber halfRes[kHalfResolution][kHalfResolution];
 
-	OrderedDitherGenerator<kHalfResolution>::generateOrderedDither(halfRes);
+	OrderedDitherGenerator<TNumber, kHalfResolution>::generateOrderedDither(halfRes);
 
 	const int kHalfResNumSteps = kHalfResolution * kHalfResolution;
 	for (int y = 0; y < kHalfResolution; y++) {
@@ -66,10 +66,18 @@ void OrderedDitherGenerator<TNumber, 1>::generateOrderedDither(TNumber (&pattern
 	pattern[0][0] = 0;
 }
 
-inline int quantize8To5(int value, byte orderedDither16x16) {
+inline int quantize8To5Byte(int value, byte orderedDither16x16) {
 	return (value * 249 + (orderedDither16x16 << 3)) >> 11;
 }
 
+inline int quantize8To5UShort(int value, uint16 orderedDither16x16) {
+	return (value * 249 + orderedDither16x16) >> 11;
+}
+
+inline int expand5To8(int value) {
+	return (value * 33) >> 2;
+}
+
 MacFontFormatting::MacFontFormatting() : fontID(0), fontFlags(0), size(12) {
 }
 
@@ -244,6 +252,77 @@ void renderProject(Runtime *runtime, Window *mainWindow) {
 		renderDirectElement(*it, mainWindow);
 }
 
+void convert32To16(Graphics::Surface &destSurface, const Graphics::Surface &srcSurface) {
+	const Graphics::PixelFormat srcFmt = srcSurface.format;
+	const Graphics::PixelFormat destFmt = destSurface.format;
+
+	assert(srcFmt.bytesPerPixel == 4);
+	assert(destFmt.bytesPerPixel == 2);
+	assert(destSurface.w == srcSurface.w);
+	assert(srcSurface.h == destSurface.h);
+
+	uint16 ditherPattern[16][16];
+	OrderedDitherGenerator<uint16, 16>::generateOrderedDither(ditherPattern);
+
+	for (int x = 0; x < 16; x++) {
+		for (int y = 0; y < 16; y++)
+			ditherPattern[y][x] <<= 3;
+	}
+
+
+	size_t w = srcSurface.w;
+	size_t h = srcSurface.h;
+
+	for (size_t y = 0; y < h; y++) {
+		const uint16 *ditherRow = ditherPattern[y % 16];
+		const uint32 *srcRow = static_cast<const uint32*>(srcSurface.getBasePtr(0, y));
+		uint16 *destRow = static_cast<uint16 *>(destSurface.getBasePtr(0, y));
+
+		for (size_t x = 0; x < w; x++) {
+			uint16 ditherOffset = ditherRow[x % 16];
+			uint32 packed32 = srcRow[x];
+			uint8 r = (packed32 >> srcFmt.rShift) & 0xff;
+			uint8 g = (packed32 >> srcFmt.gShift) & 0xff;
+			uint8 b = (packed32 >> srcFmt.bShift) & 0xff;
+
+			r = quantize8To5UShort(r, ditherOffset);
+			g = quantize8To5UShort(g, ditherOffset);
+			b = quantize8To5UShort(b, ditherOffset);
+			destRow[x] = (r << destFmt.rShift) | (g << destFmt.gShift) | (b << destFmt.bShift);
+		}
+	}
+}
+
+void convert16To32(Graphics::Surface &destSurface, const Graphics::Surface &srcSurface) {
+	const Graphics::PixelFormat srcFmt = srcSurface.format;
+	const Graphics::PixelFormat destFmt = destSurface.format;
+
+	assert(srcFmt.bytesPerPixel == 2);
+	assert(destFmt.bytesPerPixel == 4);
+	assert(destSurface.w == srcSurface.w);
+	assert(srcSurface.h == destSurface.h);
+
+	size_t w = srcSurface.w;
+	size_t h = srcSurface.h;
+
+	for (size_t y = 0; y < h; y++) {
+		const uint32 *srcRow = static_cast<const uint32 *>(srcSurface.getBasePtr(0, y));
+		uint16 *destRow = static_cast<uint16 *>(destSurface.getBasePtr(0, y));
+
+		for (size_t x = 0; x < w; x++) {
+			uint32 packed16 = srcRow[x];
+			uint8 r = (packed16 >> srcFmt.rShift) & 0x1f;
+			uint8 g = (packed16 >> srcFmt.gShift) & 0x1f;
+			uint8 b = (packed16 >> srcFmt.bShift) & 0x1f;
+
+			r = expand5To8(r);
+			g = expand5To8(g);
+			b = expand5To8(b);
+			destRow[x] = (r << destFmt.rShift) | (g << destFmt.gShift) | (b << destFmt.bShift);
+		}
+	}
+}
+
 } // End of namespace Render
 
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/render.h b/engines/mtropolis/render.h
index 3e6284e2b90..190b5f9b3db 100644
--- a/engines/mtropolis/render.h
+++ b/engines/mtropolis/render.h
@@ -30,6 +30,7 @@
 namespace Graphics {
 
 class ManagedSurface;
+struct Surface;
 
 } // End of namespace Graphics
 
@@ -111,6 +112,8 @@ namespace Render {
 
 uint32 resolveRGB(uint8 r, uint8 g, uint8 b, const Graphics::PixelFormat &fmt);
 void renderProject(Runtime *runtime, Window *mainWindow);
+void convert32To16(Graphics::Surface &destSurface, const Graphics::Surface &srcSurface);
+void convert16To32(Graphics::Surface &destSurface, const Graphics::Surface &srcSurface);
 
 } // End of namespace Render
 
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index a6f2cb8b796..7e6f4b5fc3b 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -5044,6 +5044,15 @@ bool VariableModifier::isVariable() const {
 	return true;
 }
 
+bool VariableModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "value") {
+		varGetValue(thread, result);
+		return true;
+	}
+
+	return Modifier::readAttribute(thread, result, attrib);
+}
+
 DynamicValueWriteProxy VariableModifier::createWriteProxy() {
 	DynamicValueWriteProxy proxy;
 	proxy.pod.objectRef = this;
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 32780db9673..55e4d258da0 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -2077,6 +2077,8 @@ public:
 	virtual bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) = 0;
 	virtual void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const = 0;
 
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
+
 	virtual DynamicValueWriteProxy createWriteProxy();
 
 private:


Commit: 0248d394a9aa901fcc3cb221780bad35496ef67e
    https://github.com/scummvm/scummvm/commit/0248d394a9aa901fcc3cb221780bad35496ef67e
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add more attributes

Changed paths:
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 8d8571682d7..8433a13835a 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -364,6 +364,7 @@ bool SoundElement::load(ElementLoaderContext &context, const Data::SoundElement
 		return false;
 
 	_paused = ((data.soundFlags & Data::SoundElement::kPaused) != 0);
+	_loop = ((data.soundFlags & Data::SoundElement::kLoop) != 0);
 	_leftVolume = data.leftVolume;
 	_rightVolume = data.rightVolume;
 	_balance = data.balance;
@@ -374,10 +375,29 @@ bool SoundElement::load(ElementLoaderContext &context, const Data::SoundElement
 }
 
 bool SoundElement::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "loop") {
+		result.setBool(_loop);
+		return true;
+	} else if (attrib == "volume") {
+		result.setInt((_leftVolume + _rightVolume) / 2);
+		return true;
+	}
+
 	return NonVisualElement::readAttribute(thread, result, attrib);
 }
 
 MiniscriptInstructionOutcome SoundElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
+	if (attrib == "loop") {
+		DynamicValueWriteFuncHelper<SoundElement, &SoundElement::scriptSetLoop>::create(this, writeProxy);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "volume") {
+		DynamicValueWriteFuncHelper<SoundElement, &SoundElement::scriptSetVolume>::create(this, writeProxy);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "balance") {
+		DynamicValueWriteFuncHelper<SoundElement, &SoundElement::scriptSetBalance>::create(this, writeProxy);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
 	return NonVisualElement::writeRefAttribute(thread, writeProxy, attrib);
 }
 
@@ -387,4 +407,58 @@ void SoundElement::activate() {
 void SoundElement::deactivate() {
 }
 
+MiniscriptInstructionOutcome SoundElement::scriptSetLoop(MiniscriptThread *thread, const DynamicValue &value) {
+	if (value.getType() != DynamicValueTypes::kBoolean)
+		return kMiniscriptInstructionOutcomeFailed;
+
+	setLoop(value.getBool());
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome SoundElement::scriptSetVolume(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger))
+		return kMiniscriptInstructionOutcomeFailed;
+
+	if (asInteger < 0)
+		asInteger = 0;
+	else if (asInteger > 100)
+		asInteger = 100;
+
+	setVolume(asInteger);
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome SoundElement::scriptSetBalance(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger))
+		return kMiniscriptInstructionOutcomeFailed;
+
+	if (asInteger < -100)
+		asInteger = -100;
+	else if (asInteger > 100)
+		asInteger = 100;
+
+	setBalance(asInteger);
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+void SoundElement::setLoop(bool loop) {
+	_loop = loop;
+}
+
+void SoundElement::setVolume(uint16 volume) {
+	uint16 fullVolumeLeft = 100 - _balance;
+	uint16 fullVolumeRight = 100 + _balance;
+
+	// Weird math to ensure _leftVolume + _rightVolume stays divisible by 2
+	_leftVolume = (volume * fullVolumeLeft + 50) / 100;
+	_rightVolume = volume * 2 - _leftVolume;
+}
+
+void SoundElement::setBalance(int16 balance) {
+	_balance = balance;
+	setVolume((_leftVolume + _rightVolume) / 2);
+}
+
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index ed7f77747f4..a78fb8bee96 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -213,10 +213,19 @@ public:
 #endif
 
 private:
+	MiniscriptInstructionOutcome scriptSetLoop(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetVolume(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetBalance(MiniscriptThread *thread, const DynamicValue &value);
+
+	void setLoop(bool loop);
+	void setVolume(uint16 volume);
+	void setBalance(int16 balance);
+
 	uint16 _leftVolume;
 	uint16 _rightVolume;
 	int16 _balance;
 	uint32 _assetID;
+	bool _loop;
 
 	Runtime *_runtime;
 };
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 836c5f6fbbe..10b1d5f9a63 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -107,14 +107,97 @@ Common::SharedPtr<Modifier> CursorModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new CursorModifier(*this));
 }
 
-bool STransCtModifier::load(const PlugInModifierLoaderContext& context, const Data::Standard::STransCtModifier& data) {
+bool STransCtModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::STransCtModifier &data) {
+	if (data.enableWhen.type != Data::PlugInTypeTaggedValue::kEvent ||
+		data.disableWhen.type != Data::PlugInTypeTaggedValue::kEvent ||
+		data.transitionType.type != Data::PlugInTypeTaggedValue::kInteger ||
+		data.transitionDirection.type != Data::PlugInTypeTaggedValue::kInteger ||
+		data.steps.type != Data::PlugInTypeTaggedValue::kInteger ||
+		data.duration.type != Data::PlugInTypeTaggedValue::kInteger ||
+		data.fullScreen.type != Data::PlugInTypeTaggedValue::kBoolean)
+		return false;
+
+	if (!_enableWhen.load(data.enableWhen.value.asEvent) || !_disableWhen.load(data.disableWhen.value.asEvent))
+		return false;
+
+	_transitionType = data.transitionType.value.asInt;
+	_transitionDirection = data.transitionDirection.value.asInt;
+	_steps = data.steps.value.asInt;
+	_duration = data.duration.value.asInt;
+	_fullScreen = data.fullScreen.value.asBoolean;
+
 	return true;
 }
 
+bool STransCtModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "rate") {
+		if (_duration <= (kMaxDuration / 100))
+			result.setInt(100);
+		else if (_duration >= kMaxDuration)
+			result.setInt(1);
+		else
+			result.setInt((kMaxDuration + (_duration / 2)) / _duration);
+		return true;
+	} else if (attrib == "steps") {
+		result.setInt(_steps);
+		return true;
+	}
+
+	return Modifier::readAttribute(thread, result, attrib);
+}
+
+MiniscriptInstructionOutcome STransCtModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
+	if (attrib == "rate") {
+		DynamicValueWriteFuncHelper<STransCtModifier, &STransCtModifier::scriptSetRate>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "steps") {
+		DynamicValueWriteFuncHelper<STransCtModifier, &STransCtModifier::scriptSetSteps>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	return Modifier::writeRefAttribute(thread, result, attrib);
+;
+}
+
+
 Common::SharedPtr<Modifier> STransCtModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new STransCtModifier(*this));
 }
 
+MiniscriptInstructionOutcome STransCtModifier::scriptSetRate(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger))
+		return kMiniscriptInstructionOutcomeFailed;
+
+	if (asInteger < 1)
+		asInteger = 1;
+	else if (asInteger > 100)
+		asInteger = 100;
+
+	if (asInteger == 100)
+		_duration = 0;
+	else
+		_duration = kMaxDuration / asInteger;
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome STransCtModifier::scriptSetSteps(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger))
+		return kMiniscriptInstructionOutcomeFailed;
+
+	if (asInteger < 4)
+		asInteger = 4;
+	else if (asInteger > 256)
+		asInteger = 100;
+
+	_steps = asInteger;
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+
 bool MediaCueMessengerModifier::load(const PlugInModifierLoaderContext& context, const Data::Standard::MediaCueMessengerModifier& data) {
 	if (data.enableWhen.type != Data::PlugInTypeTaggedValue::kEvent)
 		return false;
@@ -451,7 +534,7 @@ bool MidiModifier::load(const PlugInModifierLoaderContext &context, const Data::
 
 		_modeSpecific.file.loop = (data.modeSpecific.embedded.loop != 0);
 		_modeSpecific.file.overrideTempo = (data.modeSpecific.embedded.overrideTempo != 0);
-		_modeSpecific.file.volume = (data.modeSpecific.embedded.volume != 0);
+		_volume = data.modeSpecific.embedded.volume;
 
 		if (data.embeddedFadeIn.type != Data::PlugInTypeTaggedValue::kFloat
 			|| data.embeddedFadeOut.type != Data::PlugInTypeTaggedValue::kFloat
@@ -472,6 +555,8 @@ bool MidiModifier::load(const PlugInModifierLoaderContext &context, const Data::
 		_modeSpecific.singleNote.velocity = data.modeSpecific.singleNote.velocity;
 		_modeSpecific.singleNote.program = data.modeSpecific.singleNote.program;
 		_modeSpecific.singleNote.duration = data.singleNoteDuration.value.asFloat.toDouble();
+
+		_volume = 100;
 	}
 
 	return true;
@@ -493,6 +578,27 @@ VThreadState MidiModifier::consumeMessage(Runtime *runtime, const Common::Shared
 	return kVThreadReturn;
 }
 
+bool MidiModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "volume") {
+		result.setInt(_volume);
+		return true;
+	}
+
+	return Modifier::readAttribute(thread, result, attrib);
+}
+
+MiniscriptInstructionOutcome MidiModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
+	if (attrib == "volume") {
+		DynamicValueWriteFuncHelper<MidiModifier, &MidiModifier::scriptSetVolume>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "notevelocity") {
+		DynamicValueWriteFuncHelper<MidiModifier, &MidiModifier::scriptSetNoteVelocity>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	return Modifier::writeRefAttribute(thread, result, attrib);
+}
+
 Common::SharedPtr<Modifier> MidiModifier::shallowClone() const {
 	Common::SharedPtr<MidiModifier> clone(new MidiModifier(*this));
 
@@ -501,6 +607,40 @@ Common::SharedPtr<Modifier> MidiModifier::shallowClone() const {
 	return clone;
 }
 
+MiniscriptInstructionOutcome MidiModifier::scriptSetVolume(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger))
+		return kMiniscriptInstructionOutcomeFailed;
+
+	if (asInteger < 0)
+		asInteger = 0;
+	else if (asInteger > 100)
+		asInteger = 100;
+
+	_volume = asInteger;
+
+	if (_mode == kModeFile)
+		_plugIn->getMidi()->setVolume((_volume * 1306) >> 9);	// 100 -> 255 range
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome MidiModifier::scriptSetNoteVelocity(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger))
+		return kMiniscriptInstructionOutcomeFailed;
+
+	if (asInteger < 0)
+		asInteger = 0;
+	else if (asInteger > 127)
+		asInteger = 127;
+
+	if (_mode == kModeSingleNote)
+		_modeSpecific.singleNote.velocity = asInteger;
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 ListVariableModifier::ListVariableModifier() : _list(new DynamicList()) {
 }
 
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index 147ca450c0c..b38b0dc95a3 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -54,17 +54,36 @@ private:
 	uint32 _cursorID;
 };
 
-// Some sort of scene transition modifier
+// This appears to be basically a duplicate of scene transition modifier, except unlike that,
+// its parameters are controllable via script, and the duration scaling appears to be different
+// (probably 600000 max rate instead of 6000000)
 class STransCtModifier : public Modifier {
 public:
+	static const int32 kMaxDuration = 600000;
+
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::STransCtModifier &data);
 
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib);
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
-	const char *debugGetTypeName() const override { return "Unknown STransCt Modifier"; }
+	const char *debugGetTypeName() const override { return "STransCt Scene Transition Modifier"; }
 #endif
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+
+	MiniscriptInstructionOutcome scriptSetRate(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetSteps(MiniscriptThread *thread, const DynamicValue &value);
+
+	Event _enableWhen;
+	Event _disableWhen;
+
+	int32 _transitionType;
+	int32 _transitionDirection;
+	int32 _steps;
+	int32 _duration;
+	bool _fullScreen;
 };
 
 class MediaCueMessengerModifier : public Modifier {
@@ -136,17 +155,23 @@ public:
 	bool respondsToEvent(const Event &evt) const override;
 	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
 
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib);
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "MIDI Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusPartial; }
 #endif
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
+	MiniscriptInstructionOutcome scriptSetVolume(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetNoteVelocity(MiniscriptThread *thread, const DynamicValue &value);
+
 	struct FilePart {
 		bool loop;
 		bool overrideTempo;
-		bool volume;
 		double tempo;
 		double fadeIn;
 		double fadeOut;
@@ -175,6 +200,7 @@ private:
 
 	Mode _mode;
 	ModeSpecificUnion _modeSpecific;
+	uint8 _volume;	// We need this always available because scripts will try to set it and then read it even in single note mode
 
 	Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> _embeddedFile;
 
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 7e6f4b5fc3b..b252b26941b 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -4785,6 +4785,12 @@ MiniscriptInstructionOutcome VisualElement::writeRefAttribute(MiniscriptThread *
 	} else if (attrib == "position") {
 		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetPosition>::create(this, writeProxy);
 		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "width") {
+		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetWidth>::create(this, writeProxy);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "height") {
+		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetHeight>::create(this, writeProxy);
+		return kMiniscriptInstructionOutcomeContinue;
 	}
 
 	return Element::writeRefAttribute(thread, writeProxy, attrib);
@@ -4842,17 +4848,17 @@ bool VisualElement::loadCommon(const Common::String &name, uint32 guid, const Da
 	return true;
 }
 
-MiniscriptInstructionOutcome VisualElement::scriptSetDirect(MiniscriptThread *thread, const DynamicValue &dest) {
-	if (dest.getType() == DynamicValueTypes::kBoolean) {
-		_directToScreen = dest.getBool();
+MiniscriptInstructionOutcome VisualElement::scriptSetDirect(MiniscriptThread *thread, const DynamicValue &value) {
+	if (value.getType() == DynamicValueTypes::kBoolean) {
+		_directToScreen = value.getBool();
 		return kMiniscriptInstructionOutcomeContinue;
 	}
 	return kMiniscriptInstructionOutcomeFailed;
 }
 
-MiniscriptInstructionOutcome VisualElement::scriptSetPosition(MiniscriptThread *thread, const DynamicValue &dest) {
-	if (dest.getType() == DynamicValueTypes::kPoint) {
-		const Point16 &destPoint = dest.getPoint();
+MiniscriptInstructionOutcome VisualElement::scriptSetPosition(MiniscriptThread *thread, const DynamicValue &value) {
+	if (value.getType() == DynamicValueTypes::kPoint) {
+		const Point16 &destPoint = value.getPoint();
 		int32 xDelta = destPoint.x - _rect.left;
 		int32 yDelta = destPoint.y - _rect.right;
 
@@ -4864,6 +4870,26 @@ MiniscriptInstructionOutcome VisualElement::scriptSetPosition(MiniscriptThread *
 	return kMiniscriptInstructionOutcomeFailed;
 }
 
+MiniscriptInstructionOutcome VisualElement::scriptSetWidth(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger))
+		return kMiniscriptInstructionOutcomeFailed;
+
+	_rect.right = _rect.left + asInteger;
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome VisualElement::scriptSetHeight(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger))
+		return kMiniscriptInstructionOutcomeFailed;
+
+	_rect.bottom = _rect.top + asInteger;
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 void VisualElement::offsetTranslate(int32 xDelta, int32 yDelta, bool cachedOriginOnly) {
 	if (!cachedOriginOnly) {
 		_rect.left += xDelta;
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 55e4d258da0..5b3748088db 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -1974,6 +1974,8 @@ protected:
 	MiniscriptInstructionOutcome scriptSetDirect(MiniscriptThread *thread, const DynamicValue &dest);
 	MiniscriptInstructionOutcome scriptSetPosition(MiniscriptThread *thread, const DynamicValue &dest);
 	MiniscriptInstructionOutcome scriptSetVisibility(MiniscriptThread *thread, const DynamicValue &result);
+	MiniscriptInstructionOutcome scriptSetWidth(MiniscriptThread *thread, const DynamicValue &dest);
+	MiniscriptInstructionOutcome scriptSetHeight(MiniscriptThread *thread, const DynamicValue &dest);
 
 	void offsetTranslate(int32 xDelta, int32 yDelta, bool cachedOriginOnly);
 


Commit: b84f7be7d6d63adfa320e5dc51c8f95df0b55aec
    https://github.com/scummvm/scummvm/commit/b84f7be7d6d63adfa320e5dc51c8f95df0b55aec
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix wrong channel indexes for Mac format 32-bit images

Changed paths:
    engines/mtropolis/assets.cpp


diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index f1316747144..a1da773a159 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -364,9 +364,9 @@ const Common::SharedPtr<CachedImage> &ImageAsset::loadAndCacheImage(Runtime *run
 		case kColorDepthMode32Bit: {
 				if (imageFormat == ImageAsset::kImageFormatMac) {
 					for (int x = 0; x < width; x++) {
-						uint8 r = inRowBytes[x * 4 + 0];
-						uint8 g = inRowBytes[x * 4 + 1];
-						uint8 b = inRowBytes[x * 4 + 2];
+						uint8 r = inRowBytes[x * 4 + 1];
+						uint8 g = inRowBytes[x * 4 + 2];
+						uint8 b = inRowBytes[x * 4 + 3];
 						uint32 repacked = (255 << pixelFmt.aShift) | (r << pixelFmt.rShift) | (g << pixelFmt.gShift) | (b << pixelFmt.bShift);
 						static_cast<uint32 *>(outBase)[x] = repacked;
 					}


Commit: 2a90865bcc168133d50b7fa7855c7dc815ad93e0
    https://github.com/scummvm/scummvm/commit/2a90865bcc168133d50b7fa7855c7dc815ad93e0
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: More script attribs, enough to boot to start scene on Mac version

Changed paths:
    engines/mtropolis/plugin/obsidian.cpp
    engines/mtropolis/plugin/obsidian.h
    engines/mtropolis/runtime.cpp


diff --git a/engines/mtropolis/plugin/obsidian.cpp b/engines/mtropolis/plugin/obsidian.cpp
index 7775098e327..b157ee2f1df 100644
--- a/engines/mtropolis/plugin/obsidian.cpp
+++ b/engines/mtropolis/plugin/obsidian.cpp
@@ -22,6 +22,8 @@
 #include "mtropolis/plugin/obsidian.h"
 #include "mtropolis/plugins.h"
 
+#include "mtropolis/miniscript.h"
+
 namespace MTropolis {
 
 namespace Obsidian {
@@ -42,10 +44,79 @@ Common::SharedPtr<Modifier> RectShiftModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new RectShiftModifier(*this));
 }
 
+TextWorkModifier::TextWorkModifier() : _firstChar(0), _lastChar(0) {
+}
+
 bool TextWorkModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::TextWorkModifier &data) {
 	return true;
 }
 
+bool TextWorkModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "numchar") {
+		result.setInt(_string.size());
+		return true;
+	} else if (attrib == "output") {
+		int32 firstChar = _firstChar - 1;
+		int32 len = _lastChar - _firstChar + 1;
+		if (_firstChar < 0) {
+			len += firstChar;
+			firstChar = 0;
+		}
+		if (len <= 0 || static_cast<size_t>(firstChar) >= _string.size())
+			result.setString("");
+		else {
+			const size_t availChars = _string.size() - firstChar;
+			if (static_cast<size_t>(len) > availChars)
+				len = availChars;
+			result.setString(_string.substr(firstChar, len));
+		}
+		return true;
+	} else if (attrib == "exists") {
+		bool exists = (_string.find(_token) != Common::String::npos);
+		result.setInt(exists ? 1 : 0);
+		return true;
+	} else if (attrib == "index") {
+		size_t index = _string.find(_token);
+		if (index == Common::String::npos)
+			index = 0;
+		else
+			index++;
+
+		result.setInt(index);
+		return true;
+	} else if (attrib == "numword") {
+		thread->error("Not-yet-implemented TextWork attribute 'numword'");
+		return false;
+	}
+
+	return Modifier::readAttribute(thread, result, attrib);
+}
+
+MiniscriptInstructionOutcome TextWorkModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
+	if (attrib == "string") {
+		DynamicValueWriteStringHelper::create(&_string, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "firstchar") {
+		DynamicValueWriteIntegerHelper<int32>::create(&_firstChar, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "lastchar") {
+		DynamicValueWriteIntegerHelper<int32>::create(&_lastChar, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "token") {
+		DynamicValueWriteStringHelper::create(&_token, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "firstword") {
+		thread->error("Not-yet-implemented TextWork attrib 'firstword'");
+		return kMiniscriptInstructionOutcomeFailed;
+	} else if (attrib == "lastword") {
+		thread->error("Not-yet-implemented TextWork attrib 'lastword'");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	return Modifier::writeRefAttribute(thread, result, attrib);
+}
+
+
 Common::SharedPtr<Modifier> TextWorkModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new TextWorkModifier(*this));
 }
diff --git a/engines/mtropolis/plugin/obsidian.h b/engines/mtropolis/plugin/obsidian.h
index dc3e755d5c1..f7ce43d3768 100644
--- a/engines/mtropolis/plugin/obsidian.h
+++ b/engines/mtropolis/plugin/obsidian.h
@@ -57,14 +57,26 @@ private:
 
 class TextWorkModifier : public Modifier {
 public:
+	TextWorkModifier();
+
 	bool load(const PlugInModifierLoaderContext &context, const Data::Obsidian::TextWorkModifier &data);
 
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib);
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "TextWork Modifier"; }
 #endif
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+
+	Common::String _string;
+	Common::String _token;
+
+	// These appear to be 1-based?
+	int32 _firstChar;
+	int32 _lastChar;
 };
 
 class ObsidianPlugIn : public MTropolis::PlugIn {
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index b252b26941b..4633fab2338 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -2212,6 +2212,48 @@ bool Structural::readAttribute(MiniscriptThread *thread, DynamicValue &result, c
 		else
 			result.clear();
 		return true;
+	} else if (attrib == "previous") {
+		Structural *parent = getParent();
+		if (parent) {
+			const Common::Array<Common::SharedPtr<Structural> > &neighborhood = parent->getChildren();
+			bool found = false;
+			size_t foundIndex = 0;
+			for (size_t i = 0; i < neighborhood.size(); i++) {
+				if (neighborhood[i].get() == this) {
+					foundIndex = i;
+					found = true;
+					break;
+				}
+			}
+
+			if (found && foundIndex > 0)
+				result.setObject(neighborhood[foundIndex - 1]->getSelfReference());
+			else
+				result.clear();
+		} else
+			result.clear();
+		return true;
+	} else if (attrib == "next") {
+		Structural *parent = getParent();
+		if (parent) {
+			const Common::Array<Common::SharedPtr<Structural> > &neighborhood = parent->getChildren();
+			bool found = false;
+			size_t foundIndex = 0;
+			for (size_t i = 0; i < neighborhood.size(); i++) {
+				if (neighborhood[i].get() == this) {
+					foundIndex = i;
+					found = true;
+					break;
+				}
+			}
+
+			if (found && foundIndex < neighborhood.size() - 1)
+				result.setObject(neighborhood[foundIndex + 1]->getSelfReference());
+			else
+				result.clear();
+		} else
+			result.clear();
+		return true;
 	}
 
 	return RuntimeObject::readAttribute(thread, result, attrib);


Commit: b64d73849d9ac23e05d65e721ea2cb3f3d3e391c
    https://github.com/scummvm/scummvm/commit/b64d73849d9ac23e05d65e721ea2cb3f3d3e391c
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Refactor key message dispatch, add mouse messages, fix elements being parented incorrectly.

Changed paths:
    engines/mtropolis/elements.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/render.cpp
    engines/mtropolis/render.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 8433a13835a..5c0f1cae32f 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -449,7 +449,6 @@ void SoundElement::setLoop(bool loop) {
 
 void SoundElement::setVolume(uint16 volume) {
 	uint16 fullVolumeLeft = 100 - _balance;
-	uint16 fullVolumeRight = 100 + _balance;
 
 	// Weird math to ensure _leftVolume + _rightVolume stays divisible by 2
 	_leftVolume = (volume * fullVolumeLeft + 50) / 100;
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 627ffc802cd..4a5b423e19f 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -688,8 +688,13 @@ Common::SharedPtr<Modifier> CollisionDetectionMessengerModifier::shallowClone()
 }
 
 KeyboardMessengerModifier::~KeyboardMessengerModifier() {
-	if (_signaller)
-		_signaller->removeReceiver(this);
+}
+
+KeyboardMessengerModifier::KeyboardMessengerModifier() : _isEnabled(false) {
+}
+
+bool KeyboardMessengerModifier::isKeyboardMessenger() const {
+	return true;
 }
 
 bool KeyboardMessengerModifier::load(ModifierLoaderContext &context, const Data::KeyboardMessengerModifier &data) {
@@ -744,27 +749,24 @@ bool KeyboardMessengerModifier::respondsToEvent(const Event &evt) const {
 
 VThreadState KeyboardMessengerModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
  	if (Event::create(EventIDs::kParentEnabled, 0).respondsTo(msg->getEvent())) {
-		if (!_signaller)
-			_signaller = runtime->getProject()->notifyOnKeyboardEvent(this);
+		_isEnabled = true;
 	} else if (Event::create(EventIDs::kParentDisabled, 0).respondsTo(msg->getEvent())) {
-		if (_signaller) {
-			_signaller->removeReceiver(this);
-			_signaller.reset();
-		}
+		_isEnabled = false;
 	}
 
 	return kVThreadReturn;
 }
 
-
 Common::SharedPtr<Modifier> KeyboardMessengerModifier::shallowClone() const {
 	Common::SharedPtr<KeyboardMessengerModifier> cloned(new KeyboardMessengerModifier(*this));
-
-	cloned->_signaller.reset();
+	cloned->_isEnabled = false;
 	return cloned;
 }
 
-void KeyboardMessengerModifier::onKeyboardEvent(Runtime *runtime, Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt) {
+bool KeyboardMessengerModifier::checkKeyEventTrigger(Runtime *runtime, Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt, Common::String &outCharStr) const {
+	if (!_isEnabled)
+		return false;
+
 	bool responds = false;
 	if (evtType == Common::EVENT_KEYDOWN) {
 		if (repeat)
@@ -775,32 +777,30 @@ void KeyboardMessengerModifier::onKeyboardEvent(Runtime *runtime, Common::EventT
 		responds = _onUp;
 
 	if (!responds)
-		return;
+		return false;
 
 	if (_keyModCommand) {
 		if (runtime->getPlatform() == kProjectPlatformWindows) {
 			// Windows projects check "alt"
 			if ((keyEvt.flags & Common::KBD_ALT) == 0)
-				return;
+				return false;
 		} else if (runtime->getPlatform() == kProjectPlatformMacintosh) {
 			if ((keyEvt.flags & Common::KBD_META) == 0)
-				return;
+				return false;
 		}
 	}
 
 	if (_keyModControl) {
 		if ((keyEvt.flags & Common::KBD_CTRL) == 0)
-			return;
+			return false;
 	}
 
 	if (_keyModOption) {
 		if ((keyEvt.flags & Common::KBD_ALT) == 0)
-			return;
+			return false;
 	}
 
-	Common::String encodedChar;
-
-	Common::String resolvedCharStr;
+	outCharStr.clear();
 
 	KeyCodeType resolvedType = kAny;
 	switch (keyEvt.keycode) {
@@ -855,31 +855,35 @@ void KeyboardMessengerModifier::onKeyboardEvent(Runtime *runtime, Common::EventT
 			bool isQuestion = (keyEvt.ascii == '?');
 			uint32 uchar = keyEvt.ascii;
 			Common::U32String u(&uchar, 1);
-			resolvedCharStr = u.encode(Common::kMacRoman);
+			outCharStr = u.encode(Common::kMacRoman);
 
 			// STUPID HACK PLEASE FIX ME: ScummVM has no way of just telling us that the character mapping failed,
 			// we we have to check if it encoded "?"
-			if (resolvedCharStr.size() < 1 || (resolvedCharStr[0] == '?' && !isQuestion))
-				return;
+			if (outCharStr.size() < 1 || (outCharStr[0] == '?' && !isQuestion))
+				return false;
 
 			resolvedType = kMacRomanChar;
 		} break;
 	}
 
 	if (_keyCodeType != kAny && resolvedType != _keyCodeType)
-		return;
+		return false;
 
-	if (_keyCodeType == kMacRomanChar && (resolvedCharStr.size() == 0 || resolvedCharStr[0] != _macRomanChar))
-		return;
+	if (_keyCodeType == kMacRomanChar && (outCharStr.size() == 0 || outCharStr[0] != _macRomanChar))
+		return false;
+
+	return true;
+}
 
+void KeyboardMessengerModifier::dispatchMessage(Runtime *runtime, const Common::String &charStr) {
 	Common::SharedPtr<MessageProperties> msgProps;
 	if (_sendSpec.with.getType() == DynamicValueTypes::kIncomingData) {
-		if (resolvedCharStr.size() != 1)
+		if (charStr.size() != 1)
 			warning("Keyboard messenger is supposed to send the character code, but they key was a special key and we haven't implemented conversion of those keycodes");
 
-		DynamicValue charStr;
-		charStr.setString(resolvedCharStr);
-		_sendSpec.sendFromMessengerWithCustomData(runtime, this, charStr);
+		DynamicValue charStrValue;
+		charStrValue.setString(charStr);
+		_sendSpec.sendFromMessengerWithCustomData(runtime, this, charStrValue);
 	} else {
 		_sendSpec.sendFromMessenger(runtime, this);
 	}
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index edec3cc85c0..2abe541a222 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -443,15 +443,21 @@ private:
 	bool _sendToOnlyFirstCollidingElement;
 };
 
-class KeyboardMessengerModifier : public Modifier, public IKeyboardEventReceiver {
+class KeyboardMessengerModifier : public Modifier {
 public:
+	KeyboardMessengerModifier();
 	~KeyboardMessengerModifier();
 
+	bool isKeyboardMessenger() const;
+
 	bool load(ModifierLoaderContext &context, const Data::KeyboardMessengerModifier &data);
 
 	bool respondsToEvent(const Event &evt) const override;
 	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
 
+	bool checkKeyEventTrigger(Runtime *runtime, Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt, Common::String &outChar) const;
+	void dispatchMessage(Runtime *runtime, const Common::String &charStr);
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Keyboard Messenger Modifier"; }
 #endif
@@ -459,8 +465,6 @@ public:
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
-	void onKeyboardEvent(Runtime *runtime, Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt) override;
-
 	void visitInternalReferences(IStructuralReferenceVisitor *visitor) override;
 	void linkInternalReferences(ObjectLinkingScope *scope) override;
 
@@ -492,12 +496,11 @@ private:
 	bool _keyModControl : 1;
 	bool _keyModCommand : 1;
 	bool _keyModOption : 1;
+	bool _isEnabled : 1;
 	KeyCodeType _keyCodeType;
 	char _macRomanChar;
 
 	MessengerSendSpec _sendSpec;
-
-	Common::SharedPtr<KeyboardEventSignaller> _signaller;
 };
 
 class TextStyleModifier : public Modifier {
diff --git a/engines/mtropolis/render.cpp b/engines/mtropolis/render.cpp
index 9ff270a88b1..d1b1805d4d1 100644
--- a/engines/mtropolis/render.cpp
+++ b/engines/mtropolis/render.cpp
@@ -169,6 +169,8 @@ void Window::onMouseMove(int32 x, int32 y) {
 void Window::onMouseUp(int32 x, int32 y, int mouseButton) {
 }
 
+void Window::onKeyboardEvent(const Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt) {
+}
 
 namespace Render {
 
diff --git a/engines/mtropolis/render.h b/engines/mtropolis/render.h
index 190b5f9b3db..8c3db3b1c57 100644
--- a/engines/mtropolis/render.h
+++ b/engines/mtropolis/render.h
@@ -22,6 +22,7 @@
 #ifndef MTROPOLIS_RENDER_H
 #define MTROPOLIS_RENDER_H
 
+#include "common/events.h"
 #include "common/ptr.h"
 #include "common/scummsys.h"
 
@@ -98,8 +99,9 @@ public:
 	virtual void onMouseDown(int32 x, int32 y, int mouseButton);
 	virtual void onMouseMove(int32 x, int32 y);
 	virtual void onMouseUp(int32 x, int32 y, int mouseButton);
+	virtual void onKeyboardEvent(const Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt);
 
-private:
+protected:
 	int32 _x;
 	int32 _y;
 	Common::SharedPtr<Graphics::ManagedSurface> _surface;
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 4633fab2338..7d680d52993 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -46,6 +46,53 @@
 
 namespace MTropolis {
 
+class MainWindow : public Window {
+public:
+	MainWindow(const WindowParameters &windowParams);
+
+	void onMouseDown(int32 x, int32 y, int mouseButton) override;
+	void onMouseMove(int32 x, int32 y) override;
+	void onMouseUp(int32 x, int32 y, int mouseButton) override;
+	void onKeyboardEvent(const Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt) override;
+
+private:
+	bool _mouseButtonStates[Actions::kMouseButtonCount];
+};
+
+MainWindow::MainWindow(const WindowParameters &windowParams) : Window(windowParams) {
+	for (int i = 0; i < Actions::kMouseButtonCount; i++)
+		_mouseButtonStates[i] = false;
+}
+
+void MainWindow::onMouseDown(int32 x, int32 y, int mouseButton) {
+	if (!_mouseButtonStates[mouseButton]) {
+		_mouseButtonStates[mouseButton] = true;
+
+		if (mouseButton == Actions::kMouseButtonLeft) {
+			_runtime->queueOSEvent(Common::SharedPtr<OSEvent>(new MouseInputEvent(kOSEventTypeMouseDown, x, y, static_cast<Actions::MouseButton>(mouseButton))));
+		}
+	}
+}
+
+void MainWindow::onMouseMove(int32 x, int32 y) {
+	_runtime->queueOSEvent(Common::SharedPtr<OSEvent>(new MouseInputEvent(kOSEventTypeMouseMove, x, y, Actions::kMouseButtonLeft)));
+}
+
+void MainWindow::onMouseUp(int32 x, int32 y, int mouseButton) {
+	if (_mouseButtonStates[mouseButton]) {
+		_mouseButtonStates[mouseButton] = false;
+
+		if (mouseButton == Actions::kMouseButtonLeft) {
+			_runtime->queueOSEvent(Common::SharedPtr<OSEvent>(new MouseInputEvent(kOSEventTypeMouseUp, x, y, static_cast<Actions::MouseButton>(mouseButton))));
+		}
+	}
+}
+
+void MainWindow::onKeyboardEvent(const Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt) {
+	_runtime->queueOSEvent(Common::SharedPtr<OSEvent>(new KeyboardInputEvent(kOSEventTypeKeyboard, evtType, repeat, keyEvt)));
+}
+
+
 class ModifierInnerScopeBuilder : public IStructuralReferenceVisitor {
 public:
 	ModifierInnerScopeBuilder(ObjectLinkingScope *scope);
@@ -2427,6 +2474,17 @@ void Structural::activate() {
 void Structural::deactivate() {
 }
 
+void Structural::recursiveCollectObjectsMatchingCriteria(Common::Array<Common::WeakPtr<RuntimeObject> > &results, bool (*evalFunc)(void *userData, RuntimeObject *object), void *userData, bool onlyEnabled) {
+	if (evalFunc(userData, this))
+		results.push_back(this->getSelfReference().lock());
+
+	for (const Common::SharedPtr<Structural> &child : _children)
+		child->recursiveCollectObjectsMatchingCriteria(results, evalFunc, userData, onlyEnabled);
+
+	for (const Common::SharedPtr<Modifier> &modifier : _modifiers)
+		modifier->recursiveCollectObjectsMatchingCriteria(results, evalFunc, userData, onlyEnabled);
+}
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 SupportStatus Structural::debugGetSupportStatus() const {
 	return kSupportStatusNone;
@@ -2748,6 +2806,47 @@ VThreadState MessageDispatch::continuePropagating(Runtime *runtime) {
 	return kVThreadReturn;
 }
 
+KeyEventDispatch::KeyEventDispatch(const Common::SharedPtr<KeyboardInputEvent> &evt) : _dispatchIndex(0), _evt(evt) {
+}
+
+Common::Array<Common::WeakPtr<RuntimeObject> >& KeyEventDispatch::getKeyboardMessengerArray() {
+	return _keyboardMessengers;
+}
+
+bool KeyEventDispatch::keyboardMessengerFilterFunc(void *userData, RuntimeObject *object) {
+	if (!object->isModifier())
+		return false;
+
+	return static_cast<Modifier *>(object)->isKeyboardMessenger();
+}
+
+bool KeyEventDispatch::isTerminated() const {
+	return _dispatchIndex == _keyboardMessengers.size();
+}
+
+VThreadState KeyEventDispatch::continuePropagating(Runtime *runtime) {
+	KeyboardInputEvent *evt = _evt.get();
+
+	// This is kind of messy but we have to guard against situations where a key event triggers a clone
+	// which may itself contain a keyboard messenger.  (Do multiple messengers respond to keystrokes?)
+	while (_dispatchIndex < _keyboardMessengers.size()) {
+		Common::SharedPtr<RuntimeObject> obj = _keyboardMessengers[_dispatchIndex++].lock();
+		assert(obj->isModifier());
+
+		Modifier *modifier = static_cast<Modifier *>(obj.get());
+		assert(modifier->isKeyboardMessenger());
+
+		KeyboardMessengerModifier *msgr = static_cast<KeyboardMessengerModifier *>(modifier);
+		Common::String charStr;
+		if (msgr->checkKeyEventTrigger(runtime, evt->getKeyEventType(), evt->isRepeat(), evt->getKeyState(), charStr)) {
+			msgr->dispatchMessage(runtime, charStr);
+			return kVThreadReturn;
+		}
+	}
+
+	return kVThreadReturn;
+}
+
 void ScheduledEvent::cancel() {
 	if (_scheduler)
 		_scheduler->removeEvent(this);
@@ -2855,12 +2954,56 @@ const byte DefaultCursor::_cursorPalette[9] = {
 	255, 255, 255
 };
 
+
+OSEvent::OSEvent(OSEventType eventType) : _eventType(eventType) {
+}
+
+OSEvent::~OSEvent() {
+}
+
+OSEventType OSEvent::getEventType() const {
+	return _eventType;
+}
+
+MouseInputEvent::MouseInputEvent(OSEventType eventType, int32 x, int32 y, Actions::MouseButton button) : OSEvent(eventType), _x(x), _y(y), _button(button) {
+}
+
+int32 MouseInputEvent::getX() const {
+	return _x;
+}
+
+int32 MouseInputEvent::getY() const {
+	return _y;
+}
+
+Actions::MouseButton MouseInputEvent::getButton() const {
+	return _button;
+}
+
+
+KeyboardInputEvent::KeyboardInputEvent(OSEventType osEventType, Common::EventType keyEventType, bool repeat, const Common::KeyState &keyEvt)
+	: OSEvent(osEventType), _keyEventType(keyEventType), _repeat(repeat), _keyEvt(keyEvt) {
+}
+
+Common::EventType KeyboardInputEvent::getKeyEventType() const {
+	return _keyEventType;
+}
+
+bool KeyboardInputEvent::isRepeat() const {
+	return _repeat;
+}
+
+const Common::KeyState &KeyboardInputEvent::getKeyState() const {
+	return _keyEvt;
+}
+
 Runtime::SceneStackEntry::SceneStackEntry() {
 }
 
 Runtime::Runtime(OSystem *system) : _nextRuntimeGUID(1), _realDisplayMode(kColorDepthModeInvalid), _fakeDisplayMode(kColorDepthModeInvalid),
 									_displayWidth(1024), _displayHeight(768), _realTimeBase(0), _playTimeBase(0), _sceneTransitionState(kSceneTransitionStateNotTransitioning),
-									_system(system), _lastFrameCursor(nullptr), _defaultCursor(new DefaultCursor()), _platform(kProjectPlatformUnknown) {
+									_system(system), _lastFrameCursor(nullptr), _defaultCursor(new DefaultCursor()), _platform(kProjectPlatformUnknown),
+									_cachedMousePosition(Point16::create(0, 0)), _trackedMouseOutside(false) {
 	_random.reset(new Common::RandomSource("mtropolis"));
 
 	_vthread.reset(new VThread());
@@ -2908,6 +3051,53 @@ bool Runtime::runFrame() {
 			break;
 		}
 
+		if (_osEventQueue.size() > 0) {
+			Common::SharedPtr<OSEvent> evt = _osEventQueue[0];
+			_osEventQueue.remove_at(0);
+
+			OSEventType evtType = evt->getEventType();
+			switch (evtType)
+			{
+			case kOSEventTypeKeyboard:
+				if (_project) {
+					Common::SharedPtr<KeyEventDispatch> dispatch(new KeyEventDispatch(evt.staticCast<KeyboardInputEvent>()));
+
+					// We don't want to filter by enabled because of the edge case where a keyboard
+					// messenger fires and disables or enables other keyboard messengers.
+					// Not sure this is actually possible though... can multiple messengers respond to
+					// the same keystroke?  Not sure.
+					_project->recursiveCollectObjectsMatchingCriteria(dispatch->getKeyboardMessengerArray(), KeyEventDispatch::keyboardMessengerFilterFunc, dispatch.get(), false);
+
+					if (dispatch->getKeyboardMessengerArray().size() > 0) {
+						DispatchKeyTaskData *taskData = _vthread->pushTask("Runtime::dispatchKeyTask", this, &Runtime::dispatchKeyTask);
+						taskData->dispatch = dispatch;
+					}
+				}
+				break;
+			case kOSEventTypeMouseDown:
+			case kOSEventTypeMouseUp:
+			case kOSEventTypeMouseMove: {
+					MouseInputEvent *mouseEvt = static_cast<MouseInputEvent *>(evt.get());
+
+					// Maybe shouldn't post the update mouse button task if non-left buttons are pressed?
+					if ((evtType == kOSEventTypeMouseDown || evtType == kOSEventTypeMouseUp) && mouseEvt->getButton() == Actions::kMouseButtonLeft) {
+						UpdateMouseStateTaskData *taskData = _vthread->pushTask("Runtime::updateMouseStateTask", this, &Runtime::updateMouseStateTask);
+						taskData->mouseDown = (evtType == kOSEventTypeMouseDown);
+					}
+
+					// Pushed second, so this happens first
+					if (mouseEvt->getX() != _cachedMousePosition.x || mouseEvt->getY() != _cachedMousePosition.y) {
+						UpdateMousePositionTaskData *taskData = _vthread->pushTask("Runtime::updateMousePositionTask1", this, &Runtime::updateMousePositionTask1);
+						taskData->x = mouseEvt->getX();
+						taskData->y = mouseEvt->getY();
+					}
+				} break;
+			default:
+				break;
+			}
+			continue;
+		}
+
 		if (_queuedProjectDesc) {
 			Common::SharedPtr<ProjectDescription> desc = _queuedProjectDesc;
 			_queuedProjectDesc.reset();
@@ -3415,6 +3605,68 @@ void Runtime::recursiveActivateStructural(Structural *structural) {
 	}
 }
 
+bool Runtime::isStructuralMouseInteractive(Structural *structural) {
+	for (const Common::SharedPtr<Modifier> &modifier : structural->getModifiers()) {
+		if (isModifierMouseInteractive(modifier.get()))
+			return true;
+	}
+
+	return false;
+}
+
+bool Runtime::isModifierMouseInteractive(Modifier *modifier) {
+	const EventIDs::EventID evtIDs[] = {
+		EventIDs::kMouseUp,
+		EventIDs::kMouseDown,
+		EventIDs::kMouseOver,
+		EventIDs::kMouseOutside,
+		EventIDs::kMouseTrackedInside,
+		EventIDs::kMouseTracking,
+		EventIDs::kMouseTrackedOutside,
+		EventIDs::kMouseUpInside,
+		EventIDs::kMouseUpOutside
+	};
+
+	for (EventIDs::EventID evtID : evtIDs) {
+		if (modifier->respondsToEvent(Event::create(evtID, 0)))
+			return true;
+	}
+
+	IModifierContainer *propagationContainer = modifier->getMessagePropagationContainer();
+	if (propagationContainer) {
+		for (const Common::SharedPtr<Modifier> &child : propagationContainer->getModifiers()) {
+			if (isModifierMouseInteractive(child.get()))
+				return true;
+		}
+	}
+
+	return false;
+}
+
+void Runtime::recursiveFindMouseCollision(Structural *&bestResult, int &bestLayer, Structural *candidate, int32 relativeX, int32 relativeY) {
+	int32 childRelativeX = relativeX;
+	int32 childRelativeY = relativeY;
+	if (candidate->isElement()) {
+		Element *element = static_cast<Element *>(candidate);
+		if (element->isVisual()) {
+			VisualElement *visual = static_cast<VisualElement *>(candidate);
+			int layer = visual->getLayer();
+			if (layer > bestLayer && visual->isMouseInsideBox(relativeX, relativeY) && isStructuralMouseInteractive(visual) && visual->isMouseCollisionAtPoint(relativeX, relativeY)) {
+				bestResult = candidate;
+				bestLayer = layer;
+			}
+
+			childRelativeX -= visual->getRelativeRect().left;
+			childRelativeY -= visual->getRelativeRect().top;
+		}
+	}
+
+
+	for (const Common::SharedPtr<Structural> &child : candidate->getChildren()) {
+		recursiveFindMouseCollision(bestResult, bestLayer, child.get(), childRelativeX, childRelativeY);
+	}
+}
+
 void Runtime::queueEventAsLowLevelSceneStateTransitionAction(const Event &evt, Structural *root, bool cascade, bool relay) {
 	Common::SharedPtr<MessageProperties> props(new MessageProperties(evt, DynamicValue(), Common::WeakPtr<RuntimeObject>()));
 	Common::SharedPtr<MessageDispatch> msg(new MessageDispatch(props, root, cascade, relay, false));
@@ -3465,6 +3717,21 @@ VThreadState Runtime::dispatchMessageTask(const DispatchMethodTaskData &data) {
 	}
 }
 
+VThreadState Runtime::dispatchKeyTask(const DispatchKeyTaskData &data) {
+	Common::SharedPtr<KeyEventDispatch> dispatchPtr = data.dispatch;
+	KeyEventDispatch &dispatch = *dispatchPtr.get();
+
+	if (dispatch.isTerminated())
+		return kVThreadReturn;
+	else {
+		// Requeue propagation after whatever happens with this propagation step
+		DispatchKeyTaskData *requeueData = _vthread->pushTask("Runtime::dispatchKeyTask", this, &Runtime::dispatchKeyTask);
+		requeueData->dispatch = dispatchPtr;
+
+		return dispatch.continuePropagating(this);
+	}
+}
+
 VThreadState Runtime::consumeMessageTask(const ConsumeMessageTaskData &data) {
 	IMessageConsumer *consumer = data.consumer;
 	assert(consumer->respondsToEvent(data.message->getEvent()));
@@ -3476,9 +3743,176 @@ VThreadState Runtime::consumeCommandTask(const ConsumeCommandTaskData &data) {
 	return structural->consumeCommand(this, data.message);
 }
 
+VThreadState Runtime::updateMouseStateTask(const UpdateMouseStateTaskData &data) {
+	struct MessageToSend {
+		EventIDs::EventID eventID;
+		Structural *target;
+	};
+
+	Common::Array<MessageToSend> messagesToSend;
+
+	if (data.mouseDown) {
+		// Mouse down
+		Common::SharedPtr<Structural> tracked = _mouseOverObject.lock();
+		if (tracked) {
+			_mouseTrackingObject = tracked;
+			_trackedMouseOutside = false;
+
+			MessageToSend msg;
+			msg.eventID = EventIDs::kMouseUp;
+			msg.target = tracked.get();
+			messagesToSend.push_back(msg);
+		}
+	} else {
+		// Mouse up
+		Common::SharedPtr<Structural> tracked = _mouseTrackingObject.lock();
+		if (tracked) {
+			{
+				MessageToSend msg;
+				msg.eventID = EventIDs::kMouseUp;
+				msg.target = tracked.get();
+				messagesToSend.push_back(msg);
+			}
+
+			{
+				MessageToSend msg;
+				msg.eventID = _trackedMouseOutside ? EventIDs::kMouseUpOutside : EventIDs::kMouseUpInside;
+				msg.target = tracked.get();
+				messagesToSend.push_back(msg);
+			}
+
+			_mouseTrackingObject.reset();
+			_trackedMouseOutside = false;
+		}
+	}
+
+	DynamicValue mousePtValue;
+	mousePtValue.setPoint(Point16::create(_cachedMousePosition.x, _cachedMousePosition.y));
+
+	for (size_t ri = 0; ri < messagesToSend.size(); ri++) {
+		const MessageToSend &msg = messagesToSend[messagesToSend.size() - 1 - ri];
+		Common::SharedPtr<MessageProperties> props(new MessageProperties(Event::create(msg.eventID, 0), mousePtValue, nullptr));
+		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(props, msg.target, false, true, false));
+		sendMessageOnVThread(dispatch);
+	}
+
+	return kVThreadReturn;
+}
+
+
+VThreadState Runtime::updateMousePositionTask1(const UpdateMousePositionTaskData &data) {
+	if (!_project)
+		return kVThreadReturn;
+
+	// This intentionally replicates a mTropolis bug/quirk where drag motion modifiers DO NOT
+	// prevent "Mouse Outside" events from occurring if the mouse is moved fast enough to get
+	// outside of the object.  Also, note that "Mouse Outside" intentionally DOES NOT match up
+	// with the logic of "Mouse Up Inside" and "Mouse Up Outside" in updateMouseStateTask.
+	//
+	// e.g. if you drag an object under another object and then release the mouse, then
+	// what should happen is a Mouse Outside event is sent to the bottom object, and then
+	// a Mouse Up Inside event is sent when the button is released.
+
+	Structural *collisionItem = nullptr;
+	int bestLayer = INT_MIN;
+
+	for (size_t ri = 0; ri < _sceneStack.size(); ri++) {
+		const SceneStackEntry &sceneStackEntry = _sceneStack[_sceneStack.size() - 1 - ri];
+		recursiveFindMouseCollision(collisionItem, bestLayer, sceneStackEntry.scene.get(), data.x, data.y);
+		if (collisionItem)
+			break;
+	}
+
+	Common::SharedPtr<Structural> newMouseOver;
+	Common::SharedPtr<Structural> oldMouseOver = _mouseOverObject.lock();
+	if (collisionItem)
+		newMouseOver = collisionItem->getSelfReference().lock().staticCast<Structural>();
+
+	struct MessageToSend {
+		EventIDs::EventID eventID;
+		Structural *target;
+	};
+
+	Common::Array<MessageToSend> messagesToSend;
+
+	if (newMouseOver != oldMouseOver) {
+		if (oldMouseOver) {
+			MessageToSend msg;
+			msg.eventID = EventIDs::kMouseOutside;
+			msg.target = oldMouseOver.get();
+			messagesToSend.push_back(msg);
+		}
+		if (newMouseOver) {
+			MessageToSend msg;
+			msg.eventID = EventIDs::kMouseOver;
+			msg.target = newMouseOver.get();
+			messagesToSend.push_back(msg);
+		}
+
+		_mouseOverObject = newMouseOver;
+	}
+
+	Common::SharedPtr<Structural> tracked = _mouseTrackingObject.lock();
+
+
+	if (tracked) {
+		{
+			MessageToSend msg;
+			msg.eventID = EventIDs::kMouseTracking;
+			msg.target = tracked.get();
+			messagesToSend.push_back(msg);
+		}
+
+		assert(tracked->isElement());
+		Element *element = static_cast<Element *>(tracked.get());
+		assert(element->isVisual());
+		VisualElement *visual = static_cast<VisualElement *>(element);
+		Point16 parentOrigin = visual->getParentOrigin();
+		int32 relativeX = data.x - parentOrigin.x;
+		int32 relativeY = data.y - parentOrigin.y;
+		bool mouseOutside = !visual->isMouseInsideBox(relativeX, relativeY) || !visual->isMouseCollisionAtPoint(relativeX, relativeY);
+
+		if (mouseOutside != _trackedMouseOutside) {
+			if (mouseOutside) {
+				MessageToSend msg;
+				msg.eventID = EventIDs::kMouseTrackedOutside;
+				msg.target = tracked.get();
+				messagesToSend.push_back(msg);
+			} else {
+				MessageToSend msg;
+				msg.eventID = EventIDs::kMouseTrackedInside;
+				msg.target = tracked.get();
+				messagesToSend.push_back(msg);
+			}
+
+			_trackedMouseOutside = mouseOutside;
+		}
+	}
+
+	DynamicValue mousePtValue;
+	mousePtValue.setPoint(Point16::create(data.x, data.y));
+
+	for (size_t ri = 0; ri < messagesToSend.size(); ri++) {
+		const MessageToSend &msg = messagesToSend[messagesToSend.size() - 1 - ri];
+		Common::SharedPtr<MessageProperties> props(new MessageProperties(Event::create(msg.eventID, 0), mousePtValue, nullptr));
+		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(props, msg.target, false, true, false));
+		sendMessageOnVThread(dispatch);
+	}
+
+	_cachedMousePosition.x = data.x;
+	_cachedMousePosition.y = data.y;
+
+	return kVThreadReturn;
+}
+
 void Runtime::queueMessage(const Common::SharedPtr<MessageDispatch>& dispatch) {
 	_messageQueue.push_back(dispatch);
 }
+
+void Runtime::queueOSEvent(const Common::SharedPtr<OSEvent> &osEvent) {
+	_osEventQueue.push_back(osEvent);
+}
+
 Scheduler &Runtime::getScheduler() {
 	return _scheduler;
 }
@@ -3591,8 +4025,9 @@ void Runtime::onMouseUp(int32 x, int32 y, Actions::MouseButton mButton) {
 }
 
 void Runtime::onKeyboardEvent(const Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt) {
-	if (_project)
-		_project->onKeyboardEvent(this, evtType, repeat, keyEvt);
+	Common::SharedPtr<Window> focusWindow = _keyFocusWindow.lock();
+	if (focusWindow)
+		focusWindow->onKeyboardEvent(evtType, repeat, keyEvt);
 }
 
 Common::RandomSource* Runtime::getRandom() const {
@@ -3619,9 +4054,11 @@ void Runtime::ensureMainWindowExists() {
 
 		int32 centeredX = (static_cast<int32>(_displayWidth) - static_cast<int32>(presentationSettings.width)) / 2;
 		int32 centeredY = (static_cast<int32>(_displayHeight) - static_cast<int32>(presentationSettings.height)) / 2;
-		Common::SharedPtr<Window> mainWindow(new Window(WindowParameters(this, centeredX, centeredY, presentationSettings.width, presentationSettings.height, _displayModePixelFormats[_realDisplayMode])));
+		Common::SharedPtr<Window> mainWindow(new MainWindow(WindowParameters(this, centeredX, centeredY, presentationSettings.width, presentationSettings.height, _displayModePixelFormats[_realDisplayMode])));
 		addWindow(mainWindow);
 		_mainWindow.reset(mainWindow);
+
+		_keyFocusWindow = mainWindow;
 	}
 }
 
@@ -4669,7 +5106,7 @@ void Project::loadContextualObject(size_t streamIndex, ChildLoaderStack &stack,
 					// Visual elements can contain non-visual element children, but non-visual elements
 					// can only contain non-visual element children
 					loaderContext.containerUnion.filteredElements.filterFunc = element->isVisual() ? Data::DataObjectTypes::isElement : Data::DataObjectTypes::isNonVisualElement;
-					loaderContext.containerUnion.filteredElements.structural = container;
+					loaderContext.containerUnion.filteredElements.structural = element.get();
 					loaderContext.remainingCount = 0;
 					loaderContext.type = ChildLoaderContext::kTypeFilteredElements;
 
@@ -4793,6 +5230,14 @@ uint16 VisualElement::getLayer() const {
 	return _layer;
 }
 
+bool VisualElement::isMouseInsideBox(int32 relativeX, int32 relativeY) const {
+	return relativeX >= _rect.left && relativeX < _rect.right && relativeY >= _rect.top && relativeY < _rect.bottom;
+}
+
+bool VisualElement::isMouseCollisionAtPoint(int32 relativeX, int32 relativeY) const {
+	return true;
+}
+
 bool VisualElement::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
 	if (attrib == "visible") {
 		result.setBool(_visible);
@@ -4842,7 +5287,7 @@ const Rect16 &VisualElement::getRelativeRect() const {
 	return _rect;
 }
 
-Point16 VisualElement::getGlobalPosition() const {
+Point16 VisualElement::getParentOrigin() const {
 	Point16 pos = Point16::create(0, 0);
 	if (_parent && _parent->isElement()) {
 		Element *element = static_cast<Element *>(_parent);
@@ -4851,6 +5296,12 @@ Point16 VisualElement::getGlobalPosition() const {
 		}
 	}
 
+	return pos;
+}
+
+Point16 VisualElement::getGlobalPosition() const {
+	Point16 pos = getParentOrigin();
+
 	pos.x += _rect.left;
 	pos.y += _rect.top;
 
@@ -5035,6 +5486,10 @@ bool Modifier::isCompoundVariable() const {
 	return false;
 }
 
+bool Modifier::isKeyboardMessenger() const {
+	return false;
+}
+
 bool Modifier::isModifier() const {
 	return true;
 }
@@ -5088,6 +5543,22 @@ bool Modifier::loadPlugInHeader(const PlugInModifierLoaderContext &plugInContext
 	return true;
 }
 
+void Modifier::recursiveCollectObjectsMatchingCriteria(Common::Array<Common::WeakPtr<RuntimeObject> > &results, bool (*evalFunc)(void *userData, RuntimeObject *object), void *userData, bool onlyEnabled) {
+	if (evalFunc(userData, this))
+		results.push_back(getSelfReference());
+
+	IModifierContainer *childContainer = nullptr;
+	if (onlyEnabled)
+		childContainer = getMessagePropagationContainer();
+	else
+		childContainer = getChildContainer();
+
+	if (childContainer) {
+		for (const Common::SharedPtr<Modifier> &child : childContainer->getModifiers())
+			child->recursiveCollectObjectsMatchingCriteria(results, evalFunc, userData, onlyEnabled);
+	}
+}
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 
 SupportStatus Modifier::debugGetSupportStatus() const {
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 5b3748088db..480b5284071 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -64,6 +64,7 @@ class AssetManagerInterface;
 class CursorGraphic;
 class CursorGraphicCollection;
 class Element;
+class KeyboardInputEvent;
 class MessageDispatch;
 class MiniscriptThread;
 class Modifier;
@@ -1203,6 +1204,22 @@ private:
 	bool _isCommand;
 };
 
+class KeyEventDispatch {
+public:
+	explicit KeyEventDispatch(const Common::SharedPtr<KeyboardInputEvent> &evt);
+
+	Common::Array<Common::WeakPtr<RuntimeObject> > &getKeyboardMessengerArray();
+	static bool keyboardMessengerFilterFunc(void *userData, RuntimeObject *object);
+
+	bool isTerminated() const;
+	VThreadState continuePropagating(Runtime *runtime);
+	
+private:
+	Common::Array<Common::WeakPtr<RuntimeObject> > _keyboardMessengers;
+	size_t _dispatchIndex;
+	const Common::SharedPtr<KeyboardInputEvent> _evt;
+};
+
 class Scheduler;
 
 class ScheduledEvent : Common::NonCopyable {
@@ -1252,6 +1269,53 @@ private:
 	Common::Array<Common::SharedPtr<ScheduledEvent>> _events;
 };
 
+enum OSEventType {
+	kOSEventTypeMouseDown,
+	kOSEventTypeMouseUp,
+	kOSEventTypeMouseMove,
+
+	kOSEventTypeKeyboard,
+};
+
+class OSEvent {
+public:
+	explicit OSEvent(OSEventType eventType);
+	virtual ~OSEvent();
+
+	OSEventType getEventType() const;
+
+private:
+	OSEventType _eventType;
+};
+
+class MouseInputEvent : public OSEvent {
+public:
+	explicit MouseInputEvent(OSEventType eventType, int32 x, int32 y, Actions::MouseButton button);
+
+	int32 getX() const;
+	int32 getY() const;
+	Actions::MouseButton getButton() const;
+
+private:
+	int32 _x;
+	int32 _y;
+	Actions::MouseButton _button;
+};
+
+class KeyboardInputEvent : public OSEvent {
+public:
+	explicit KeyboardInputEvent(OSEventType osEventType, Common::EventType keyEventType, bool repeat, const Common::KeyState &keyEvt);
+
+	Common::EventType getKeyEventType() const;
+	bool isRepeat() const;
+	const Common::KeyState &getKeyState() const;
+
+private:
+	Common::EventType _keyEventType;
+	bool _repeat;
+	const Common::KeyState _keyEvt;
+};
+
 class Runtime {
 public:
 	explicit Runtime(OSystem *system);
@@ -1309,6 +1373,8 @@ public:
 	void sendMessageOnVThread(const Common::SharedPtr<MessageDispatch> &dispatch);
 	void queueMessage(const Common::SharedPtr<MessageDispatch> &dispatch);
 
+	void queueOSEvent(const Common::SharedPtr<OSEvent> &osEvent);
+
 	Scheduler &getScheduler();
 
 	void getScenesInRenderOrder(Common::Array<Structural *> &scenes) const;
@@ -1367,6 +1433,10 @@ private:
 		Common::SharedPtr<MessageDispatch> dispatch;
 	};
 
+	struct DispatchKeyTaskData {
+		Common::SharedPtr<KeyEventDispatch> dispatch;
+	};
+
 	struct ConsumeMessageTaskData {
 		IMessageConsumer *consumer;
 		Common::SharedPtr<MessageProperties> message;
@@ -1377,6 +1447,15 @@ private:
 		Common::SharedPtr<MessageProperties> message;
 	};
 
+	struct UpdateMouseStateTaskData {
+		bool mouseDown;
+	};
+
+	struct UpdateMousePositionTaskData {
+		int32 x;
+		int32 y;
+	};
+
 	static Common::SharedPtr<Structural> findDefaultSharedSceneForScene(Structural *scene);
 	void executeTeardown(const Teardown &teardown);
 	void executeLowLevelSceneStateTransition(const LowLevelSceneStateTransitionAction &transitionAction);
@@ -1387,6 +1466,10 @@ private:
 	void recursiveDeactivateStructural(Structural *structural);
 	void recursiveActivateStructural(Structural *structural);
 
+	static bool isStructuralMouseInteractive(Structural *structural);
+	static bool isModifierMouseInteractive(Modifier *modifier);
+	static void recursiveFindMouseCollision(Structural *&bestResult, int &bestLayer, Structural *candidate, int32 relativeX, int32 relativeY);
+
 	void queueEventAsLowLevelSceneStateTransitionAction(const Event &evt, Structural *root, bool cascade, bool relay);
 
 	void loadScene(const Common::SharedPtr<Structural> &scene);
@@ -1397,14 +1480,19 @@ private:
 	void refreshPlayTime();	// Updates play time to be in sync with the system clock.  Used so that events occurring after storage access don't skip.
 
 	VThreadState dispatchMessageTask(const DispatchMethodTaskData &data);
+	VThreadState dispatchKeyTask(const DispatchKeyTaskData &data);
 	VThreadState consumeMessageTask(const ConsumeMessageTaskData &data);
 	VThreadState consumeCommandTask(const ConsumeCommandTaskData &data);
+	VThreadState updateMouseStateTask(const UpdateMouseStateTaskData &data);
+	VThreadState updateMousePositionTask1(const UpdateMousePositionTaskData &data);
+	VThreadState updateMousePositionTask2(const UpdateMousePositionTaskData &data);
 
 	Common::Array<VolumeState> _volumes;
 	Common::SharedPtr<ProjectDescription> _queuedProjectDesc;
 	Common::SharedPtr<Project> _project;
 	Common::ScopedPtr<VThread> _vthread;
 	Common::Array<Common::SharedPtr<MessageDispatch> > _messageQueue;
+	Common::Array<Common::SharedPtr<OSEvent> > _osEventQueue;
 	ObjectLinkingScope _rootLinkingScope;
 
 	Common::Array<Teardown> _pendingTeardowns;
@@ -1448,6 +1536,7 @@ private:
 	Common::SharedPtr<Graphics::Cursor> _defaultCursor;
 
 	Common::WeakPtr<Window> _mouseFocusWindow;
+	Common::WeakPtr<Window> _keyFocusWindow;
 	bool _mouseFocusFlags[Actions::kMouseButtonCount];
 
 	ProjectPlatform _platform;
@@ -1456,6 +1545,17 @@ private:
 	Common::SharedPtr<WorldManagerInterface> _worldManagerInterface;
 	Common::SharedPtr<AssetManagerInterface> _assetManagerInterface;
 
+	Point16 _cachedMousePosition;
+
+	// Mouse control is tracked in two ways: Mouse over is detected with mouse movement AND when
+	// "refreshCursor" is set on the world manager, it indicates the frontmost object that
+	// responds to any mouse event.  The mouse tracking object is the object that was clicked.
+	// These can differ if the user holds down the mouse and moves it to a spot where the tracked
+	// object is either not clickable, or is behind another object with mouse collision.
+	Common::WeakPtr<Structural> _mouseOverObject;
+	Common::WeakPtr<Structural> _mouseTrackingObject;
+	bool _trackedMouseOutside;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	Common::SharedPtr<Debugger> _debugger;
 #endif
@@ -1615,6 +1715,8 @@ public:
 	virtual void activate();
 	virtual void deactivate();
 
+	void recursiveCollectObjectsMatchingCriteria(Common::Array<Common::WeakPtr<RuntimeObject> > &results, bool (*evalFunc)(void *userData, RuntimeObject *object), void *userData, bool onlyEnabled);
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	SupportStatus debugGetSupportStatus() const override;
 	const Common::String &debugGetName() const override;
@@ -1955,9 +2057,15 @@ public:
 	bool isDirectToScreen() const;
 	uint16 getLayer() const;
 
+	bool isMouseInsideBox(int32 relativeX, int32 relativeY) const;
+
+	// Returns true if there is mouse collision at a specified point, assuming it has already passed isMouseInsideBox
+	virtual bool isMouseCollisionAtPoint(int32 relativeX, int32 relativeY) const;
+
 	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
 	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) override;
 
+	Point16 getParentOrigin() const;
 	Point16 getGlobalPosition() const;
 	const Rect16 &getRelativeRect() const;
 
@@ -2019,6 +2127,8 @@ public:
 	virtual bool isVariable() const;
 	virtual bool isBehavior() const;
 	virtual bool isCompoundVariable() const;
+	virtual bool isKeyboardMessenger() const;
+
 	bool isModifier() const override;
 
 	// This should only return a propagation container if messages should actually be propagated (i.e. NOT switched-off behaviors!)
@@ -2046,6 +2156,8 @@ public:
 
 	bool loadPlugInHeader(const PlugInModifierLoaderContext &plugInContext);
 
+	void recursiveCollectObjectsMatchingCriteria(Common::Array<Common::WeakPtr<RuntimeObject> > &results, bool (*evalFunc)(void *userData, RuntimeObject *object), void *userData, bool onlyEnabled);
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	SupportStatus debugGetSupportStatus() const override;
 	const Common::String &debugGetName() const override;


Commit: 49f7dc0c2fbf38f02b19ca31db15535150a39d80
    https://github.com/scummvm/scummvm/commit/49f7dc0c2fbf38f02b19ca31db15535150a39d80
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix some wrong IDs, debugging menu MIDI not working

Changed paths:
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 10b1d5f9a63..ba798a45b51 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -570,6 +570,7 @@ VThreadState MidiModifier::consumeMessage(Runtime *runtime, const Common::Shared
 	if (_executeWhen.respondsTo(msg->getEvent())) {
 		if (_mode == kModeFile) {
 			if (_embeddedFile) {
+				debug(2, "MIDI (%p): Playing embedded file", this);
 				_plugIn->getMidi()->playFile(&_embeddedFile->contents[0], _embeddedFile->contents.size());
 			}
 		}
@@ -619,8 +620,11 @@ MiniscriptInstructionOutcome MidiModifier::scriptSetVolume(MiniscriptThread *thr
 
 	_volume = asInteger;
 
-	if (_mode == kModeFile)
-		_plugIn->getMidi()->setVolume((_volume * 1306) >> 9);	// 100 -> 255 range
+	if (_mode == kModeFile) {
+		const int normalizedVolume = (_volume * 1306) >> 9;
+		debug(2, "MIDI (%p): Changing volume to %i", this, normalizedVolume);
+		_plugIn->getMidi()->setVolume(normalizedVolume); // 100 -> 255 range
+	}
 
 	return kMiniscriptInstructionOutcomeContinue;
 }
@@ -635,8 +639,10 @@ MiniscriptInstructionOutcome MidiModifier::scriptSetNoteVelocity(MiniscriptThrea
 	else if (asInteger > 127)
 		asInteger = 127;
 
-	if (_mode == kModeSingleNote)
+	if (_mode == kModeSingleNote) {
+		debug(2, "MIDI (%p): Changing note velocity to %i", this, asInteger);
 		_modeSpecific.singleNote.velocity = asInteger;
+	}
 
 	return kMiniscriptInstructionOutcomeContinue;
 }
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 7d680d52993..d637246966d 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -1654,8 +1654,12 @@ void MessengerSendSpec::resolveDestination(Runtime *runtime, Modifier *sender, C
 		case kMessageDestModifiersParent:
 			resolveVariableObjectType(sender->getParent().lock().get(), outStructuralDest, outModifierDest);
 			break;
-		case kMessageDestChildren:
 		case kMessageDestElementsParent:
+			resolveHierarchyStructuralDestination(runtime, sender, outStructuralDest, outModifierDest, isElementFilter);
+			if (!outStructuralDest.expired())
+				outStructuralDest = outStructuralDest.lock()->getParent()->getSelfReference().staticCast<Structural>();
+			break;
+		case kMessageDestChildren:
 		case kMessageDestSubsection:
 		case kMessageDestSourcesParent:
 		case kMessageDestBehavior:
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 480b5284071..9b7835e796a 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -205,13 +205,13 @@ enum EventID {
 	kTransitionStarted = 503,
 	kTransitionEnded = 504,
 
-	kMouseUp = 301,
-	kMouseDown = 302,
+	kMouseDown = 301,
+	kMouseUp = 302,
 	kMouseOver = 303,
 	kMouseOutside = 304,
 	kMouseTrackedInside = 305,
-	kMouseTracking = 306,
-	kMouseTrackedOutside = 307,
+	kMouseTrackedOutside = 306,
+	kMouseTracking = 307,
 	kMouseUpInside = 309,
 	kMouseUpOutside = 310,
 


Commit: 69459bd4708b8d398e150272735850c7da4380f1
    https://github.com/scummvm/scummvm/commit/69459bd4708b8d398e150272735850c7da4380f1
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Script fixes

Changed paths:
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 291cd0d6c56..caf9e4889c8 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -495,7 +495,7 @@ MiniscriptInstructionOutcome Set::execute(MiniscriptThread *thread) const {
 		const DynamicValueWriteProxyPOD &proxy = target.value.getWriteProxyPOD();
 		outcome = proxy.ifc->write(thread, srcValue.value, proxy.objectRef, proxy.ptrOrOffset);
 		if (outcome == kMiniscriptInstructionOutcomeFailed) {
-			thread->error("Failed to assign value");
+			thread->error("Failed to assign value to proxy");
 			return outcome;
 		}
 	} else {
@@ -1330,13 +1330,23 @@ MiniscriptInstructionOutcome GetChild::execute(MiniscriptThread *thread) const {
 
 MiniscriptInstructionOutcome GetChild::readRValueAttrib(MiniscriptThread *thread, DynamicValue &valueSrcDest, const Common::String &attrib) const {
 	switch (valueSrcDest.getType()) {
+	case DynamicValueTypes::kPoint:
+		if (attrib == "x")
+			valueSrcDest.setInt(valueSrcDest.getPoint().x);
+		else if (attrib == "y")
+			valueSrcDest.setInt(valueSrcDest.getPoint().y);
+		else {
+			thread->error("Point has no attribute '" + attrib + "'");
+			return kMiniscriptInstructionOutcomeFailed;
+		}
+		break;
 	case DynamicValueTypes::kIntegerRange:
 		if (attrib == "start")
 			valueSrcDest.setInt(valueSrcDest.getIntRange().min);
 		else if (attrib == "end")
 			valueSrcDest.setInt(valueSrcDest.getIntRange().max);
 		else {
-			thread->error(Common::String("Integer range has no attribute '") + attrib + "'");
+			thread->error("Integer range has no attribute '" + attrib + "'");
 			return kMiniscriptInstructionOutcomeFailed;
 		}
 		break;
@@ -1347,21 +1357,31 @@ MiniscriptInstructionOutcome GetChild::readRValueAttrib(MiniscriptThread *thread
 		else if (attrib == "magnitude")
 			valueSrcDest.setInt(valueSrcDest.getVector().magnitude);
 		else {
-			thread->error(Common::String("Vector has no attribute '") + attrib + "'");
+			thread->error("Vector has no attribute '" + attrib + "'");
 			return kMiniscriptInstructionOutcomeFailed;
 		}
 		break;
 	case DynamicValueTypes::kObject: {
 			Common::SharedPtr<RuntimeObject> obj = valueSrcDest.getObject().object.lock();
 			if (!obj) {
-				thread->error("Unable to read attribute '" + attrib + "' from invalid object");
+				thread->error("Unable to read object attribute '" + attrib + "' from invalid object");
 				return kMiniscriptInstructionOutcomeFailed;
 			} else if (!obj->readAttribute(thread, valueSrcDest, attrib)) {
-				thread->error("Unable to read attribute '" + attrib + "'");
+				thread->error("Unable to read object attribute '" + attrib + "'");
+				return kMiniscriptInstructionOutcomeFailed;
+			}
+		} break;
+	case DynamicValueTypes::kList: {
+			Common::SharedPtr<DynamicList> list = valueSrcDest.getList();
+			if (attrib == "count") {
+				valueSrcDest.setInt(list->getSize());
+			} else {
+				thread->error("Unable to read list attribute '" + attrib + "'");
 				return kMiniscriptInstructionOutcomeFailed;
 			}
 		} break;
 	default:
+		thread->error("Unable to read attribute '" + attrib + "' from rvalue");
 		return kMiniscriptInstructionOutcomeFailed;
 	}
 
@@ -1537,7 +1557,7 @@ MiniscriptInstructionOutcome PushGlobal::execute(MiniscriptThread *thread) const
 		value.setObject(ObjectReference(thread->getMessageProperties()->getSource()));
 		break;
 	case kGlobalRefMouse:
-		thread->error("'mouse' global ref not yet implemented");
+		value.setPoint(thread->getRuntime()->getCachedMousePosition());
 		return kMiniscriptInstructionOutcomeFailed;
 	case kGlobalRefTicks:
 		value.setInt(thread->getRuntime()->getPlayTime() * 60 / 1000);
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 4a5b423e19f..df05a05011b 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -996,11 +996,8 @@ void CompoundVariableModifier::visitInternalReferences(IStructuralReferenceVisit
 bool CompoundVariableModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
 	Modifier *var = findChildByName(attrib);
 	if (var) {
-		if (var->isVariable()) {
-			static_cast<VariableModifier *>(var)->varGetValue(thread, result);
-		} else {
-			result.setObject(var->getSelfReference());
-		}
+		// Shouldn't dereference the value here, some scripts (e.g. "<go dest> on MUI" in Obsidian) depend on it not being dereferenced
+		result.setObject(var->getSelfReference());
 		return true;
 	}
 	return Modifier::readAttribute(thread, result, attrib);
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index ba798a45b51..5fd9cb8e52a 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -292,9 +292,9 @@ bool ObjectReferenceVariableModifier::varSetValue(MiniscriptThread *thread, cons
 	switch (value.getType()) {
 	case DynamicValueTypes::kNull:
 	case DynamicValueTypes::kObject:
-		return scriptSetObject(thread, value);
+		return scriptSetObject(thread, value) == kMiniscriptInstructionOutcomeContinue;
 	case DynamicValueTypes::kString:
-		return scriptSetPath(thread, value);
+		return scriptSetPath(thread, value) == kMiniscriptInstructionOutcomeContinue;
 	default:
 		return false;
 	}
@@ -723,12 +723,21 @@ void ListVariableModifier::varGetValue(MiniscriptThread *thread, DynamicValue &d
 	dest.setList(_list);
 }
 
+bool ListVariableModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "count") {
+		result.setInt(_list->getSize());
+		return true;
+	}
+
+	return Modifier::readAttribute(thread, result, attrib);
+}
+
 bool ListVariableModifier::readAttributeIndexed(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib, const DynamicValue &index) {
 	if (attrib == "value") {
 		size_t realIndex = 0;
 		return _list->dynamicValueToIndex(realIndex, index) && _list->getAtIndex(realIndex, result);
 	}
-	return false;
+	return Modifier::readAttributeIndexed(thread, result, attrib, index);
 }
 
 MiniscriptInstructionOutcome ListVariableModifier::writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib, const DynamicValue &index) {
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index b38b0dc95a3..6200677c1b4 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -218,6 +218,7 @@ public:
 	bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override;
 	void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override;
 
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
 	bool readAttributeIndexed(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib, const DynamicValue &index) override;
 	MiniscriptInstructionOutcome writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib, const DynamicValue &index) override;
 
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index d637246966d..5f9b86ea473 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -2305,6 +2305,31 @@ bool Structural::readAttribute(MiniscriptThread *thread, DynamicValue &result, c
 		} else
 			result.clear();
 		return true;
+	} else if (attrib == "scene") {
+		result.clear();
+
+		// Scene returns the scene of the Miniscript modifier, even though it's looked up
+		// as if it's an element property, because it's treated like a keyword.
+		RuntimeObject *possibleScene = thread->getModifier();
+		while (possibleScene) {
+			if (possibleScene->isModifier()) {
+				possibleScene = static_cast<Modifier *>(possibleScene)->getParent().lock().get();
+				continue;
+			}
+
+			if (possibleScene->isStructural()) {
+				Structural *parent = static_cast<Structural *>(possibleScene)->getParent();
+				if (parent->isSubsection())
+					break;
+				else
+					possibleScene = parent;
+			}
+		}
+		if (possibleScene)
+			result.setObject(possibleScene->getSelfReference());
+		else
+			result.clear();
+		return true;
 	}
 
 	return RuntimeObject::readAttribute(thread, result, attrib);
@@ -4034,7 +4059,11 @@ void Runtime::onKeyboardEvent(const Common::EventType evtType, bool repeat, cons
 		focusWindow->onKeyboardEvent(evtType, repeat, keyEvt);
 }
 
-Common::RandomSource* Runtime::getRandom() const {
+const Point16 &Runtime::getCachedMousePosition() const {
+	return _cachedMousePosition;
+}
+
+Common::RandomSource *Runtime::getRandom() const {
 	return _random.get();
 }
 
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 9b7835e796a..5aea197819f 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -1392,6 +1392,8 @@ public:
 	void onMouseUp(int32 x, int32 y, Actions::MouseButton mButton);
 	void onKeyboardEvent(const Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt);
 
+	const Point16 &getCachedMousePosition() const;
+
 	Common::RandomSource *getRandom() const;
 	WorldManagerInterface *getWorldManagerInterface() const;
 	AssetManagerInterface *getAssetManagerInterface() const;


Commit: 957037b6b6fdc0fb238c41649a17859eb7c60b3d
    https://github.com/scummvm/scummvm/commit/957037b6b6fdc0fb238c41649a17859eb7c60b3d
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Auto-start movies that aren't paused

Changed paths:
    engines/mtropolis/elements.cpp
    engines/mtropolis/plugin/standard.cpp


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 5c0f1cae32f..584996b3f72 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -130,6 +130,11 @@ void MovieElement::activate() {
 
 	_unloadSignaller = project->notifyOnSegmentUnload(segmentIndex, this);
 	_postRenderSignaller = project->notifyOnPostRender(this);
+
+	if (!_paused && _visible) {
+		StartPlayingTaskData *startPlayingTaskData = _runtime->getVThread().pushTask("MovieElement::startPlayingTask", this, &MovieElement::startPlayingTask);
+		startPlayingTaskData->runtime = _runtime;
+	}
 }
 
 void MovieElement::deactivate() {
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 5fd9cb8e52a..f0a43d8e16d 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -570,8 +570,10 @@ VThreadState MidiModifier::consumeMessage(Runtime *runtime, const Common::Shared
 	if (_executeWhen.respondsTo(msg->getEvent())) {
 		if (_mode == kModeFile) {
 			if (_embeddedFile) {
-				debug(2, "MIDI (%p): Playing embedded file", this);
+				debug(2, "MIDI (%x '%s'): Playing embedded file", getStaticGUID(), getName().c_str());
 				_plugIn->getMidi()->playFile(&_embeddedFile->contents[0], _embeddedFile->contents.size());
+			} else {
+				debug(2, "MIDI (%x '%s'): Digested execute event but don't have anything to play", getStaticGUID(), getName().c_str());
 			}
 		}
 	}
@@ -622,7 +624,7 @@ MiniscriptInstructionOutcome MidiModifier::scriptSetVolume(MiniscriptThread *thr
 
 	if (_mode == kModeFile) {
 		const int normalizedVolume = (_volume * 1306) >> 9;
-		debug(2, "MIDI (%p): Changing volume to %i", this, normalizedVolume);
+		debug(2, "MIDI (%x '%s'): Changing volume to %i", getStaticGUID(), getName().c_str(), normalizedVolume);
 		_plugIn->getMidi()->setVolume(normalizedVolume); // 100 -> 255 range
 	}
 
@@ -640,7 +642,7 @@ MiniscriptInstructionOutcome MidiModifier::scriptSetNoteVelocity(MiniscriptThrea
 		asInteger = 127;
 
 	if (_mode == kModeSingleNote) {
-		debug(2, "MIDI (%p): Changing note velocity to %i", this, asInteger);
+		debug(2, "MIDI (%x '%s'): Changing note velocity to %i", getStaticGUID(), getName().c_str(), asInteger);
 		_modeSpecific.singleNote.velocity = asInteger;
 	}
 


Commit: fd57b238909a699def6a8d9a17de7a7b2c0d95fa
    https://github.com/scummvm/scummvm/commit/fd57b238909a699def6a8d9a17de7a7b2c0d95fa
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix up some script things, get music in intro credits working (partially)

Changed paths:
  A engines/mtropolis/notes/notes.txt
  A engines/mtropolis/notes/obsidian.txt
  R engines/mtropolis/notes.txt
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 584996b3f72..cd2b8a4b4e9 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -53,7 +53,7 @@ void GraphicElement::render(Window *window) {
 
 MovieElement::MovieElement()
 	: _cacheBitmap(false), _reversed(false), _haveFiredAtFirstCel(false), _haveFiredAtLastCel(false)
-	, _loop(false), _alternate(false), _playEveryFrame(false), _assetID(0), _runtime(nullptr) {
+	, _alternate(false), _playEveryFrame(false), _assetID(0), _runtime(nullptr) {
 }
 
 MovieElement::~MovieElement() {
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index a78fb8bee96..37344a82887 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -85,7 +85,6 @@ private:
 	VThreadState startPlayingTask(const StartPlayingTaskData &taskData);
 
 	bool _cacheBitmap;
-	bool _loop;
 	bool _alternate;
 	bool _playEveryFrame;
 	bool _reversed;
@@ -148,7 +147,6 @@ public:
 
 private:
 	bool _cacheBitmap;
-	bool _loop;
 
 	// If set, then carry over residual frame time and display at the desired rate.  If not set, reset residual each frame for smoother animation.
 	bool _maintainRate;
@@ -225,7 +223,6 @@ private:
 	uint16 _rightVolume;
 	int16 _balance;
 	uint32 _assetID;
-	bool _loop;
 
 	Runtime *_runtime;
 };
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index caf9e4889c8..1c670384891 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -602,38 +602,55 @@ MiniscriptInstructionOutcome BinaryArithInstruction::execute(MiniscriptThread *t
 	DynamicValue &rs = thread->getStackValueFromTop(0).value;
 	DynamicValue &lsDest = thread->getStackValueFromTop(1).value;
 
-	double leftVal = 0.0;
-	switch (lsDest.getType()) {
-	case DynamicValueTypes::kInteger:
-		leftVal = lsDest.getInt();
-		break;
-	case DynamicValueTypes::kFloat:
-		leftVal = lsDest.getFloat();
-		break;
-	default:
-		thread->error("Invalid left-side type for binary arithmetic operator");
-		return kMiniscriptInstructionOutcomeFailed;
-	}
+	if (lsDest.getType() == DynamicValueTypes::kPoint && rs.getType() == DynamicValueTypes::kPoint) {
+		Point16 lsPoint = lsDest.getPoint();
+		Point16 rsPoint = rs.getPoint();
 
-	double rightVal = 0.0;
-	switch (rs.getType()) {
-	case DynamicValueTypes::kInteger:
-		rightVal = rs.getInt();
-		break;
-	case DynamicValueTypes::kFloat:
-		rightVal = rs.getFloat();
-		break;
-	default:
-		thread->error("Invalid right-side type for binary arithmetic operator");
-		return kMiniscriptInstructionOutcomeFailed;
-	}
+		double resultX = 0.0;
+		double resultY = 0.0;
+		outcome = arithExecute(thread, resultX, lsPoint.x, rsPoint.x);
+		if (outcome != kMiniscriptInstructionOutcomeContinue)
+			return outcome;
 
-	double result = 0.0;
-	outcome = arithExecute(thread, result, leftVal, rightVal);
-	if (outcome != kMiniscriptInstructionOutcomeContinue)
-		return outcome;
+		outcome = arithExecute(thread, resultY, lsPoint.y, rsPoint.y);
+		if (outcome != kMiniscriptInstructionOutcomeContinue)
+			return outcome;
+
+		lsDest.setPoint(Point16::create(static_cast<int16>(round(resultX)), static_cast<int16>(round(resultY))));
+	} else {
+		double leftVal = 0.0;
+		switch (lsDest.getType()) {
+		case DynamicValueTypes::kInteger:
+			leftVal = lsDest.getInt();
+			break;
+		case DynamicValueTypes::kFloat:
+			leftVal = lsDest.getFloat();
+			break;
+		default:
+			thread->error("Invalid left-side type for binary arithmetic operator");
+			return kMiniscriptInstructionOutcomeFailed;
+		}
+
+		double rightVal = 0.0;
+		switch (rs.getType()) {
+		case DynamicValueTypes::kInteger:
+			rightVal = rs.getInt();
+			break;
+		case DynamicValueTypes::kFloat:
+			rightVal = rs.getFloat();
+			break;
+		default:
+			thread->error("Invalid right-side type for binary arithmetic operator");
+			return kMiniscriptInstructionOutcomeFailed;
+		}
 
-	lsDest.setFloat(result);
+		double result = 0.0;
+		outcome = arithExecute(thread, result, leftVal, rightVal);
+		if (outcome != kMiniscriptInstructionOutcomeContinue)
+			return outcome;
+
+		lsDest.setFloat(result);
+	}
 
 	thread->popValues(1);
 
@@ -1353,9 +1370,9 @@ MiniscriptInstructionOutcome GetChild::readRValueAttrib(MiniscriptThread *thread
 
 	case DynamicValueTypes::kVector:
 		if (attrib == "angle")
-			valueSrcDest.setInt(valueSrcDest.getVector().angleRadians * (180.0 / M_PI));
+			valueSrcDest.setFloat(valueSrcDest.getVector().angleRadians * (180.0 / M_PI));
 		else if (attrib == "magnitude")
-			valueSrcDest.setInt(valueSrcDest.getVector().magnitude);
+			valueSrcDest.setFloat(valueSrcDest.getVector().magnitude);
 		else {
 			thread->error("Vector has no attribute '" + attrib + "'");
 			return kMiniscriptInstructionOutcomeFailed;
@@ -1404,7 +1421,18 @@ MiniscriptInstructionOutcome GetChild::readRValueAttribIndexed(MiniscriptThread
 			return kMiniscriptInstructionOutcomeFailed;
 		}
 		break;
+	case DynamicValueTypes::kObject: {
+			Common::SharedPtr<RuntimeObject> obj = valueSrcDest.getObject().object.lock();
+			if (!obj) {
+				thread->error("Unable to read object indexed attribute '" + attrib + "' from invalid object");
+				return kMiniscriptInstructionOutcomeFailed;
+			} else if (!obj->readAttributeIndexed(thread, valueSrcDest, attrib, index)) {
+				thread->error("Unable to read object indexed attribute '" + attrib + "'");
+				return kMiniscriptInstructionOutcomeFailed;
+			}
+		} break;
 	default:
+		thread->error("Unable to read indexed rvalue attribute '" + attrib + "'");
 		return kMiniscriptInstructionOutcomeFailed;
 	}
 
@@ -1500,7 +1528,7 @@ MiniscriptInstructionOutcome ListAppend::execute(MiniscriptThread *thread) const
 
 	thread->popValues(1);
 
-	return kMiniscriptInstructionOutcomeFailed;
+	return kMiniscriptInstructionOutcomeContinue;
 }
 
 MiniscriptInstructionOutcome PushValue::execute(MiniscriptThread *thread) const {
@@ -1558,7 +1586,7 @@ MiniscriptInstructionOutcome PushGlobal::execute(MiniscriptThread *thread) const
 		break;
 	case kGlobalRefMouse:
 		value.setPoint(thread->getRuntime()->getCachedMousePosition());
-		return kMiniscriptInstructionOutcomeFailed;
+		break;
 	case kGlobalRefTicks:
 		value.setInt(thread->getRuntime()->getPlayTime() * 60 / 1000);
 		break;
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index df05a05011b..42169605c79 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -593,6 +593,7 @@ VThreadState TimerMessengerModifier::consumeMessage(Runtime *runtime, const Comm
 		if (_scheduledEvent)
 			_scheduledEvent->cancel();
 	} else if (_executeWhen.respondsTo(msg->getEvent())) {
+		debug(3, "Timer %x '%s' scheduled to execute in %i milliseconds", getStaticGUID(), getName().c_str(), _milliseconds);
 		if (!_scheduledEvent) {
 			_scheduledEvent = runtime->getScheduler().scheduleMethod<TimerMessengerModifier, &TimerMessengerModifier::activate>(runtime->getPlayTime() + _milliseconds, this);
 		}
@@ -617,6 +618,7 @@ Common::SharedPtr<Modifier> TimerMessengerModifier::shallowClone() const {
 }
 
 void TimerMessengerModifier::activate(Runtime *runtime) {
+	debug(3, "Timer %x '%s' triggered", getStaticGUID(), getName().c_str());
 	if (_looping)
 		_scheduledEvent = runtime->getScheduler().scheduleMethod<TimerMessengerModifier, &TimerMessengerModifier::activate>(runtime->getPlayTime() + _milliseconds, this);
 	else
@@ -1152,6 +1154,31 @@ void VectorVariableModifier::varGetValue(MiniscriptThread *thread, DynamicValue
 	dest.setVector(_vector);
 }
 
+bool VectorVariableModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "magnitude") {
+		result.setFloat(_vector.magnitude);
+		return true;
+	} else if (attrib == "angle") {
+		_vector.scriptGetAngleDegrees(result);
+		return true;
+	}
+
+	return VariableModifier::readAttribute(thread, result, attrib);
+}
+
+MiniscriptInstructionOutcome VectorVariableModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
+	if (attrib == "magnitude") {
+		DynamicValueWriteFloatHelper<double>::create(&_vector.magnitude, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "angle") {
+		DynamicValueWriteFuncHelper<AngleMagVector, &AngleMagVector::scriptSetAngleDegrees>::create(&_vector, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	return writeRefAttribute(thread, result, attrib);
+}
+
+
 Common::SharedPtr<Modifier> VectorVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new VectorVariableModifier(*this));
 }
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 2abe541a222..82c2a2ce6e9 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -689,6 +689,9 @@ public:
 	bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override;
 	void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override;
 
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib);
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Vector Variable Modifier"; }
 	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
diff --git a/engines/mtropolis/notes.txt b/engines/mtropolis/notes/notes.txt
similarity index 100%
rename from engines/mtropolis/notes.txt
rename to engines/mtropolis/notes/notes.txt
diff --git a/engines/mtropolis/notes/obsidian.txt b/engines/mtropolis/notes/obsidian.txt
new file mode 100644
index 00000000000..42a2964564a
--- /dev/null
+++ b/engines/mtropolis/notes/obsidian.txt
@@ -0,0 +1,3 @@
+MIDI behavior:
+
+Uses a special MIDI file "ANO&Vol.mid" which triggers AllNoteOff on all channels.
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 5f9b86ea473..e5cbc021a04 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -821,7 +821,7 @@ bool DynamicList::dynamicValueToIndex(size_t &outIndex, const DynamicValue &valu
 		int32 i = value.getInt();
 		if (i < 1)
 			return false;
-		static_cast<size_t>(i - 1);
+		outIndex = static_cast<size_t>(i - 1);
 	}
 	return true;
 }
@@ -869,6 +869,7 @@ Common::SharedPtr<DynamicList> DynamicList::clone() const {
 
 	if (_container)
 		clonedList->_container = _container->clone();
+	clonedList->_type = _type;
 
 	return clonedList;
 }
@@ -2219,7 +2220,7 @@ MiniscriptInstructionOutcome SystemInterface::setVolumeName(MiniscriptThread *th
 	return kMiniscriptInstructionOutcomeContinue;
 }
 
-Structural::Structural() : _parent(nullptr), _paused(false) {
+Structural::Structural() : _parent(nullptr), _paused(false), _loop(false) {
 }
 
 Structural::~Structural() {
@@ -2243,6 +2244,9 @@ bool Structural::readAttribute(MiniscriptThread *thread, DynamicValue &result, c
 	} else if (attrib == "paused") {
 		result.setBool(_paused);
 		return true;
+	} else if (attrib == "loop") {
+		result.setBool(_paused);
+		return true;
 	} else if (attrib == "this") {
 		// Yes, "this" is an attribute
 		result.setObject(thread->getModifier()->getSelfReference());
@@ -2364,6 +2368,9 @@ MiniscriptInstructionOutcome Structural::writeRefAttribute(MiniscriptThread *thr
 		} else {
 			return kMiniscriptInstructionOutcomeFailed;
 		}
+	} else if (attrib == "loop") {
+		DynamicValueWriteFuncHelper<Structural, &Structural::scriptSetLoop>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
 	}
 
 	return RuntimeObject::writeRefAttribute(thread, result, attrib);
@@ -2565,6 +2572,15 @@ MiniscriptInstructionOutcome Structural::scriptSetPaused(MiniscriptThread *threa
 	return kMiniscriptInstructionOutcomeYieldToVThreadNoRetry;
 }
 
+MiniscriptInstructionOutcome Structural::scriptSetLoop(MiniscriptThread *thread, const DynamicValue &value) {
+	if (value.getType() != DynamicValueTypes::kBoolean)
+		return kMiniscriptInstructionOutcomeFailed;
+
+	_loop = value.getBool();
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 ObjectLinkingScope::ObjectLinkingScope() : _parent(nullptr) {
 }
 
@@ -2746,8 +2762,10 @@ VThreadState MessageDispatch::continuePropagating(Runtime *runtime) {
 				}
 
 				// Post to the message action itself to VThread
-				if (responds)
+				if (responds) {
+					debug(3, "Modifier %x '%s' consumed message (%i,%i)", modifier->getStaticGUID(), modifier->getName().c_str(), _msg->getEvent().eventType, _msg->getEvent().eventInfo);
 					runtime->postConsumeMessageTask(modifier, _msg);
+				}
 
 				return kVThreadReturn;
 			} break;
@@ -2835,6 +2853,38 @@ VThreadState MessageDispatch::continuePropagating(Runtime *runtime) {
 	return kVThreadReturn;
 }
 
+const Common::SharedPtr<MessageProperties> &MessageDispatch::getMsg() const {
+	return _msg;
+}
+
+bool MessageDispatch::isCascade() const {
+	return _cascade;
+}
+
+bool MessageDispatch::isRelay() const {
+	return _relay;
+}
+
+
+RuntimeObject *MessageDispatch::getRootPropagator() const {
+	if (_propagationStack.size() > 0) {
+		const PropagationStack &lowest = _propagationStack[0];
+		switch (lowest.propagationStage) {
+		case PropagationStack::kStageSendToModifier:
+			return lowest.ptr.modifier;
+		case PropagationStack::kStageSendCommand:
+		case PropagationStack::kStageSendToStructuralChildren:
+		case PropagationStack::kStageSendToStructuralModifiers:
+		case PropagationStack::kStageSendToStructuralSelf:
+			return lowest.ptr.structural;
+		default:
+			break;
+		}
+	}
+
+	return nullptr;
+}
+
 KeyEventDispatch::KeyEventDispatch(const Common::SharedPtr<KeyboardInputEvent> &evt) : _dispatchIndex(0), _evt(evt) {
 }
 
@@ -3727,6 +3777,44 @@ void Runtime::loadScene(const Common::SharedPtr<Structural>& scene) {
 }
 
 void Runtime::sendMessageOnVThread(const Common::SharedPtr<MessageDispatch> &dispatch) {
+#ifndef DISABLE_TEXT_CONSOLE
+	const char *nameStr = "";
+	int srcID = 0;
+	const char *destStr = "";
+	int destID = 0;
+	Common::SharedPtr<RuntimeObject> src = dispatch->getMsg()->getSource().lock();
+
+	if (src) {
+		srcID = src->getStaticGUID();
+		if (src->isStructural())
+			nameStr = static_cast<Structural *>(src.get())->getName().c_str();
+		else if (src->isModifier())
+			nameStr = static_cast<Modifier *>(src.get())->getName().c_str();
+	}
+
+	RuntimeObject *dest = dispatch->getRootPropagator();
+	if (dest) {
+		destID = dest->getStaticGUID();
+		if (dest->isStructural())
+			destStr = static_cast<Structural *>(dest)->getName().c_str();
+		else if (dest->isModifier())
+			destStr = static_cast<Modifier *>(dest)->getName().c_str();
+
+	}
+
+	const Event evt = dispatch->getMsg()->getEvent();
+	bool cascade = dispatch->isCascade();
+	bool relay = dispatch->isRelay();
+
+	Common::String msgDebugString;
+	if (evt.eventType == EventIDs::kAuthorMessage && _project)
+		msgDebugString = Common::String::format("'%s'", _project->findAuthorMessageName(evt.eventInfo));
+	else
+		msgDebugString = Common::String::format("(%i,%i)", evt.eventType, evt.eventInfo);
+
+	debug(3, "Object %x '%s' posted message %s to %x '%s'  mod: %s   ele: %s", srcID, nameStr, msgDebugString.c_str(), destID, destStr, relay ? "all" : "first", cascade ? "all" : "targetOnly");
+#endif
+
 	DispatchMethodTaskData *taskData = _vthread->pushTask("Runtime::dispatchMessageTask", this, &Runtime::dispatchMessageTask);
 	taskData->dispatch = dispatch;
 }
@@ -4734,6 +4822,25 @@ Common::SharedPtr<KeyboardEventSignaller> Project::notifyOnKeyboardEvent(IKeyboa
 	return _keyboardEventSignaller;
 }
 
+const char *Project::findAuthorMessageName(uint32 id) const {
+	for (size_t i = 0; i < _labelSuperGroups.size(); i++) {
+		const LabelSuperGroup &sg = _labelSuperGroups[i];
+		if (sg.name == "Author Messages") {
+			size_t firstNode = sg.firstRootNodeIndex;
+			size_t numNodes = sg.numTotalNodes;
+			for (size_t j = 0; j < numNodes; j++) {
+				const LabelTree &treeNode = _labelTree[j + firstNode];
+				if (treeNode.id == id)
+					return treeNode.name.c_str();
+			}
+
+			break;
+		}
+	}
+
+	return "Unknown";
+}
+
 void Project::loadBootStream(size_t streamIndex) {
 	const StreamDesc &streamDesc = _streams[streamIndex];
 
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 5aea197819f..16cbb9c9f0f 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -1172,6 +1172,12 @@ public:
 	bool isTerminated() const;
 	VThreadState continuePropagating(Runtime *runtime);
 
+	const Common::SharedPtr<MessageProperties> &getMsg() const;
+	RuntimeObject *getRootPropagator() const;
+
+	bool isCascade() const;
+	bool isRelay() const;
+
 private:
 	struct PropagationStack {
 		union Ptr {
@@ -1732,6 +1738,7 @@ protected:
 	virtual ObjectLinkingScope *getPersistentModifierScope();
 
 	MiniscriptInstructionOutcome scriptSetPaused(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetLoop(MiniscriptThread *thread, const DynamicValue &value);
 
 	// If you override this, you must override visitInternalReferences too.
 	virtual void linkInternalReferences(ObjectLinkingScope *outerScope);
@@ -1750,6 +1757,10 @@ protected:
 	// "Paused"/"Unpaused" events.
 	bool _paused;
 
+	// "loop" appears to have been made available on everything in 1.2.  Obsidian depends on it
+	// being available for sound indexes to be properly set up.
+	bool _loop;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	Common::SharedPtr<DebugInspector> _debugInspector;
 #endif
@@ -1895,6 +1906,8 @@ public:
 	void onKeyboardEvent(Runtime *runtime, const Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt);
 	Common::SharedPtr<KeyboardEventSignaller> notifyOnKeyboardEvent(IKeyboardEventReceiver *receiver);
 
+	const char *findAuthorMessageName(uint32 id) const;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Project"; }
 #endif


Commit: 00adebcb81e2d2cbc2ec25177dcfa1489a5d7bb3
    https://github.com/scummvm/scummvm/commit/00adebcb81e2d2cbc2ec25177dcfa1489a5d7bb3
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Refactor MIDI player to support all of the necessary multi-input jank

Changed paths:
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h


diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index f0a43d8e16d..9a7422506d2 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -33,20 +33,143 @@ namespace MTropolis {
 
 namespace Standard {
 
-class MidiPlayer : public Audio::MidiPlayer {
+class MultiMidiPlayer;
+
+// I guess this follows QuickTime quirks, but basically, mTropolis pipes multiple inputs to a single
+// output device, and is totally cool with multiple devices stomping each other.
+//
+// Obsidian actually has a timer that plays a MIDI file that fires AllNoteOff on every channel every
+// 30 seconds, presumably to work around stuck notes, along with workarounds for the workaround,
+// i.e. the intro sequence has a silent part exactly 30 seconds in timed to sync up with the mute.
+//
+// NOTE: Due to SharedPtr not currently being atomic, MidiFilePlayers MUST BE DESTROYED ON THE
+// MAIN THREAD so there is no contention over the file refcount.
+class MidiFilePlayer {
 public:
-	explicit MidiPlayer(int8 source);
-	~MidiPlayer();
+	virtual ~MidiFilePlayer();
+};
+
+// This extends MidiDriver_BASE because we need to intercept commands to modulate the volume
+// separately for each input.
+class MidiFilePlayerImpl : public MidiFilePlayer, public MidiDriver_BASE {
+public:
+	explicit MidiFilePlayerImpl(MidiDriver_BASE *outputDriver, const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, uint32 baseTempo, uint8 volume);
+	~MidiFilePlayerImpl();
+
+	// Do not call any of these directly since they're not thread-safe, expose them via MultiMidiPlayer
+	void stop();
+	void play();
+	void pause();
+	void resume();
+	void setVolume(uint8 volume);
+	void detach();
+	void onTimer();
+
+private:
+	void send(uint32 b) override;
+
+	Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> _file;
+	Common::SharedPtr<MidiParser> _parser;
+	MidiDriver_BASE *_outputDriver;
+	uint8 _volume;
+};
+
+class MultiMidiPlayer : public Audio::MidiPlayer {
+public:
+	MultiMidiPlayer();
+	~MultiMidiPlayer();
+
+	MidiFilePlayer *createFilePlayer(const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, uint8 volume);
+	void deleteFilePlayer(MidiFilePlayer *player);
 
-	void playFile(const void *data, size_t size);
-	int8 getSource() const;
+	void setPlayerVolume(MidiFilePlayer *player, uint8 volume);
+	void stopPlayer(MidiFilePlayer *player);
+	void playPlayer(MidiFilePlayer *player);
+	void pausePlayer(MidiFilePlayer *player);
+	void resumePlayer(MidiFilePlayer *player);
+
+	uint32 getBaseTempo() const;
+
+	void send(uint32 b) override;
 
 private:
-	int8 _source;
-	bool _isInitialized;
+	void onTimer() override;
+
+	static void timerCallback(void *refCon);
+
+	Common::Mutex _mutex;
+	Common::Array<Common::SharedPtr<MidiFilePlayerImpl> > _players;
 };
 
-MidiPlayer::MidiPlayer(int8 source) : _isInitialized(false), _source(source) {
+MidiFilePlayer::~MidiFilePlayer() {
+}
+
+MidiFilePlayerImpl::MidiFilePlayerImpl(MidiDriver_BASE *outputDriver, const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, uint32 baseTempo, uint8 volume)
+	: _file(file), _outputDriver(outputDriver), _parser(nullptr) {
+	Common::SharedPtr<MidiParser> parser(MidiParser::createParser_SMF());
+
+	if (file->contents.size() != 0 && parser->loadMusic(&file->contents[0], file->contents.size())) {
+		parser->setTrack(0);
+		parser->startPlaying();
+		parser->setMidiDriver(this);
+		parser->setTimerRate(baseTempo);
+
+		_parser = parser;
+	}
+}
+
+MidiFilePlayerImpl::~MidiFilePlayerImpl() {
+	assert(!_parser);	// Call detach first!
+}
+
+void MidiFilePlayerImpl::stop() {
+	_parser->stopPlaying();
+}
+
+void MidiFilePlayerImpl::play() {
+	_parser->startPlaying();
+}
+
+void MidiFilePlayerImpl::pause() {
+	_parser->pausePlaying();
+}
+
+void MidiFilePlayerImpl::resume() {
+	_parser->resumePlaying();
+}
+
+void MidiFilePlayerImpl::setVolume(uint8 volume) {
+	_volume = volume;
+}
+
+void MidiFilePlayerImpl::detach() {
+	if (_parser) {
+		_parser->setMidiDriver(nullptr);
+		_parser.reset();
+	}
+}
+
+void MidiFilePlayerImpl::onTimer() {
+	if (_parser)
+		_parser->onTimer();
+}
+
+void MidiFilePlayerImpl::send(uint32 b) {
+	byte command = (b & 0xF0);
+
+	if (command == MIDI_COMMAND_NOTE_ON || command == MIDI_COMMAND_NOTE_OFF) {
+		byte velocity = (b >> 16) & 0xFF;
+		velocity = (velocity * _volume * 257 + 256) >> 16;
+		b = (b & 0xff00ffff) | (velocity << 16);
+	}
+
+	_outputDriver->send(b);
+}
+
+MultiMidiPlayer::MultiMidiPlayer() {
+	createDriver();
+
+	/*
 	MidiDriver::DeviceHandle deviceHdl = MidiDriver::detectDevice(MDT_MIDI | MDT_PREFER_GM);
 	if (!deviceHdl)
 		return;
@@ -54,6 +177,7 @@ MidiPlayer::MidiPlayer(int8 source) : _isInitialized(false), _source(source) {
 	_driver = MidiDriver::createMidi(deviceHdl);
 	if (!_driver)
 		return;
+	*/
 
 	if (_driver->open() != 0) {
 		_driver->close();
@@ -62,37 +186,87 @@ MidiPlayer::MidiPlayer(int8 source) : _isInitialized(false), _source(source) {
 		return;
 	}
 
-	_driver->setTimerCallback(static_cast<Audio::MidiPlayer *>(this), &timerCallback);
+	_driver->setTimerCallback(this, &timerCallback);
+}
+
+MultiMidiPlayer::~MultiMidiPlayer() {
+	Common::StackLock lock(_mutex);
+
+}
 
-	_isInitialized = true;
+void MultiMidiPlayer::timerCallback(void *refCon) {
+	static_cast<MultiMidiPlayer *>(refCon)->onTimer();
 }
 
-MidiPlayer::~MidiPlayer() {
-	stop();
+void MultiMidiPlayer::onTimer() {
+	Common::StackLock lock(_mutex);
 
-	if (_parser)
-		delete _parser;
+	for (const Common::SharedPtr<MidiFilePlayerImpl> &player : _players)
+		player->onTimer();
+}
+
+
+MidiFilePlayer *MultiMidiPlayer::createFilePlayer(const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, uint8 volume) {
+	Common::SharedPtr<MidiFilePlayerImpl> filePlayer(new MidiFilePlayerImpl(this, file, getBaseTempo(), volume));
+
+	{
+		Common::StackLock lock(_mutex);
+		_players.push_back(filePlayer);
+	}
+
+	return filePlayer.get();
 }
 
-void MidiPlayer::playFile(const void *data, size_t size) {
+void MultiMidiPlayer::deleteFilePlayer(MidiFilePlayer *player) {
+	Common::SharedPtr<MidiFilePlayerImpl> ref;
+
+	for (Common::Array<Common::SharedPtr<MidiFilePlayerImpl> >::iterator it = _players.begin(), itEnd = _players.end(); it != itEnd; ++it) {
+		if (it->get() == player) {
+			{
+				Common::StackLock lock(_mutex);
+				ref = *it;
+				_players.erase(it);
+				ref->stop();
+			}
+			break;
+		}
+	}
+
+	if (ref)
+		ref->detach();
+}
+
+void MultiMidiPlayer::setPlayerVolume(MidiFilePlayer *player, uint8 volume) {
 	Common::StackLock lock(_mutex);
+	static_cast<MidiFilePlayerImpl *>(player)->setVolume(volume);
+}
 
-	stop();
+void MultiMidiPlayer::stopPlayer(MidiFilePlayer *player) {
+	Common::StackLock lock(_mutex);
+	static_cast<MidiFilePlayerImpl *>(player)->stop();
+}
 
-	_parser = MidiParser::createParser_SMF();
-	_parser->setMidiDriver(this);
-	_parser->setTimerRate(_driver->getBaseTempo());
+void MultiMidiPlayer::playPlayer(MidiFilePlayer *player) {
+	Common::StackLock lock(_mutex);
+	static_cast<MidiFilePlayerImpl *>(player)->play();
+}
 
-	// FIXME: MIDI API shouldn't need mutable void input...
-	_parser->loadMusic(static_cast<byte *>(const_cast<void *>(data)), size);
-	_parser->setTrack(0);
-	_parser->startPlaying();
+void MultiMidiPlayer::pausePlayer(MidiFilePlayer *player) {
+	Common::StackLock lock(_mutex);
+	static_cast<MidiFilePlayerImpl *>(player)->pause();
+}
+
+void MultiMidiPlayer::resumePlayer(MidiFilePlayer *player) {
+	Common::StackLock lock(_mutex);
+	static_cast<MidiFilePlayerImpl *>(player)->resume();
+}
 
-	resume();
+uint32 MultiMidiPlayer::getBaseTempo() const {
+	return _driver->getBaseTempo();
 }
 
-int8 MidiPlayer::getSource() const {
-	return _source;
+void MultiMidiPlayer::send(uint32 b) {
+	_driver->send(b);
 }
 
 bool CursorModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::CursorModifier &data) {
@@ -510,10 +684,12 @@ RuntimeObject *ObjectReferenceVariableModifier::getObjectParent(RuntimeObject *o
 	return nullptr;
 }
 
-MidiModifier::MidiModifier() : _plugIn(nullptr) {
+MidiModifier::MidiModifier() : _plugIn(nullptr), _filePlayer(nullptr) {
 }
 
 MidiModifier::~MidiModifier() {
+	if (_filePlayer)
+		_plugIn->getMidi()->deleteFilePlayer(_filePlayer);
 }
 
 bool MidiModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::MidiModifier &data) {
@@ -571,7 +747,9 @@ VThreadState MidiModifier::consumeMessage(Runtime *runtime, const Common::Shared
 		if (_mode == kModeFile) {
 			if (_embeddedFile) {
 				debug(2, "MIDI (%x '%s'): Playing embedded file", getStaticGUID(), getName().c_str());
-				_plugIn->getMidi()->playFile(&_embeddedFile->contents[0], _embeddedFile->contents.size());
+				if (!_filePlayer)
+					_filePlayer = _plugIn->getMidi()->createFilePlayer(_embeddedFile, _volume * 255 / 100);
+				_plugIn->getMidi()->playPlayer(_filePlayer);
 			} else {
 				debug(2, "MIDI (%x '%s'): Digested execute event but don't have anything to play", getStaticGUID(), getName().c_str());
 			}
@@ -623,9 +801,9 @@ MiniscriptInstructionOutcome MidiModifier::scriptSetVolume(MiniscriptThread *thr
 	_volume = asInteger;
 
 	if (_mode == kModeFile) {
-		const int normalizedVolume = (_volume * 1306) >> 9;
-		debug(2, "MIDI (%x '%s'): Changing volume to %i", getStaticGUID(), getName().c_str(), normalizedVolume);
-		_plugIn->getMidi()->setVolume(normalizedVolume); // 100 -> 255 range
+		debug(2, "MIDI (%x '%s'): Changing volume to %i", getStaticGUID(), getName().c_str(), _volume);
+		if (_filePlayer)
+			_plugIn->getMidi()->setPlayerVolume(_filePlayer, _volume * 255 / 100);
 	}
 
 	return kMiniscriptInstructionOutcomeContinue;
@@ -822,7 +1000,7 @@ StandardPlugIn::StandardPlugIn()
 	, _listVarModifierFactory(this)
 	, _sysInfoModifierFactory(this)
 	, _lastAllocatedSourceID(0) {
-	_midi.reset(new MidiPlayer(0));
+	_midi.reset(new MultiMidiPlayer());
 }
 
 StandardPlugIn::~StandardPlugIn() {
@@ -846,7 +1024,7 @@ StandardPlugInHacks& StandardPlugIn::getHacks() {
 	return _hacks;
 }
 
-MidiPlayer *StandardPlugIn::getMidi() const {
+MultiMidiPlayer *StandardPlugIn::getMidi() const {
 	return _midi.get();
 }
 
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index 6200677c1b4..71468d13e41 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -36,7 +36,8 @@ class Runtime;
 namespace Standard {
 
 class StandardPlugIn;
-class MidiPlayer;
+class MidiFilePlayer;
+class MultiMidiPlayer;
 
 class CursorModifier : public Modifier {
 public:
@@ -207,6 +208,7 @@ private:
 	bool _isActive;
 
 	StandardPlugIn *_plugIn;
+	MidiFilePlayer *_filePlayer;
 };
 
 class ListVariableModifier : public VariableModifier {
@@ -270,7 +272,7 @@ public:
 	const StandardPlugInHacks &getHacks() const;
 	StandardPlugInHacks &getHacks();
 
-	MidiPlayer *getMidi() const;
+	MultiMidiPlayer *getMidi() const;
 
 	int8 allocateMidiSource();
 	void deallocateMidiSource(int8 source);
@@ -284,7 +286,7 @@ private:
 	PlugInModifierFactory<ListVariableModifier, Data::Standard::ListVariableModifier> _listVarModifierFactory;
 	PlugInModifierFactory<SysInfoModifier, Data::Standard::SysInfoModifier> _sysInfoModifierFactory;
 
-	Common::SharedPtr<MidiPlayer> _midi;
+	Common::SharedPtr<MultiMidiPlayer> _midi;
 	StandardPlugInHacks _hacks;
 
 	Common::Array<int8> _deallocatedSources;


Commit: bc6d7b3e8f6b2258f5b33abd2f905063c80a44cb
    https://github.com/scummvm/scummvm/commit/bc6d7b3e8f6b2258f5b33abd2f905063c80a44cb
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Save games

Changed paths:
  A engines/mtropolis/saveload.cpp
  A engines/mtropolis/saveload.h
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/metaengine.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/module.mk
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/mtropolis.h
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index cd2b8a4b4e9..dc517e10270 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -263,7 +263,7 @@ void ImageElement::render(Window *window) {
 	}
 }
 
-MToonElement::MToonElement() {
+MToonElement::MToonElement() : _cel1Based(1) {
 }
 
 MToonElement::~MToonElement() {
@@ -284,6 +284,25 @@ bool MToonElement::load(ElementLoaderContext &context, const Data::MToonElement
 	return true;
 }
 
+bool MToonElement::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "cel") {
+		result.setInt(_cel1Based);
+		return true;
+	}
+
+	return VisualElement::readAttribute(thread, result, attrib);
+}
+
+MiniscriptInstructionOutcome MToonElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
+	if (attrib == "cel") {
+		// TODO proper support
+		DynamicValueWriteIntegerHelper<uint32>::create(&_cel1Based, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	return VisualElement::writeRefAttribute(thread, result, attrib);
+}
+
 void MToonElement::activate() {
 }
 
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index 37344a82887..9065f7f9f20 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -135,6 +135,9 @@ public:
 
 	bool load(ElementLoaderContext &context, const Data::MToonElement &data);
 
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) override;
+
 	void activate() override;
 	void deactivate() override;
 
@@ -152,9 +155,10 @@ private:
 	bool _maintainRate;
 
 	uint32 _assetID;
-	Runtime *_runtime;
 	uint32 _rateTimes10000;
+	uint32 _cel1Based;
 
+	Runtime *_runtime;
 	Common::SharedPtr<Graphics::Surface> _renderSurface;
 };
 
diff --git a/engines/mtropolis/metaengine.cpp b/engines/mtropolis/metaengine.cpp
index bfbd24791e4..680c3444000 100644
--- a/engines/mtropolis/metaengine.cpp
+++ b/engines/mtropolis/metaengine.cpp
@@ -59,7 +59,19 @@ public:
 };
 
 bool MTropolisMetaEngine::hasFeature(MetaEngineFeature f) const {
-	return false;
+	switch (f) {
+	case kSupportsListSaves:
+	case kSupportsDeleteSave:
+	case kSavesSupportMetaInfo:
+	case kSavesSupportThumbnail:
+	case kSavesSupportCreationDate:
+	case kSavesSupportPlayTime:
+	case kSimpleSavesNames:
+	case kSavesUseExtendedFormat:
+		return true;
+	default:
+		return false;
+	}
 }
 
 bool MTropolis::MTropolisEngine::hasFeature(EngineFeature f) const {
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 42169605c79..924ac9d4954 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -22,11 +22,41 @@
 #include "mtropolis/miniscript.h"
 #include "mtropolis/modifiers.h"
 #include "mtropolis/modifier_factory.h"
+#include "mtropolis/saveload.h"
 
 #include "common/memstream.h"
 
 namespace MTropolis {
 
+class CompoundVarSaver : public ISaveWriter {
+public:
+	explicit CompoundVarSaver(RuntimeObject *object);
+
+	bool writeSave(Common::WriteStream *stream) override;
+
+private:
+	RuntimeObject *_object;
+};
+
+CompoundVarSaver::CompoundVarSaver(RuntimeObject *object) : _object(object) {
+}
+
+bool CompoundVarSaver::writeSave(Common::WriteStream *stream) {
+	if (_object == nullptr || !_object->isModifier())
+		return false;
+
+	Modifier *modifier = static_cast<Modifier *>(_object);
+	Common::SharedPtr<ModifierSaveLoad> saveLoad = modifier->getSaveLoad();
+	if (!saveLoad)
+		return false;
+
+	saveLoad->save(modifier, stream);
+	return !stream->err();
+}
+
+
+
+
 bool BehaviorModifier::load(ModifierLoaderContext &context, const Data::BehaviorModifier &data) {
 	if (data.numChildren > 0) {
 		ChildLoaderContext loaderContext;
@@ -208,8 +238,26 @@ bool SaveAndRestoreModifier::respondsToEvent(const Event &evt) const {
 }
 
 VThreadState SaveAndRestoreModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	if (_saveOrRestoreValue.getType() != DynamicValueTypes::kVariableReference) {
+		warning("Save/restore failed, don't know how to use something that isn't a var reference");
+		return kVThreadError;
+	}
+
+	const VarReference &var = _saveOrRestoreValue.getVarReference();
+
+	Common::WeakPtr<RuntimeObject> objWeak;
+	var.resolve(this, objWeak);
+
+	if (objWeak.expired()) {
+		warning("Save failed, couldn't resolve compound var");
+		return kVThreadError;
+	}
+
+	RuntimeObject *obj = objWeak.lock().get();
+
 	if (_saveWhen.respondsTo(msg->getEvent())) {
-		error("Saves not implemented yet");
+		CompoundVarSaver saver(obj);
+		runtime->getSaveProvider()->promptSave(&saver);
 		return kVThreadReturn;
 	} else if (_restoreWhen.respondsTo(msg->getEvent())) {
 		error("Restores not implemented yet");
@@ -853,19 +901,21 @@ bool KeyboardMessengerModifier::checkKeyEventTrigger(Runtime *runtime, Common::E
 	case Common::KEYCODE_DOWN:
 		resolvedType = kDelete;
 		break;
-	default: {
+	default:
+		if (keyEvt.ascii != 0) {
 			bool isQuestion = (keyEvt.ascii == '?');
 			uint32 uchar = keyEvt.ascii;
 			Common::U32String u(&uchar, 1);
 			outCharStr = u.encode(Common::kMacRoman);
 
 			// STUPID HACK PLEASE FIX ME: ScummVM has no way of just telling us that the character mapping failed,
-			// we we have to check if it encoded "?"
+			// so we have to check if it encoded "?"
 			if (outCharStr.size() < 1 || (outCharStr[0] == '?' && !isQuestion))
 				return false;
 
 			resolvedType = kMacRomanChar;
-		} break;
+		}
+		break;
 	}
 
 	if (_keyCodeType != kAny && resolvedType != _keyCodeType)
@@ -976,6 +1026,10 @@ bool CompoundVariableModifier::load(ModifierLoaderContext &context, const Data::
 	return true;
 }
 
+Common::SharedPtr<ModifierSaveLoad> CompoundVariableModifier::getSaveLoad() {
+	return Common::SharedPtr<ModifierSaveLoad>(new SaveLoad(this));
+}
+
 IModifierContainer *CompoundVariableModifier::getChildContainer() {
 	return this;
 }
@@ -1046,6 +1100,45 @@ Modifier *CompoundVariableModifier::findChildByName(const Common::String &name)
 	return nullptr;
 }
 
+CompoundVariableModifier::SaveLoad::SaveLoad(CompoundVariableModifier *modifier) : _modifier(modifier) {
+	for (const Common::SharedPtr<Modifier> &child : modifier->_children) {
+		Common::SharedPtr<ModifierSaveLoad> childSL = child->getSaveLoad();
+		if (childSL) {
+			ChildSaveLoad childSaveLoad;
+			childSaveLoad.saveLoad = childSL;
+			childSaveLoad.modifier = child.get();
+			_childrenSaveLoad.push_back(childSaveLoad);
+		}
+	}
+}
+
+void CompoundVariableModifier::SaveLoad::saveInternal(Common::WriteStream *stream) const {
+	stream->writeUint32BE(_childrenSaveLoad.size());
+	for (const ChildSaveLoad &childSL : _childrenSaveLoad)
+		childSL.saveLoad->save(childSL.modifier, stream);
+}
+
+bool CompoundVariableModifier::SaveLoad::loadInternal(Common::ReadStream *stream) {
+	const uint32 numChildren = stream->readUint32BE();
+	if (stream->err())
+		return false;
+
+	if (numChildren != _childrenSaveLoad.size())
+		return false;
+
+	for (const ChildSaveLoad &childSL : _childrenSaveLoad) {
+		if (!childSL.saveLoad->load(childSL.modifier, stream))
+			return false;
+	}
+
+	return true;
+}
+
+void CompoundVariableModifier::SaveLoad::commitLoad() const {
+	for (const ChildSaveLoad &childSL : _childrenSaveLoad)
+		childSL.saveLoad->commitLoad();
+}
+
 Common::SharedPtr<Modifier> CompoundVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new CompoundVariableModifier(*this));
 }
@@ -1059,6 +1152,10 @@ bool BooleanVariableModifier::load(ModifierLoaderContext &context, const Data::B
 	return true;
 }
 
+Common::SharedPtr<ModifierSaveLoad> BooleanVariableModifier::getSaveLoad() {
+	return Common::SharedPtr<ModifierSaveLoad>(new SaveLoad(this));
+}
+
 bool BooleanVariableModifier::varSetValue(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kBoolean)
 		_value = value.getBool();
@@ -1076,6 +1173,27 @@ Common::SharedPtr<Modifier> BooleanVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new BooleanVariableModifier(*this));
 }
 
+BooleanVariableModifier::SaveLoad::SaveLoad(BooleanVariableModifier *modifier) : _modifier(modifier) {
+	_value = _modifier->_value;
+}
+
+void BooleanVariableModifier::SaveLoad::commitLoad() const {
+	_modifier->_value = _value;
+}
+
+void BooleanVariableModifier::SaveLoad::saveInternal(Common::WriteStream *stream) const {
+	stream->writeByte(_value ? 1 : 0);
+}
+
+bool BooleanVariableModifier::SaveLoad::loadInternal(Common::ReadStream *stream) {
+	byte b = stream->readByte();
+	if (stream->err())
+		return false;
+
+	_value = (b != 0);
+	return true;
+}
+
 bool IntegerVariableModifier::load(ModifierLoaderContext& context, const Data::IntegerVariableModifier& data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -1085,6 +1203,10 @@ bool IntegerVariableModifier::load(ModifierLoaderContext& context, const Data::I
 	return true;
 }
 
+Common::SharedPtr<ModifierSaveLoad> IntegerVariableModifier::getSaveLoad() {
+	return Common::SharedPtr<ModifierSaveLoad>(new SaveLoad(this));
+}
+
 bool IntegerVariableModifier::varSetValue(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kFloat)
 		_value = static_cast<int32>(floor(value.getFloat() + 0.5));
@@ -1104,6 +1226,27 @@ Common::SharedPtr<Modifier> IntegerVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new IntegerVariableModifier(*this));
 }
 
+IntegerVariableModifier::SaveLoad::SaveLoad(IntegerVariableModifier *modifier) : _modifier(modifier) {
+	_value = _modifier->_value;
+}
+
+void IntegerVariableModifier::SaveLoad::commitLoad() const {
+	_modifier->_value = _value;
+}
+
+void IntegerVariableModifier::SaveLoad::saveInternal(Common::WriteStream *stream) const {
+	stream->writeSint32BE(_value);
+}
+
+bool IntegerVariableModifier::SaveLoad::loadInternal(Common::ReadStream *stream) {
+	_value = stream->readSint32BE();
+
+	if (stream->err())
+		return false;
+
+	return true;
+}
+
 bool IntegerRangeVariableModifier::load(ModifierLoaderContext& context, const Data::IntegerRangeVariableModifier& data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -1114,6 +1257,10 @@ bool IntegerRangeVariableModifier::load(ModifierLoaderContext& context, const Da
 	return true;
 }
 
+Common::SharedPtr<ModifierSaveLoad> IntegerRangeVariableModifier::getSaveLoad() {
+	return Common::SharedPtr<ModifierSaveLoad>(new SaveLoad(this));
+}
+
 bool IntegerRangeVariableModifier::varSetValue(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kIntegerRange)
 		_range = value.getIntRange();
@@ -1131,6 +1278,29 @@ Common::SharedPtr<Modifier> IntegerRangeVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new IntegerRangeVariableModifier(*this));
 }
 
+IntegerRangeVariableModifier::SaveLoad::SaveLoad(IntegerRangeVariableModifier *modifier) : _modifier(modifier) {
+	_range = _modifier->_range;
+}
+
+void IntegerRangeVariableModifier::SaveLoad::commitLoad() const {
+	_modifier->_range = _range;
+}
+
+void IntegerRangeVariableModifier::SaveLoad::saveInternal(Common::WriteStream *stream) const {
+	stream->writeSint32BE(_range.min);
+	stream->writeSint32BE(_range.max);
+}
+
+bool IntegerRangeVariableModifier::SaveLoad::loadInternal(Common::ReadStream *stream) {
+	_range.min = stream->readSint32BE();
+	_range.max = stream->readSint32BE();
+
+	if (stream->err())
+		return false;
+
+	return true;
+}
+
 bool VectorVariableModifier::load(ModifierLoaderContext &context, const Data::VectorVariableModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -1141,6 +1311,10 @@ bool VectorVariableModifier::load(ModifierLoaderContext &context, const Data::Ve
 	return true;
 }
 
+Common::SharedPtr<ModifierSaveLoad> VectorVariableModifier::getSaveLoad() {
+	return Common::SharedPtr<ModifierSaveLoad>(new SaveLoad(this));
+}
+
 bool VectorVariableModifier::varSetValue(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kVector)
 		_vector = value.getVector();
@@ -1183,6 +1357,29 @@ Common::SharedPtr<Modifier> VectorVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new VectorVariableModifier(*this));
 }
 
+VectorVariableModifier::SaveLoad::SaveLoad(VectorVariableModifier *modifier) : _modifier(modifier) {
+	_vector = _modifier->_vector;
+}
+
+void VectorVariableModifier::SaveLoad::commitLoad() const {
+	_modifier->_vector = _vector;
+}
+
+void VectorVariableModifier::SaveLoad::saveInternal(Common::WriteStream *stream) const {
+	stream->writeDoubleBE(_vector.angleRadians);
+	stream->writeDoubleBE(_vector.magnitude);
+}
+
+bool VectorVariableModifier::SaveLoad::loadInternal(Common::ReadStream *stream) {
+	_vector.angleRadians = stream->readDoubleBE();
+	_vector.magnitude = stream->readDoubleBE();
+
+	if (stream->err())
+		return false;
+
+	return true;
+}
+
 bool PointVariableModifier::load(ModifierLoaderContext &context, const Data::PointVariableModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -1193,6 +1390,10 @@ bool PointVariableModifier::load(ModifierLoaderContext &context, const Data::Poi
 	return true;
 }
 
+Common::SharedPtr<ModifierSaveLoad> PointVariableModifier::getSaveLoad() {
+	return Common::SharedPtr<ModifierSaveLoad>(new SaveLoad(this));
+}
+
 bool PointVariableModifier::varSetValue(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kPoint)
 		_value = value.getPoint();
@@ -1210,6 +1411,29 @@ Common::SharedPtr<Modifier> PointVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new PointVariableModifier(*this));
 }
 
+PointVariableModifier::SaveLoad::SaveLoad(PointVariableModifier *modifier) : _modifier(modifier) {
+	_value = _modifier->_value;
+}
+
+void PointVariableModifier::SaveLoad::commitLoad() const {
+	_modifier->_value = _value;
+}
+
+void PointVariableModifier::SaveLoad::saveInternal(Common::WriteStream *stream) const {
+	stream->writeSint16BE(_value.x);
+	stream->writeSint16BE(_value.y);
+}
+
+bool PointVariableModifier::SaveLoad::loadInternal(Common::ReadStream *stream) {
+	_value.x = stream->readSint16BE();
+	_value.y = stream->readSint16BE();
+
+	if (stream->err())
+		return false;
+
+	return true;
+}
+
 bool FloatingPointVariableModifier::load(ModifierLoaderContext &context, const Data::FloatingPointVariableModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -1219,6 +1443,10 @@ bool FloatingPointVariableModifier::load(ModifierLoaderContext &context, const D
 	return true;
 }
 
+Common::SharedPtr<ModifierSaveLoad> FloatingPointVariableModifier::getSaveLoad() {
+	return Common::SharedPtr<ModifierSaveLoad>(new SaveLoad(this));
+}
+
 bool FloatingPointVariableModifier::varSetValue(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kInteger)
 		_value = value.getInt();
@@ -1238,6 +1466,27 @@ Common::SharedPtr<Modifier> FloatingPointVariableModifier::shallowClone() const
 	return Common::SharedPtr<Modifier>(new FloatingPointVariableModifier(*this));
 }
 
+FloatingPointVariableModifier::SaveLoad::SaveLoad(FloatingPointVariableModifier *modifier) : _modifier(modifier) {
+	_value = _modifier->_value;
+}
+
+void FloatingPointVariableModifier::SaveLoad::commitLoad() const {
+	_modifier->_value = _value;
+}
+
+void FloatingPointVariableModifier::SaveLoad::saveInternal(Common::WriteStream *stream) const {
+	stream->writeDoubleBE(_value);
+}
+
+bool FloatingPointVariableModifier::SaveLoad::loadInternal(Common::ReadStream *stream) {
+	_value = stream->readDoubleBE();
+
+	if (stream->err())
+		return false;
+
+	return true;
+}
+
 bool StringVariableModifier::load(ModifierLoaderContext &context, const Data::StringVariableModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -1247,6 +1496,10 @@ bool StringVariableModifier::load(ModifierLoaderContext &context, const Data::St
 	return true;
 }
 
+Common::SharedPtr<ModifierSaveLoad> StringVariableModifier::getSaveLoad() {
+	return Common::SharedPtr<ModifierSaveLoad>(new SaveLoad(this));
+}
+
 bool StringVariableModifier::varSetValue(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kString)
 		_value = value.getString();
@@ -1264,4 +1517,38 @@ Common::SharedPtr<Modifier> StringVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new StringVariableModifier(*this));
 }
 
+StringVariableModifier::SaveLoad::SaveLoad(StringVariableModifier *modifier) : _modifier(modifier) {
+	_value = _modifier->_value;
+}
+
+void StringVariableModifier::SaveLoad::commitLoad() const {
+	_modifier->_value = _value;
+}
+
+void StringVariableModifier::SaveLoad::saveInternal(Common::WriteStream *stream) const {
+	stream->writeUint32BE(_value.size());
+	stream->writeString(_value);
+}
+
+bool StringVariableModifier::SaveLoad::loadInternal(Common::ReadStream *stream) {
+	uint32 size = stream->readUint32BE();
+
+	if (stream->err())
+		return false;
+
+	_value.clear();
+
+	if (size > 0) {
+		Common::Array<char> chars;
+		chars.resize(size);
+		stream->read(&chars[0], size);
+		if (stream->err())
+			return false;
+
+		_value = Common::String(&chars[0], size);
+	}
+
+	return true;
+}
+
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 82c2a2ce6e9..2ace965ffb7 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -601,17 +601,37 @@ class CompoundVariableModifier : public Modifier, public IModifierContainer {
 public:
 	bool load(ModifierLoaderContext &context, const Data::CompoundVariableModifier &data);
 
+	Common::SharedPtr<ModifierSaveLoad> getSaveLoad() override;
+
 	IModifierContainer *getChildContainer() override;
 
 	bool isCompoundVariable() const override { return true; }
 
-
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Compound Variable Modifier"; }
 	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
+	class SaveLoad : public ModifierSaveLoad {
+	public:
+		explicit SaveLoad(CompoundVariableModifier *modifier);
+
+		void saveInternal(Common::WriteStream *stream) const override;
+		bool loadInternal(Common::ReadStream *stream) override;
+		void commitLoad() const override;
+
+	private:
+		struct ChildSaveLoad {
+			Modifier *modifier;
+			Common::SharedPtr<ModifierSaveLoad> saveLoad;
+		};
+
+		Common::Array<ChildSaveLoad> _childrenSaveLoad;
+
+		CompoundVariableModifier *_modifier;
+	};
+
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
 	const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const override;
@@ -632,6 +652,8 @@ class BooleanVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::BooleanVariableModifier &data);
 
+	Common::SharedPtr<ModifierSaveLoad> getSaveLoad() override;
+
 	bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override;
 	void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override;
 
@@ -641,6 +663,19 @@ public:
 #endif
 
 private:
+	class SaveLoad : public ModifierSaveLoad {
+	public:
+		explicit SaveLoad(BooleanVariableModifier *modifier);
+
+	private:
+		void commitLoad() const override;
+		void saveInternal(Common::WriteStream *stream) const override;
+		bool loadInternal(Common::ReadStream *stream) override;
+
+		BooleanVariableModifier *_modifier;
+		bool _value;
+	};
+
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
 	bool _value;
@@ -650,6 +685,8 @@ class IntegerVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::IntegerVariableModifier &data);
 
+	Common::SharedPtr<ModifierSaveLoad> getSaveLoad() override;
+
 	bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override;
 	void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override;
 
@@ -659,6 +696,19 @@ public:
 #endif
 
 private:
+	class SaveLoad : public ModifierSaveLoad {
+	public:
+		explicit SaveLoad(IntegerVariableModifier *modifier);
+
+	private:
+		void commitLoad() const override;
+		void saveInternal(Common::WriteStream *stream) const override;
+		bool loadInternal(Common::ReadStream *stream) override;
+
+		IntegerVariableModifier *_modifier;
+		int32 _value;
+	};
+
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
 	int32 _value;
@@ -668,6 +718,8 @@ class IntegerRangeVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::IntegerRangeVariableModifier &data);
 
+	Common::SharedPtr<ModifierSaveLoad> getSaveLoad() override;
+
 	bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override;
 	void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override;
 
@@ -677,6 +729,19 @@ public:
 #endif
 
 private:
+	class SaveLoad : public ModifierSaveLoad {
+	public:
+		explicit SaveLoad(IntegerRangeVariableModifier *modifier);
+
+	private:
+		void commitLoad() const override;
+		void saveInternal(Common::WriteStream *stream) const override;
+		bool loadInternal(Common::ReadStream *stream) override;
+
+		IntegerRangeVariableModifier *_modifier;
+		IntRange _range;
+	};
+
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
 	IntRange _range;
@@ -686,6 +751,8 @@ class VectorVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::VectorVariableModifier &data);
 
+	Common::SharedPtr<ModifierSaveLoad> getSaveLoad() override;
+
 	bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override;
 	void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override;
 
@@ -698,6 +765,19 @@ public:
 #endif
 
 private:
+	class SaveLoad : public ModifierSaveLoad {
+	public:
+		explicit SaveLoad(VectorVariableModifier *modifier);
+
+	private:
+		void commitLoad() const override;
+		void saveInternal(Common::WriteStream *stream) const override;
+		bool loadInternal(Common::ReadStream *stream) override;
+
+		VectorVariableModifier *_modifier;
+		AngleMagVector _vector;
+	};
+
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
 	AngleMagVector _vector;
@@ -707,6 +787,8 @@ class PointVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::PointVariableModifier &data);
 
+	Common::SharedPtr<ModifierSaveLoad> getSaveLoad() override;
+
 	bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override;
 	void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override;
 
@@ -716,6 +798,19 @@ public:
 #endif
 
 private:
+	class SaveLoad : public ModifierSaveLoad {
+	public:
+		explicit SaveLoad(PointVariableModifier *modifier);
+
+	private:
+		void commitLoad() const override;
+		void saveInternal(Common::WriteStream *stream) const override;
+		bool loadInternal(Common::ReadStream *stream) override;
+
+		PointVariableModifier *_modifier;
+		Point16 _value;
+	};
+
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
 	Point16 _value;
@@ -725,6 +820,8 @@ class FloatingPointVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::FloatingPointVariableModifier &data);
 
+	Common::SharedPtr<ModifierSaveLoad> getSaveLoad() override;
+
 	bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override;
 	void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override;
 
@@ -734,6 +831,19 @@ public:
 #endif
 
 private:
+	class SaveLoad : public ModifierSaveLoad {
+	public:
+		explicit SaveLoad(FloatingPointVariableModifier *modifier);
+
+	private:
+		void commitLoad() const override;
+		void saveInternal(Common::WriteStream *stream) const override;
+		bool loadInternal(Common::ReadStream *stream) override;
+
+		FloatingPointVariableModifier *_modifier;
+		double _value;
+	};
+
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
 	double _value;
@@ -743,6 +853,8 @@ class StringVariableModifier : public VariableModifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::StringVariableModifier &data);
 
+	Common::SharedPtr<ModifierSaveLoad> getSaveLoad() override;
+
 	bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override;
 	void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override;
 
@@ -752,6 +864,19 @@ public:
 #endif
 
 private:
+	class SaveLoad : public ModifierSaveLoad {
+	public:
+		explicit SaveLoad(StringVariableModifier *modifier);
+
+	private:
+		void commitLoad() const override;
+		void saveInternal(Common::WriteStream *stream) const override;
+		bool loadInternal(Common::ReadStream *stream) override;
+
+		StringVariableModifier *_modifier;
+		Common::String _value;
+	};
+
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
 	Common::String _value;
diff --git a/engines/mtropolis/module.mk b/engines/mtropolis/module.mk
index e140df188eb..96df62aa7f6 100644
--- a/engines/mtropolis/module.mk
+++ b/engines/mtropolis/module.mk
@@ -20,6 +20,7 @@ MODULE_OBJS = \
 	plugin/standard_data.o \
 	render.o \
 	runtime.o \
+	saveload.o \
 	vthread.o
 
 # This module can be built as a plugin
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index 25bfd150449..70a03a07727 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -270,7 +270,7 @@ Common::Error MTropolisEngine::run() {
 	int preferredHeight = 768;
 	ColorDepthMode preferredColorDepthMode = kColorDepthMode8Bit;
 
-	_runtime.reset(new Runtime(_system));
+	_runtime.reset(new Runtime(_system, this, nullptr));
 
 	if (_gameDescription->gameID == GID_OBSIDIAN && _gameDescription->desc.platform == Common::kPlatformWindows) {
 		preferredWidth = 640;
diff --git a/engines/mtropolis/mtropolis.h b/engines/mtropolis/mtropolis.h
index 933e8febe8d..2a3ec76d025 100644
--- a/engines/mtropolis/mtropolis.h
+++ b/engines/mtropolis/mtropolis.h
@@ -23,6 +23,7 @@
 #define MTROPOLIS_MTROPOLIS_H
 
 #include "mtropolis/detection.h"
+#include "mtropolis/saveload.h"
 
 #include "engines/engine.h"
 
@@ -38,11 +39,10 @@
  */
 namespace MTropolis {
 
-
-
 class Runtime;
+class RuntimeObject;
 
-class MTropolisEngine : public ::Engine {
+class MTropolisEngine : public ::Engine, public ISaveUIProvider {
 protected:
 	// Engine APIs
 	Common::Error run() override;
@@ -59,6 +59,9 @@ public:
 	uint16 getVersion() const;
 	Common::Platform getPlatform() const;
 
+	bool promptSave(ISaveWriter *writer) override;
+	//bool promptLoad(ISaveReader *reader) override;
+
 public:
 	void handleEvents();
 
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 9a7422506d2..ccc19c44332 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -167,17 +167,7 @@ void MidiFilePlayerImpl::send(uint32 b) {
 }
 
 MultiMidiPlayer::MultiMidiPlayer() {
-	createDriver();
-
-	/*
-	MidiDriver::DeviceHandle deviceHdl = MidiDriver::detectDevice(MDT_MIDI | MDT_PREFER_GM);
-	if (!deviceHdl)
-		return;
-
-	_driver = MidiDriver::createMidi(deviceHdl);
-	if (!_driver)
-		return;
-	*/
+	createDriver(MDT_MIDI | MDT_PREFER_GM);
 
 	if (_driver->open() != 0) {
 		_driver->close();
@@ -425,6 +415,10 @@ bool ObjectReferenceVariableModifier::load(const PlugInModifierLoaderContext &co
 	return true;
 }
 
+Common::SharedPtr<ModifierSaveLoad> ObjectReferenceVariableModifier::getSaveLoad() {
+	return Common::SharedPtr<ModifierSaveLoad>(new SaveLoad(this));
+}
+
 // Object reference variables are somewhat unusual in that they don't store a simple value,
 // they instead have "object" and "path" attributes AND as a value, they resolve to the
 // modifier itself.
@@ -684,6 +678,41 @@ RuntimeObject *ObjectReferenceVariableModifier::getObjectParent(RuntimeObject *o
 	return nullptr;
 }
 
+ObjectReferenceVariableModifier::SaveLoad::SaveLoad(ObjectReferenceVariableModifier *modifier) : _modifier(modifier) {
+	_objectPath = _modifier->_objectPath;
+}
+
+void ObjectReferenceVariableModifier::SaveLoad::commitLoad() const {
+	_modifier->_object.reset();
+	_modifier->_fullPath.clear();
+	_modifier->_objectPath = _objectPath;
+}
+
+void ObjectReferenceVariableModifier::SaveLoad::saveInternal(Common::WriteStream *stream) const {
+	stream->writeUint32BE(_objectPath.size());
+	stream->writeString(_objectPath);
+}
+
+bool ObjectReferenceVariableModifier::SaveLoad::loadInternal(Common::ReadStream *stream) {
+	uint32 stringLen = stream->readUint32BE();
+	if (stream->err())
+		return false;
+
+	_objectPath.clear();
+
+	if (stringLen) {
+		Common::Array<char> strChars;
+		strChars.resize(stringLen);
+		stream->read(&strChars[0], stringLen);
+		if (stream->err())
+			return false;
+
+		_objectPath = Common::String(&strChars[0], stringLen);
+	}
+
+	return true;
+}
+
 MidiModifier::MidiModifier() : _plugIn(nullptr), _filePlayer(nullptr) {
 }
 
@@ -890,6 +919,10 @@ bool ListVariableModifier::load(const PlugInModifierLoaderContext &context, cons
 	return true;
 }
 
+Common::SharedPtr<ModifierSaveLoad> ListVariableModifier::getSaveLoad() {
+	return Common::SharedPtr<ModifierSaveLoad>(new SaveLoad(this));
+}
+
 bool ListVariableModifier::varSetValue(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kList)
 		_list = value.getList()->clone();
@@ -942,6 +975,127 @@ Common::SharedPtr<Modifier> ListVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new ListVariableModifier(*this));
 }
 
+ListVariableModifier::SaveLoad::SaveLoad(ListVariableModifier *modifier) : _modifier(modifier), _list(new DynamicList()) {
+}
+
+void ListVariableModifier::SaveLoad::commitLoad() const {
+	_modifier->_list = _list;
+}
+
+void ListVariableModifier::SaveLoad::saveInternal(Common::WriteStream *stream) const {
+	stream->writeUint32BE(_list->getType());
+	stream->writeUint32BE(_list->getSize());
+
+	size_t listSize = _list->getSize();
+	for (size_t i = 0; i < listSize; i++) {
+		switch (_list->getType()) {
+		case DynamicValueTypes::kInteger:
+			stream->writeSint32BE(_list->getInt()[i]);
+			break;
+		case DynamicValueTypes::kPoint: {
+				const Point16 &pt = _list->getPoint()[i];
+				stream->writeSint16BE(pt.x);
+				stream->writeSint16BE(pt.y);
+			}
+			break;
+		case DynamicValueTypes::kIntegerRange: {
+				const IntRange &range = _list->getIntRange()[i];
+				stream->writeSint32BE(range.min);
+				stream->writeSint32BE(range.max);
+			} break;
+		case DynamicValueTypes::kFloat:
+			stream->writeDoubleBE(_list->getFloat()[i]);
+			break;
+		case DynamicValueTypes::kString: {
+				const Common::String &str = _list->getString()[i];
+				stream->writeUint32BE(str.size());
+				stream->writeString(str);
+			} break;
+		case DynamicValueTypes::kVector: {
+				const AngleMagVector &vec = _list->getVector()[i];
+				stream->writeDoubleBE(vec.angleRadians);
+				stream->writeDoubleBE(vec.magnitude);
+			} break;
+		case DynamicValueTypes::kBoolean:
+			stream->writeByte(_list->getBool()[i] ? 1 : 0);
+			break;
+		default:
+			error("Can't figure out how to write a saved variable");
+			break;
+		}
+	}
+}
+
+bool ListVariableModifier::SaveLoad::loadInternal(Common::ReadStream *stream) {
+	uint32 typeCode = stream->readUint32BE();
+	uint32 size = stream->readUint32BE();
+
+	if (stream->err())
+		return false;
+
+	for (size_t i = 0; i < size; i++) {
+		DynamicValue val;
+
+		switch (typeCode) {
+		case DynamicValueTypes::kInteger: {
+				int32 i32 = stream->readSint32BE();
+				val.setInt(i32);
+			} break;
+		case DynamicValueTypes::kPoint: {
+				Point16 pt;
+				pt.x = stream->readSint16BE();
+				pt.y = stream->readSint16BE();
+				val.setPoint(pt);
+			} break;
+		case DynamicValueTypes::kIntegerRange: {
+				IntRange range;
+				range.min = stream->readSint32BE();
+				range.max = stream->readSint32BE();
+				val.setIntRange(range);
+			} break;
+		case DynamicValueTypes::kFloat: {
+				double f;
+				f = stream->readDoubleBE();
+				val.setFloat(f);
+			} break;
+		case DynamicValueTypes::kString: {
+			uint32 strLen = stream->readUint32BE();
+			if (stream->err())
+				return false;
+
+				Common::String str;
+				if (strLen > 0) {
+					Common::Array<char> chars;
+					chars.resize(strLen);
+					stream->read(&chars[0], strLen);
+					str = Common::String(&chars[0], strLen);
+				}
+				val.setString(str);
+			} break;
+		case DynamicValueTypes::kVector: {
+				AngleMagVector vec;
+				vec.angleRadians = stream->readDoubleBE();
+				vec.magnitude = stream->readDoubleBE();
+				val.setVector(vec);
+			} break;
+		case DynamicValueTypes::kBoolean: {
+				byte b = stream->readByte();
+				val.setBool(b != 0);
+			} break;
+		default:
+			error("Can't figure out how to write a saved variable");
+			break;
+		}
+
+		if (stream->err())
+			return false;
+
+		_list->setAtIndex(i, val);
+	}
+
+	return true;
+}
+
 bool SysInfoModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::SysInfoModifier &data) {
 	return true;
 }
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index 71468d13e41..77b3aaec462 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -116,6 +116,8 @@ public:
 
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::ObjectReferenceVariableModifier &data);
 
+	Common::SharedPtr<ModifierSaveLoad> getSaveLoad() override;
+
 	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
 	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib);
 
@@ -127,6 +129,19 @@ public:
 #endif
 
 private:
+	class SaveLoad : public ModifierSaveLoad {
+	public:
+		explicit SaveLoad(ObjectReferenceVariableModifier *modifier);
+
+	private:
+		void commitLoad() const override;
+		void saveInternal(Common::WriteStream *stream) const override;
+		bool loadInternal(Common::ReadStream *stream) override;
+
+		ObjectReferenceVariableModifier *_modifier;
+		Common::String _objectPath;
+	};
+
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
 	MiniscriptInstructionOutcome scriptSetPath(MiniscriptThread *thread, const DynamicValue &value);
@@ -217,6 +232,8 @@ public:
 
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::ListVariableModifier &data);
 
+	Common::SharedPtr<ModifierSaveLoad> getSaveLoad() override;
+
 	bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override;
 	void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override;
 
@@ -230,6 +247,19 @@ public:
 #endif
 
 private:
+	class SaveLoad : public ModifierSaveLoad {
+	public:
+		explicit SaveLoad(ListVariableModifier *modifier);
+
+	private:
+		void commitLoad() const override;
+		void saveInternal(Common::WriteStream *stream) const override;
+		bool loadInternal(Common::ReadStream *stream) override;
+
+		ListVariableModifier *_modifier;
+		Common::SharedPtr<DynamicList> _list;
+	};
+
 	ListVariableModifier(const ListVariableModifier &other);
 	ListVariableModifier &operator=(const ListVariableModifier &other);
 
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index e5cbc021a04..b7c3625e931 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -1786,6 +1786,54 @@ bool Event::load(const Data::Event &data) {
 	return true;
 }
 
+bool VarReference::resolve(Structural *structuralScope, Common::WeakPtr<RuntimeObject> &outObject) const {
+	if (resolveContainer(structuralScope, outObject))
+		return true;
+
+	Structural *parent = structuralScope->getParent();
+	if (!parent)
+		return false;
+
+	return resolve(parent, outObject);
+}
+
+bool VarReference::resolve(Modifier *modifierScope, Common::WeakPtr<RuntimeObject> &outObject) const {
+	if (resolveSingleModifier(modifierScope, outObject))
+		return true;
+
+	RuntimeObject *parent = modifierScope->getParent().lock().get();
+	if (parent->isStructural())
+		return resolve(static_cast<Structural *>(parent), outObject);
+
+	if (parent->isModifier()) {
+		Modifier *parentModifier = static_cast<Modifier *>(parent);
+		IModifierContainer *parentContainer = parentModifier->getChildContainer();
+		if (parentContainer && resolveContainer(parentContainer, outObject))
+			return true;
+
+		return resolve(parentModifier, outObject);
+	}
+
+	return false;
+}
+
+bool VarReference::resolveContainer(IModifierContainer *modifierContainer, Common::WeakPtr<RuntimeObject> &outObject) const {
+	for (const Common::SharedPtr<Modifier> &modifier : modifierContainer->getModifiers())
+		if (resolveSingleModifier(modifier.get(), outObject))
+			return true;
+
+	return false;
+}
+
+bool VarReference::resolveSingleModifier(Modifier *modifier, Common::WeakPtr<RuntimeObject> &outObject) const {
+	if (modifier->getStaticGUID() == guid || (source && caseInsensitiveEqual(modifier->getName(), *source))) {
+		outObject = modifier->getSelfReference();
+		return true;
+	}
+
+	return false;
+}
+
 MiniscriptInstructionOutcome AngleMagVector::scriptSetAngleDegrees(MiniscriptThread *thread, const DynamicValue &value) {
 	double degrees = 0.0;
 	switch (value.getType())
@@ -3079,10 +3127,12 @@ const Common::KeyState &KeyboardInputEvent::getKeyState() const {
 Runtime::SceneStackEntry::SceneStackEntry() {
 }
 
-Runtime::Runtime(OSystem *system) : _nextRuntimeGUID(1), _realDisplayMode(kColorDepthModeInvalid), _fakeDisplayMode(kColorDepthModeInvalid),
-									_displayWidth(1024), _displayHeight(768), _realTimeBase(0), _playTimeBase(0), _sceneTransitionState(kSceneTransitionStateNotTransitioning),
-									_system(system), _lastFrameCursor(nullptr), _defaultCursor(new DefaultCursor()), _platform(kProjectPlatformUnknown),
-									_cachedMousePosition(Point16::create(0, 0)), _trackedMouseOutside(false) {
+Runtime::Runtime(OSystem *system, ISaveUIProvider *saveProvider, ILoadUIProvider *loadProvider)
+	: _system(system), _saveProvider(saveProvider), _loadProvider(loadProvider),
+	_nextRuntimeGUID(1), _realDisplayMode(kColorDepthModeInvalid), _fakeDisplayMode(kColorDepthModeInvalid),
+	_displayWidth(1024), _displayHeight(768), _realTimeBase(0), _playTimeBase(0), _sceneTransitionState(kSceneTransitionStateNotTransitioning),
+	_lastFrameCursor(nullptr), _defaultCursor(new DefaultCursor()), _platform(kProjectPlatformUnknown),
+	_cachedMousePosition(Point16::create(0, 0)), _trackedMouseOutside(false) {
 	_random.reset(new Common::RandomSource("mtropolis"));
 
 	_vthread.reset(new VThread());
@@ -4167,6 +4217,13 @@ SystemInterface *Runtime::getSystemInterface() const {
 	return _systemInterface.get();
 }
 
+ISaveUIProvider *Runtime::getSaveProvider() const {
+	return _saveProvider;
+}
+
+ILoadUIProvider *Runtime::getLoadProvider() const {
+	return _loadProvider;
+}
 
 void Runtime::ensureMainWindowExists() {
 	// Maybe there's a better spot for this
@@ -5578,6 +5635,43 @@ bool ModifierFlags::load(const uint32 dataModifierFlags) {
 	return true;
 }
 
+ModifierSaveLoad::~ModifierSaveLoad() {
+}
+
+void ModifierSaveLoad::save(Modifier *modifier, Common::WriteStream *stream) {
+	const Common::String &name = modifier->getName();
+
+	stream->writeUint32BE(modifier->getStaticGUID());
+	stream->writeUint16BE(name.size());
+	stream->writeString(name);
+
+	saveInternal(stream);
+}
+
+bool ModifierSaveLoad::load(Modifier *modifier, Common::ReadStream *stream) {
+	uint32 checkGUID = stream->readUint32BE();
+	uint16 nameLen = stream->readUint16BE();
+
+	if (stream->err())
+		return false;
+
+	const Common::String &name = modifier->getName();
+
+	if (name.size() != nameLen)
+		return false;
+
+	Common::Array<char> checkName;
+	checkName.resize(nameLen);
+
+	if (nameLen > 0) {
+		stream->read(&checkName[0], nameLen);
+		if (stream->err() || !memcmp(&checkName[0], name.c_str(), nameLen))
+			return false;
+	}
+
+	return loadInternal(stream);
+}
+
 Modifier::Modifier() : _parent(nullptr) {
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	_debugger = nullptr;
@@ -5630,6 +5724,10 @@ bool Modifier::isKeyboardMessenger() const {
 	return false;
 }
 
+Common::SharedPtr<ModifierSaveLoad> Modifier::getSaveLoad() {
+	return nullptr;
+}
+
 bool Modifier::isModifier() const {
 	return true;
 }
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 16cbb9c9f0f..6e4997126fb 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -82,11 +82,13 @@ class WorldManagerInterface;
 struct DynamicValue;
 struct DynamicValueReadProxy;
 struct DynamicValueWriteProxy;
+struct ILoadUIProvider;
 struct IMessageConsumer;
 struct IModifierContainer;
 struct IModifierFactory;
 struct IPlugInModifierFactory;
 struct IPlugInModifierFactoryAndDataFactory;
+struct ISaveUIProvider;
 struct IStructuralReferenceVisitor;
 struct MessageProperties;
 struct ModifierLoaderContext;
@@ -381,6 +383,13 @@ struct VarReference {
 	inline bool operator!=(const VarReference &other) const {
 		return !((*this) == other);
 	}
+
+	bool resolve(Structural *structuralScope, Common::WeakPtr<RuntimeObject> &outObject) const;
+	bool resolve(Modifier *modifierScope, Common::WeakPtr<RuntimeObject> &outObject) const;
+
+private:
+	bool resolveContainer(IModifierContainer *modifierContainer, Common::WeakPtr<RuntimeObject> &outObject) const;
+	bool resolveSingleModifier(Modifier *modifier, Common::WeakPtr<RuntimeObject> &outObject) const;
 };
 
 struct ObjectReference {
@@ -1324,7 +1333,7 @@ private:
 
 class Runtime {
 public:
-	explicit Runtime(OSystem *system);
+	explicit Runtime(OSystem *system, ISaveUIProvider *saveProvider, ILoadUIProvider *loadProvider);
 
 	bool runFrame();
 	void drawFrame();
@@ -1405,6 +1414,9 @@ public:
 	AssetManagerInterface *getAssetManagerInterface() const;
 	SystemInterface *getSystemInterface() const;
 
+	ISaveUIProvider *getSaveProvider() const;
+	ILoadUIProvider *getLoadProvider() const;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	void debugSetEnabled(bool enabled);
 	void debugBreak();
@@ -1539,6 +1551,8 @@ private:
 
 	Scheduler _scheduler;
 	OSystem *_system;
+	ISaveUIProvider *_saveProvider;
+	ILoadUIProvider *_loadProvider;
 
 	Graphics::Cursor *_lastFrameCursor;
 	Common::SharedPtr<Graphics::Cursor> _defaultCursor;
@@ -2131,6 +2145,23 @@ struct ModifierFlags {
 	bool flagsWereLoaded : 1;
 };
 
+class ModifierSaveLoad {
+public:
+	virtual ~ModifierSaveLoad();
+
+	void save(Modifier *modifier, Common::WriteStream *stream);
+	bool load(Modifier *modifier, Common::ReadStream *stream);
+	virtual void commitLoad() const = 0;
+
+protected:
+	// Saves the modifier state to a stream
+	virtual void saveInternal(Common::WriteStream *stream) const = 0;
+
+	// Loads the modifier state from a stream into the save/load state and returns true
+	// if successful.  This will not trigger any actual changes until "commit" is called.
+	virtual bool loadInternal(Common::ReadStream *stream) = 0;
+};
+
 class Modifier : public RuntimeObject, public IMessageConsumer, public IDebuggable {
 public:
 	Modifier();
@@ -2143,6 +2174,7 @@ public:
 	virtual bool isBehavior() const;
 	virtual bool isCompoundVariable() const;
 	virtual bool isKeyboardMessenger() const;
+	virtual Common::SharedPtr<ModifierSaveLoad> getSaveLoad();
 
 	bool isModifier() const override;
 
@@ -2205,6 +2237,7 @@ public:
 	virtual bool isVariable() const;
 	virtual bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) = 0;
 	virtual void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const = 0;
+	virtual Common::SharedPtr<ModifierSaveLoad> getSaveLoad() override = 0;
 
 	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
 
diff --git a/engines/mtropolis/saveload.cpp b/engines/mtropolis/saveload.cpp
new file mode 100644
index 00000000000..6cdc6bced9e
--- /dev/null
+++ b/engines/mtropolis/saveload.cpp
@@ -0,0 +1,61 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/savefile.h"
+#include "common/translation.h"
+
+#include "mtropolis/mtropolis.h"
+
+#include "gui/saveload.h"
+
+namespace MTropolis {
+
+bool MTropolisEngine::promptSave(ISaveWriter *writer) {
+	GUI::SaveLoadChooser *dialog;
+	Common::String desc;
+	int slot;
+
+	dialog = new GUI::SaveLoadChooser(_("Save game:"), _("Save"), true);
+
+	slot = dialog->runModalWithCurrentTarget();
+	desc = dialog->getResultString();
+
+	if (desc.empty()) {
+		// create our own description for the saved game, the user didnt enter it
+		desc = dialog->createDefaultSaveDescription(slot);
+	}
+
+	delete dialog;
+
+	if (slot < 0)
+		return true;
+
+	Common::String saveFileName = getSaveStateName(slot);
+	Common::SharedPtr<Common::OutSaveFile> out(_saveFileMan->openForSaving(saveFileName, false));
+	if (!writer->writeSave(out.get()) || out->err())
+		warning("An error occurred while writing file '%s'", saveFileName.c_str());
+
+	getMetaEngine()->appendExtendedSave(out.get(), getTotalPlayTime(), desc, false);
+
+	return true;
+}
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/saveload.h b/engines/mtropolis/saveload.h
new file mode 100644
index 00000000000..f8867e3510f
--- /dev/null
+++ b/engines/mtropolis/saveload.h
@@ -0,0 +1,54 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_SAVELOAD_H
+#define MTROPOLIS_SAVELOAD_H
+
+namespace Common {
+
+class ReadStream;
+class WriteStream;
+
+} // End of namespace Common
+
+namespace MTropolis {
+
+class RuntimeObject;
+
+struct ISaveWriter {
+	virtual bool writeSave(Common::WriteStream *stream) = 0;
+};
+
+struct ISaveReader {
+	virtual bool readSave(Common::ReadStream *stream) = 0;
+};
+
+struct ISaveUIProvider {
+	virtual bool promptSave(ISaveWriter *writer) = 0;
+};
+
+struct ILoadUIProvider {
+	virtual bool promptLoad(ISaveReader *reader) = 0;
+};
+
+} // End of namespace MTropolis
+
+#endif /* MTROPOLIS_SAVELOAD_H */


Commit: 68be38a707a3450babad598342b3598ecea3e50b
    https://github.com/scummvm/scummvm/commit/68be38a707a3450babad598342b3598ecea3e50b
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Script/modifier behavior fixes, mostly get cursors working

Changed paths:
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/miniscript.h
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/render.cpp
    engines/mtropolis/render.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index dc517e10270..e4ac70e5628 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -53,7 +53,7 @@ void GraphicElement::render(Window *window) {
 
 MovieElement::MovieElement()
 	: _cacheBitmap(false), _reversed(false), _haveFiredAtFirstCel(false), _haveFiredAtLastCel(false)
-	, _alternate(false), _playEveryFrame(false), _assetID(0), _runtime(nullptr) {
+	, _alternate(false), _playEveryFrame(false), _assetID(0), _runtime(nullptr), _displayFrame(nullptr) {
 }
 
 MovieElement::~MovieElement() {
@@ -151,18 +151,17 @@ void MovieElement::deactivate() {
 }
 
 void MovieElement::render(Window *window) {
-	const Graphics::Surface *videoSurface = nullptr;
 	while (_videoDecoder->needsUpdate()) {
-		videoSurface = _videoDecoder->decodeNextFrame();
+		_displayFrame = _videoDecoder->decodeNextFrame();
 		if (_playEveryFrame)
 			break;
 	}
 
-	if (videoSurface) {
+	if (_displayFrame) {
 		Graphics::ManagedSurface *target = window->getSurface().get();
-		Common::Rect srcRect(0, 0, videoSurface->w, videoSurface->h);
+		Common::Rect srcRect(0, 0, _displayFrame->w, _displayFrame->h);
 		Common::Rect destRect(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y, _cachedAbsoluteOrigin.x + _rect.getWidth(), _cachedAbsoluteOrigin.y + _rect.getHeight());
-		target->blitFrom(*videoSurface, srcRect, destRect);
+		target->blitFrom(*_displayFrame, srcRect, destRect);
 	}
 }
 
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index 9065f7f9f20..a4c320db581 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -93,6 +93,8 @@ private:
 	uint32 _assetID;
 
 	Common::SharedPtr<Video::VideoDecoder> _videoDecoder;
+	const Graphics::Surface *_displayFrame;
+
 	Common::SharedPtr<SegmentUnloadSignaller> _unloadSignaller;
 	Common::SharedPtr<PostRenderSignaller> _postRenderSignaller;
 
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 1c670384891..822705acb13 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -1574,10 +1574,12 @@ MiniscriptInstructionOutcome PushGlobal::execute(MiniscriptThread *thread) const
 	switch (_globalID) {
 	case kGlobalRefElement:
 	case kGlobalRefSection:
-	case kGlobalRefSubsection:
 	case kGlobalRefScene:
 	case kGlobalRefProject:
 		return executeFindFilteredParent(thread);
+	case kGlobalRefModifier:
+		value.setObject(thread->getModifier()->getSelfReference());
+		break;
 	case kGlobalRefIncomingData:
 		value = thread->getMessageProperties()->getValue();
 		break;
@@ -1622,9 +1624,6 @@ MiniscriptInstructionOutcome PushGlobal::executeFindFilteredParent(MiniscriptThr
 		case kGlobalRefSection:
 			isMatch = obj->isSection();
 			break;
-		case kGlobalRefSubsection:
-			isMatch = obj->isSubsection();
-			break;
 		case kGlobalRefScene:
 			// FIXME: Need better detection of scenes
 			isMatch = obj->isElement() && static_cast<Element *>(obj.get())->getParent()->isSubsection();
@@ -1819,6 +1818,10 @@ VThreadState MiniscriptThread::resume(const ResumeTaskData &taskData) {
 	if (instrsArray.size() == 0)
 		return kVThreadReturn;
 
+	if (_modifier->getStaticGUID() == 0x985d1) {
+		int n = 0;
+	}
+
 	MiniscriptInstruction *const *instrs = &instrsArray[0];
 	size_t numInstrs = instrsArray.size();
 
diff --git a/engines/mtropolis/miniscript.h b/engines/mtropolis/miniscript.h
index aab8e32eb6d..ee628553215 100644
--- a/engines/mtropolis/miniscript.h
+++ b/engines/mtropolis/miniscript.h
@@ -335,7 +335,7 @@ namespace MiniscriptInstructions {
 	private:
 		enum {
 			kGlobalRefElement = 1,
-			kGlobalRefSubsection = 2,
+			kGlobalRefModifier = 2,
 			kGlobalRefSource = 3,
 			kGlobalRefIncomingData = 4,
 			kGlobalRefMouse = 5,
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 924ac9d4954..3a566c18584 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -1157,10 +1157,19 @@ Common::SharedPtr<ModifierSaveLoad> BooleanVariableModifier::getSaveLoad() {
 }
 
 bool BooleanVariableModifier::varSetValue(MiniscriptThread *thread, const DynamicValue &value) {
-	if (value.getType() == DynamicValueTypes::kBoolean)
+	switch (value.getType()) {
+	case DynamicValueTypes::kBoolean:
 		_value = value.getBool();
-	else
+		break;
+	case DynamicValueTypes::kFloat:
+		_value = (value.getFloat() != 0.0);
+		break;
+	case DynamicValueTypes::kInteger:
+		_value = (value.getInt() != 0);
+		break;
+	default:
 		return false;
+	}
 
 	return true;
 }
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index ccc19c44332..c3dc430fab3 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -259,6 +259,24 @@ void MultiMidiPlayer::send(uint32 b) {
 	_driver->send(b);
 }
 
+CursorModifier::CursorModifier() {
+}
+
+bool CursorModifier::respondsToEvent(const Event &evt) const {
+	return _applyWhen.respondsTo(evt) || _removeWhen.respondsTo(evt);
+}
+
+VThreadState CursorModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	// As with mTropolis, this doesn't support stacking cursors
+ 	if (_applyWhen.respondsTo(msg->getEvent())) {
+		runtime->setModifierCursorOverride(_cursorID);
+	}
+	if (_removeWhen.respondsTo(msg->getEvent())) {
+		runtime->clearModifierCursorOverride();
+	}
+	return kVThreadReturn;
+}
+
 bool CursorModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::CursorModifier &data) {
 	if (!_applyWhen.load(data.applyWhen) || !_removeWhen.load(data.removeWhen))
 		return false;
@@ -268,7 +286,8 @@ bool CursorModifier::load(const PlugInModifierLoaderContext &context, const Data
 }
 
 Common::SharedPtr<Modifier> CursorModifier::shallowClone() const {
-	return Common::SharedPtr<Modifier>(new CursorModifier(*this));
+	Common::SharedPtr<CursorModifier> clone(new CursorModifier(*this));
+	return clone;
 }
 
 bool STransCtModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::STransCtModifier &data) {
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index 77b3aaec462..ae80117b609 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -41,8 +41,13 @@ class MultiMidiPlayer;
 
 class CursorModifier : public Modifier {
 public:
+	CursorModifier();
+
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::CursorModifier &data);
 
+	bool respondsToEvent(const Event &evt) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Cursor Modifier"; }
 #endif
diff --git a/engines/mtropolis/render.cpp b/engines/mtropolis/render.cpp
index d1b1805d4d1..e8ee471d74e 100644
--- a/engines/mtropolis/render.cpp
+++ b/engines/mtropolis/render.cpp
@@ -131,6 +131,14 @@ const Graphics::PixelFormat& Window::getPixelFormat() const {
 	return _surface->format;
 }
 
+const Common::SharedPtr<CursorGraphic> &Window::getCursorGraphic() const {
+	return _cursor;
+}
+
+void Window::setCursorGraphic(const Common::SharedPtr<CursorGraphic>& cursor) {
+	_cursor = cursor;
+}
+
 void Window::setStrata(int strata) {
 	_strata = strata;
 }
diff --git a/engines/mtropolis/render.h b/engines/mtropolis/render.h
index 8c3db3b1c57..3e5f9f0f8e7 100644
--- a/engines/mtropolis/render.h
+++ b/engines/mtropolis/render.h
@@ -37,6 +37,7 @@ struct Surface;
 
 namespace MTropolis {
 
+class CursorGraphic;
 class Runtime;
 class Project;
 
@@ -86,6 +87,9 @@ public:
 	const Common::SharedPtr<Graphics::ManagedSurface> &getSurface() const;
 	const Graphics::PixelFormat &getPixelFormat() const;
 
+	const Common::SharedPtr<CursorGraphic> &getCursorGraphic() const;
+	void setCursorGraphic(const Common::SharedPtr<CursorGraphic> &cursor);
+
 	void setStrata(int strata);
 	int getStrata() const;
 
@@ -104,10 +108,12 @@ public:
 protected:
 	int32 _x;
 	int32 _y;
-	Common::SharedPtr<Graphics::ManagedSurface> _surface;
 	Runtime *_runtime;
 	int _strata;
 	bool _isMouseTransparent;
+
+	Common::SharedPtr<Graphics::ManagedSurface> _surface;
+	Common::SharedPtr<CursorGraphic> _cursor;
 };
 
 namespace Render {
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index b7c3625e931..558bcfad505 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -816,7 +816,7 @@ bool DynamicList::dynamicValueToIndex(size_t &outIndex, const DynamicValue &valu
 		if (!isfinite(rounded) || rounded < 1.0 || rounded > UINT32_MAX)
 			return false;
 
-		outIndex = static_cast<size_t>(rounded);
+		outIndex = static_cast<size_t>(rounded - 1.0);
 	} else if (value.getType() == DynamicValueTypes::kInteger) {
 		int32 i = value.getInt();
 		if (i < 1)
@@ -1882,6 +1882,23 @@ PlugIn::~PlugIn() {
 ProjectResources::~ProjectResources() {
 }
 
+CursorGraphic::~CursorGraphic() {
+}
+
+MacCursorGraphic::MacCursorGraphic(const Common::SharedPtr<Graphics::MacCursor>& macCursor) : _macCursor(macCursor) {
+}
+
+Graphics::Cursor *MacCursorGraphic::getCursor() const {
+	return _macCursor.get();
+}
+
+WinCursorGraphic::WinCursorGraphic(const Common::SharedPtr<Graphics::WinCursorGroup> &winCursorGroup, Graphics::Cursor *cursor) : _winCursorGroup(winCursorGroup), _cursor(cursor) {
+}
+
+Graphics::Cursor *WinCursorGraphic::getCursor() const {
+	return _cursor;
+}
+
 CursorGraphicCollection::CursorGraphicCollection() {
 }
 
@@ -1889,21 +1906,27 @@ CursorGraphicCollection::~CursorGraphicCollection() {
 }
 
 void CursorGraphicCollection::addWinCursorGroup(uint32 cursorGroupID, const Common::SharedPtr<Graphics::WinCursorGroup> &cursorGroup) {
+	Graphics::Cursor *selectedCursor = nullptr;
 	if (cursorGroup->cursors.size() > 0) {
 		// Not sure what the proper logic should be here, but the second one seems to be the one we usually want
 		if (cursorGroup->cursors.size() > 1)
-			_cursorGraphics[cursorGroupID] = cursorGroup->cursors[1].cursor;
+			selectedCursor = cursorGroup->cursors[1].cursor;
 		else
-			_cursorGraphics[cursorGroupID] = cursorGroup->cursors.back().cursor;
-		_winCursorGroups.push_back(cursorGroup);
+			selectedCursor = cursorGroup->cursors.back().cursor;
+		_cursorGraphics[cursorGroupID].reset(new WinCursorGraphic(cursorGroup, selectedCursor));
 	}
 }
 
 void CursorGraphicCollection::addMacCursor(uint32 cursorID, const Common::SharedPtr<Graphics::MacCursor> &cursor) {
-	_cursorGraphics[cursorID] = cursor.get();
-	_macCursors.push_back(cursor);
+	_cursorGraphics[cursorID].reset(new MacCursorGraphic(cursor));
 }
 
+Common::SharedPtr<CursorGraphic> CursorGraphicCollection::getGraphicByID(uint32 id) const {
+	Common::HashMap<uint32, Common::SharedPtr<CursorGraphic> >::const_iterator it = _cursorGraphics.find(id);
+	if (it != _cursorGraphics.end())
+		return it->_value;
+	return nullptr;
+}
 
 ProjectDescription::ProjectDescription(ProjectPlatform platform) : _language(Common::EN_ANY), _platform(platform) {
 }
@@ -1952,6 +1975,10 @@ void ProjectDescription::setCursorGraphics(const Common::SharedPtr<CursorGraphic
 	_cursorGraphics = cursorGraphics;
 }
 
+const Common::SharedPtr<CursorGraphicCollection> &ProjectDescription::getCursorGraphics() const {
+	return _cursorGraphics;
+}
+
 void ProjectDescription::setLanguage(const Common::Language &language) {
 	_language = language;
 }
@@ -2466,6 +2493,17 @@ void Structural::setParent(Structural *parent) {
 	_parent = parent;
 }
 
+VisualElement *Structural::findScene() {
+	Structural *parent = _parent;
+	if (!parent)
+		return nullptr;
+
+	if (parent->isSubsection())
+		return static_cast<VisualElement *>(this);
+	else
+		return parent->findScene();
+}
+
 const Common::String &Structural::getName() const {
 	return _name;
 }
@@ -2811,6 +2849,10 @@ VThreadState MessageDispatch::continuePropagating(Runtime *runtime) {
 
 				// Post to the message action itself to VThread
 				if (responds) {
+					if (modifier->getStaticGUID() == 0x5b965)
+					{
+						int n = 0;
+					}
 					debug(3, "Modifier %x '%s' consumed message (%i,%i)", modifier->getStaticGUID(), modifier->getName().c_str(), _msg->getEvent().eventType, _msg->getEvent().eventInfo);
 					runtime->postConsumeMessageTask(modifier, _msg);
 				}
@@ -3082,6 +3124,19 @@ const byte DefaultCursor::_cursorPalette[9] = {
 };
 
 
+class DefaultCursorGraphic : public CursorGraphic {
+public:
+	Graphics::Cursor *getCursor() const override;
+
+private:
+	mutable DefaultCursor _cursor;
+};
+
+Graphics::Cursor *DefaultCursorGraphic::getCursor() const {
+	return &_cursor;
+}
+
+
 OSEvent::OSEvent(OSEventType eventType) : _eventType(eventType) {
 }
 
@@ -3131,8 +3186,8 @@ Runtime::Runtime(OSystem *system, ISaveUIProvider *saveProvider, ILoadUIProvider
 	: _system(system), _saveProvider(saveProvider), _loadProvider(loadProvider),
 	_nextRuntimeGUID(1), _realDisplayMode(kColorDepthModeInvalid), _fakeDisplayMode(kColorDepthModeInvalid),
 	_displayWidth(1024), _displayHeight(768), _realTimeBase(0), _playTimeBase(0), _sceneTransitionState(kSceneTransitionStateNotTransitioning),
-	_lastFrameCursor(nullptr), _defaultCursor(new DefaultCursor()), _platform(kProjectPlatformUnknown),
-	_cachedMousePosition(Point16::create(0, 0)), _trackedMouseOutside(false) {
+	_lastFrameCursor(nullptr), _defaultCursor(new DefaultCursorGraphic()), _platform(kProjectPlatformUnknown),
+	_cachedMousePosition(Point16::create(0, 0)), _realMousePosition(Point16::create(0, 0)), _trackedMouseOutside(false), _haveModifierOverrideCursor(false) {
 	_random.reset(new Common::RandomSource("mtropolis"));
 
 	_vthread.reset(new VThread());
@@ -3216,7 +3271,7 @@ bool Runtime::runFrame() {
 
 					// Pushed second, so this happens first
 					if (mouseEvt->getX() != _cachedMousePosition.x || mouseEvt->getY() != _cachedMousePosition.y) {
-						UpdateMousePositionTaskData *taskData = _vthread->pushTask("Runtime::updateMousePositionTask1", this, &Runtime::updateMousePositionTask1);
+						UpdateMousePositionTaskData *taskData = _vthread->pushTask("Runtime::updateMousePositionTask", this, &Runtime::updateMousePositionTask);
 						taskData->x = mouseEvt->getX();
 						taskData->y = mouseEvt->getY();
 					}
@@ -3443,17 +3498,24 @@ void Runtime::drawFrame() {
 
 	_system->updateScreen();
 
-	Graphics::Cursor *cursor = nullptr;
+	Common::SharedPtr<CursorGraphic> cursor;
+	
+	Common::SharedPtr<Window> focusWindow = _mouseFocusWindow.lock();
 
-	// ...
-	if (cursor == nullptr)
-		cursor = _defaultCursor.get();
+	if (!focusWindow)
+		focusWindow = findTopWindow(_realMousePosition.x, _realMousePosition.y);
 
-	if (cursor != _lastFrameCursor) {
-		_lastFrameCursor = cursor;
+	if (focusWindow)
+		cursor = focusWindow->getCursorGraphic();
 
+	if (!cursor)
+		cursor = _defaultCursor;
+
+	if (cursor != _lastFrameCursor) {
 		CursorMan.showMouse(true);
-		CursorMan.replaceCursor(cursor);
+		CursorMan.replaceCursor(cursor->getCursor());
+
+		_lastFrameCursor = cursor;
 	}
 
 	if (_project)
@@ -3772,7 +3834,7 @@ bool Runtime::isModifierMouseInteractive(Modifier *modifier) {
 	return false;
 }
 
-void Runtime::recursiveFindMouseCollision(Structural *&bestResult, int &bestLayer, Structural *candidate, int32 relativeX, int32 relativeY) {
+void Runtime::recursiveFindMouseCollision(Structural *&bestResult, int &bestLayer, int &bestStackHeight, Structural *candidate, int stackHeight, int32 relativeX, int32 relativeY) {
 	int32 childRelativeX = relativeX;
 	int32 childRelativeY = relativeY;
 	if (candidate->isElement()) {
@@ -3780,9 +3842,15 @@ void Runtime::recursiveFindMouseCollision(Structural *&bestResult, int &bestLaye
 		if (element->isVisual()) {
 			VisualElement *visual = static_cast<VisualElement *>(candidate);
 			int layer = visual->getLayer();
-			if (layer > bestLayer && visual->isMouseInsideBox(relativeX, relativeY) && isStructuralMouseInteractive(visual) && visual->isMouseCollisionAtPoint(relativeX, relativeY)) {
+
+			// Weird layering behavior:
+			// Objects in a higher layer in lower scenes still have higher render order, so they're on top
+			const bool isInFront = (layer > bestLayer) || (layer == bestLayer && stackHeight > bestStackHeight);
+
+			if (isInFront && visual->isMouseInsideBox(relativeX, relativeY) && isStructuralMouseInteractive(visual) && visual->isMouseCollisionAtPoint(relativeX, relativeY)) {
 				bestResult = candidate;
 				bestLayer = layer;
+				bestStackHeight = stackHeight;
 			}
 
 			childRelativeX -= visual->getRelativeRect().left;
@@ -3792,7 +3860,7 @@ void Runtime::recursiveFindMouseCollision(Structural *&bestResult, int &bestLaye
 
 
 	for (const Common::SharedPtr<Structural> &child : candidate->getChildren()) {
-		recursiveFindMouseCollision(bestResult, bestLayer, child.get(), childRelativeX, childRelativeY);
+		recursiveFindMouseCollision(bestResult, bestLayer, bestStackHeight, child.get(), stackHeight, childRelativeX, childRelativeY);
 	}
 }
 
@@ -3857,12 +3925,249 @@ void Runtime::sendMessageOnVThread(const Common::SharedPtr<MessageDispatch> &dis
 	bool relay = dispatch->isRelay();
 
 	Common::String msgDebugString;
-	if (evt.eventType == EventIDs::kAuthorMessage && _project)
-		msgDebugString = Common::String::format("'%s'", _project->findAuthorMessageName(evt.eventInfo));
-	else
-		msgDebugString = Common::String::format("(%i,%i)", evt.eventType, evt.eventInfo);
+	msgDebugString = Common::String::format("(%i,%i)", evt.eventType, evt.eventInfo);
+	if (evt.eventType == EventIDs::kAuthorMessage && _project) {
+		msgDebugString += " '";
+		msgDebugString += _project->findAuthorMessageName(evt.eventInfo);
+		msgDebugString += "'";
+	} else {
+		const char *extType = nullptr;
+		switch (evt.eventType) {
+		case EventIDs::kElementEnableEdit:
+			extType = "Element Enable Edit";
+			break;
+		case EventIDs::kElementDisableEdit:
+			extType = "Element Disable Edit";
+			break;
+		case EventIDs::kElementSelect:
+			extType = "Element Select";
+			break;
+		case EventIDs::kElementDeselect:
+			extType = "Element Deselect";
+			break;
+		case EventIDs::kElementToggleSelect:
+			extType = "Element Toggle Select";
+			break;
+		case EventIDs::kElementUpdatedCalculated:
+			extType = "Element Updated Calculated";
+			break;
+		case EventIDs::kElementShow:
+			extType = "Element Show";
+			break;
+		case EventIDs::kElementHide:
+			extType = "Element Hide";
+			break;
+		case EventIDs::kElementScrollUp:
+			extType = "Element Scroll Up";
+			break;
+		case EventIDs::kElementScrollDown:
+			extType = "Element Scroll Down";
+			break;
+		case EventIDs::kElementScrollRight:
+			extType = "Element Scroll Right";
+			break;
+		case EventIDs::kElementScrollLeft:
+			extType = "Element Scroll Left";
+			break;
+
+		case EventIDs::kMotionStarted:
+			extType = "Motion Ended";
+			break;
+		case EventIDs::kMotionEnded:
+			extType = "Motion Started";
+			break;
+
+		case EventIDs::kTransitionStarted:
+			extType = "Transition Started";
+			break;
+		case EventIDs::kTransitionEnded:
+			extType = "Transition Ended";
+			break;
+
+		case EventIDs::kMouseDown:
+			extType = "Mouse Down";
+			break;
+		case EventIDs::kMouseUp:
+			extType = "Mouse Up";
+			break;
+		case EventIDs::kMouseOver:
+			extType = "Mouse Over";
+			break;
+		case EventIDs::kMouseOutside:
+			extType = "Mouse Outside";
+			break;
+		case EventIDs::kMouseTrackedInside:
+			extType = "Mouse Tracked Inside";
+			break;
+		case EventIDs::kMouseTrackedOutside:
+			extType = "Mouse Tracked Outside";
+			break;
+		case EventIDs::kMouseTracking:
+			extType = "Mouse Tracking";
+			break;
+		case EventIDs::kMouseUpInside:
+			extType = "Mouse Up Inside";
+			break;
+		case EventIDs::kMouseUpOutside:
+			extType = "Mouse Up Outside";
+			break;
+
+		case EventIDs::kSceneStarted:
+			extType = "Mouse Up Outside";
+			break;
+		case EventIDs::kSceneEnded:
+			extType = "Scene Ended";
+			break;
+		case EventIDs::kSceneDeactivated:
+			extType = "Scene Deactivate";
+			break;
+		case EventIDs::kSceneReactivated:
+			extType = "Scene Reactivated";
+			break;
+		case EventIDs::kSceneTransitionEnded:
+			extType = "Scene Transition Ended";
+			break;
+
+		case EventIDs::kSharedSceneReturnedToScene:
+			extType = "Scene Returned To Scene";
+			break;
+		case EventIDs::kSharedSceneSceneChanged:
+			extType = "Scene Scene Changed";
+			break;
+		case EventIDs::kSharedSceneNoNextScene:
+			extType = "Shared Scene No Next Scene";
+			break;
+		case EventIDs::kSharedSceneNoPrevScene:
+			extType = "Shared Scene No Prev Scene";
+			break;
+
+		case EventIDs::kParentEnabled:
+			extType = "Parent Enabled";
+			break;
+		case EventIDs::kParentDisabled:
+			extType = "Parent Disabled";
+			break;
+		case EventIDs::kParentChanged:
+			extType = "Parent Changed";
+			break;
+
+		case EventIDs::kPreloadMedia:
+			extType = "Preload Media";
+			break;
+		case EventIDs::kFlushMedia:
+			extType = "Flush Media";
+			break;
+		case EventIDs::kPrerollMedia:
+			extType = "Preroll Media";
+			break;
+
+		case EventIDs::kCloseProject:
+			extType = "Close Project";
+			break;
+
+		case EventIDs::kUserTimeout:
+			extType = "User Timeout";
+			break;
+		case EventIDs::kProjectStarted:
+			extType = "Project Started";
+			break;
+		case EventIDs::kProjectEnded:
+			extType = "Project Ended";
+			break;
+		case EventIDs::kFlushAllMedia:
+			extType = "Flush All Media";
+			break;
+
+		case EventIDs::kAttribGet:
+			extType = "Attrib Get";
+			break;
+		case EventIDs::kAttribSet:
+			extType = "Attrib Set";
+			break;
+
+		case EventIDs::kClone:
+			extType = "Clone";
+			break;
+		case EventIDs::kKill:
+			extType = "Kill";
+			break;
+
+		case EventIDs::kPlay:
+			extType = "Play";
+			break;
+		case EventIDs::kStop:
+			extType = "Stop";
+			break;
+		case EventIDs::kPause:
+			extType = "Pause";
+			break;
+		case EventIDs::kUnpause:
+			extType = "Unpause";
+			break;
+		case EventIDs::kTogglePause:
+			extType = "Toggle Pause";
+			break;
+		case EventIDs::kAtFirstCel:
+			extType = "At First Cel";
+			break;
+		case EventIDs::kAtLastCel:
+			extType = "At Last Cell";
+			break;
+		default:
+			break;
+		}
+
+		if (extType) {
+			msgDebugString += " '";
+			msgDebugString += extType;
+			msgDebugString += "'";
+		}
+	}
+
+	Common::String valueStr;
+	const DynamicValue &payload = dispatch->getMsg()->getValue();
+
+	if (payload.getType() != DynamicValueTypes::kNull) {
+		switch (payload.getType())
+		{
+		case DynamicValueTypes::kBoolean:
+			valueStr = (payload.getBool() ? "true" : "false");
+			break;
+		case DynamicValueTypes::kInteger:
+			valueStr = Common::String::format("%i", payload.getInt());
+			break;
+		case DynamicValueTypes::kFloat:
+			valueStr = Common::String::format("%g", payload.getFloat());
+			break;
+		case DynamicValueTypes::kPoint:
+			valueStr = Common::String::format("(%i,%i)", payload.getPoint().x, payload.getPoint().y);
+			break;
+		case DynamicValueTypes::kIntegerRange:
+			valueStr = Common::String::format("(%i thru %i)", payload.getIntRange().min, payload.getIntRange().max);
+			break;
+		case DynamicValueTypes::kVector:
+			valueStr = Common::String::format("(%g deg %g mag)", payload.getVector().angleRadians * (180.0 / M_PI), payload.getVector().magnitude);
+			break;
+		case DynamicValueTypes::kString:
+			valueStr = "'" + payload.getString() + "'";
+			break;
+		case DynamicValueTypes::kList:
+			valueStr = "List";
+			break;
+		case DynamicValueTypes::kObject:
+			valueStr = "Object";
+			if (RuntimeObject *obj = payload.getObject().object.lock().get())
+				valueStr += Common::String::format(" %x", obj->getStaticGUID());
+			break;
+		default:
+			valueStr = "<BAD TYPE> (this is a bug!)";
+			break;
+		}
 
-	debug(3, "Object %x '%s' posted message %s to %x '%s'  mod: %s   ele: %s", srcID, nameStr, msgDebugString.c_str(), destID, destStr, relay ? "all" : "first", cascade ? "all" : "targetOnly");
+		valueStr = " with value " + valueStr;
+	}
+
+	debug(3, "Object %x '%s' posted message %s to %x '%s'%s  mod: %s   ele: %s", srcID, nameStr, msgDebugString.c_str(), destID, destStr, valueStr.c_str(), relay ? "all" : "first", cascade ? "all" : "targetOnly");
 #endif
 
 	DispatchMethodTaskData *taskData = _vthread->pushTask("Runtime::dispatchMessageTask", this, &Runtime::dispatchMessageTask);
@@ -3926,7 +4231,7 @@ VThreadState Runtime::updateMouseStateTask(const UpdateMouseStateTaskData &data)
 			_trackedMouseOutside = false;
 
 			MessageToSend msg;
-			msg.eventID = EventIDs::kMouseUp;
+			msg.eventID = EventIDs::kMouseDown;
 			msg.target = tracked.get();
 			messagesToSend.push_back(msg);
 		}
@@ -3967,7 +4272,7 @@ VThreadState Runtime::updateMouseStateTask(const UpdateMouseStateTaskData &data)
 }
 
 
-VThreadState Runtime::updateMousePositionTask1(const UpdateMousePositionTaskData &data) {
+VThreadState Runtime::updateMousePositionTask(const UpdateMousePositionTaskData &data) {
 	if (!_project)
 		return kVThreadReturn;
 
@@ -3981,13 +4286,12 @@ VThreadState Runtime::updateMousePositionTask1(const UpdateMousePositionTaskData
 	// a Mouse Up Inside event is sent when the button is released.
 
 	Structural *collisionItem = nullptr;
+	int bestSceneStack = INT_MIN;
 	int bestLayer = INT_MIN;
 
 	for (size_t ri = 0; ri < _sceneStack.size(); ri++) {
 		const SceneStackEntry &sceneStackEntry = _sceneStack[_sceneStack.size() - 1 - ri];
-		recursiveFindMouseCollision(collisionItem, bestLayer, sceneStackEntry.scene.get(), data.x, data.y);
-		if (collisionItem)
-			break;
+		recursiveFindMouseCollision(collisionItem, bestSceneStack, bestLayer, sceneStackEntry.scene.get(), _sceneStack.size() - 1 - ri, data.x, data.y);
 	}
 
 	Common::SharedPtr<Structural> newMouseOver;
@@ -4072,6 +4376,31 @@ VThreadState Runtime::updateMousePositionTask1(const UpdateMousePositionTaskData
 	return kVThreadReturn;
 }
 
+void Runtime::updateMainWindowCursor() {
+	const uint32 kHandPointUpID = 10005;
+	const uint32 kArrowID = 10011;
+
+	if (!_mainWindow.expired()) {
+		uint32 selectedCursor = kArrowID;
+		if (!_mouseOverObject.expired())
+			selectedCursor = kHandPointUpID;
+
+		if (_haveModifierOverrideCursor)
+			selectedCursor = _modifierOverrideCursorID;
+
+		if (_project) {
+			Common::SharedPtr<CursorGraphicCollection> cursorGraphics = _project->getCursorGraphics();
+			if (cursorGraphics) {
+				Common::SharedPtr<CursorGraphic> graphic = cursorGraphics->getGraphicByID(selectedCursor);
+				if (graphic) {
+					Common::SharedPtr<Window> mainWindow = _mainWindow.lock();
+					mainWindow->setCursorGraphic(graphic);
+				}
+			}
+		}
+	}
+}
+
 void Runtime::queueMessage(const Common::SharedPtr<MessageDispatch>& dispatch) {
 	_messageQueue.push_back(dispatch);
 }
@@ -4143,6 +4472,9 @@ ProjectPlatform Runtime::getPlatform() const {
 }
 
 void Runtime::onMouseDown(int32 x, int32 y, Actions::MouseButton mButton) {
+	_realMousePosition.x = x;
+	_realMousePosition.y = y;
+
 	Common::SharedPtr<Window> focusWindow = _mouseFocusWindow.lock();
 	if (!focusWindow) {
 		focusWindow = findTopWindow(x, y);
@@ -4162,17 +4494,22 @@ void Runtime::onMouseDown(int32 x, int32 y, Actions::MouseButton mButton) {
 }
 
 void Runtime::onMouseMove(int32 x, int32 y) {
+	_realMousePosition.x = x;
+	_realMousePosition.y = y;
+
 	Common::SharedPtr<Window> focusWindow = _mouseFocusWindow.lock();
+
 	if (!focusWindow)
 		focusWindow = findTopWindow(x, y);
 
 	if (focusWindow)
 		focusWindow->onMouseMove(x - focusWindow->getX(), y - focusWindow->getY());
-
-	// TODO: Change mouse to focus window's cursor
 }
 
 void Runtime::onMouseUp(int32 x, int32 y, Actions::MouseButton mButton) {
+	_realMousePosition.x = x;
+	_realMousePosition.y = y;
+
 	Common::SharedPtr<Window> focusWindow = _mouseFocusWindow.lock();
 	if (!focusWindow)
 		return;
@@ -4201,6 +4538,22 @@ const Point16 &Runtime::getCachedMousePosition() const {
 	return _cachedMousePosition;
 }
 
+void Runtime::setModifierCursorOverride(uint32 cursorID) {
+	if (!_haveModifierOverrideCursor || _modifierOverrideCursorID != cursorID) {
+		_haveModifierOverrideCursor = true;
+		_modifierOverrideCursorID = cursorID;
+		updateMainWindowCursor();
+	}
+}
+
+void Runtime::clearModifierCursorOverride() {
+	if (_haveModifierOverrideCursor) {
+		_haveModifierOverrideCursor = false;
+		updateMainWindowCursor();
+
+	}
+}
+
 Common::RandomSource *Runtime::getRandom() const {
 	return _random.get();
 }
@@ -4237,6 +4590,8 @@ void Runtime::ensureMainWindowExists() {
 		_mainWindow.reset(mainWindow);
 
 		_keyFocusWindow = mainWindow;
+
+		updateMainWindowCursor();
 	}
 }
 
@@ -4258,6 +4613,8 @@ void Runtime::unloadProject() {
 	// These should be last
 	_project.reset();
 	_rootLinkingScope.reset();
+
+	_haveModifierOverrideCursor = false;
 }
 
 void Runtime::refreshPlayTime() {
@@ -4591,6 +4948,7 @@ Project::~Project() {
 
 void Project::loadFromDescription(const ProjectDescription& desc) {
 	_resources = desc.getResources();
+	_cursorGraphics = desc.getCursorGraphics();
 
 	debug(1, "Loading new project...");
 
@@ -4898,6 +5256,10 @@ const char *Project::findAuthorMessageName(uint32 id) const {
 	return "Unknown";
 }
 
+const Common::SharedPtr<CursorGraphicCollection> &Project::getCursorGraphics() const {
+	return _cursorGraphics;
+}
+
 void Project::loadBootStream(size_t streamIndex) {
 	const StreamDesc &streamDesc = _streams[streamIndex];
 
@@ -5375,12 +5737,6 @@ bool Subsection::load(const Data::SubsectionStructuralDef &data) {
 }
 
 bool Subsection::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
-	if (attrib == "subsection") {
-		// Unclear why this is necessary
-		result.setObject(getSelfReference());
-		return true;
-	}
-
 	return Structural::readAttribute(thread, result, attrib);
 }
 
@@ -5454,6 +5810,9 @@ bool VisualElement::readAttribute(MiniscriptThread *thread, DynamicValue &result
 	} else if (attrib == "globalposition") {
 		result.setPoint(getGlobalPosition());
 		return true;
+	} else if (attrib == "layer") {
+		result.setInt(_layer);
+		return true;
 	}
 
 	return Element::readAttribute(thread, result, attrib);
@@ -5475,6 +5834,9 @@ MiniscriptInstructionOutcome VisualElement::writeRefAttribute(MiniscriptThread *
 	} else if (attrib == "height") {
 		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetHeight>::create(this, writeProxy);
 		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "layer") {
+		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetLayer>::create(this, writeProxy);
+		return kMiniscriptInstructionOutcomeContinue;
 	}
 
 	return Element::writeRefAttribute(thread, writeProxy, attrib);
@@ -5550,7 +5912,7 @@ MiniscriptInstructionOutcome VisualElement::scriptSetPosition(MiniscriptThread *
 	if (value.getType() == DynamicValueTypes::kPoint) {
 		const Point16 &destPoint = value.getPoint();
 		int32 xDelta = destPoint.x - _rect.left;
-		int32 yDelta = destPoint.y - _rect.right;
+		int32 yDelta = destPoint.y - _rect.top;
 
 		if (xDelta != 0 || yDelta != 0)
 			offsetTranslate(xDelta, yDelta, false);
@@ -5580,6 +5942,26 @@ MiniscriptInstructionOutcome VisualElement::scriptSetHeight(MiniscriptThread *th
 	return kMiniscriptInstructionOutcomeContinue;
 }
 
+MiniscriptInstructionOutcome VisualElement::scriptSetLayer(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger))
+		return kMiniscriptInstructionOutcomeFailed;
+
+	if (asInteger != _layer) {
+		VisualElement *scene = findScene();
+
+		// If a layer is assigned and a colliding element exists, then the layers are swapped
+		if (scene) {
+			VisualElement *collision = recursiveFindItemWithLayer(scene, asInteger);
+			if (collision)
+				collision->_layer = _layer;
+		}
+		_layer = asInteger;
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 void VisualElement::offsetTranslate(int32 xDelta, int32 yDelta, bool cachedOriginOnly) {
 	if (!cachedOriginOnly) {
 		_rect.left += xDelta;
@@ -5612,6 +5994,24 @@ VThreadState VisualElement::changeVisibilityTask(const ChangeFlagTaskData &taskD
 	return kVThreadReturn;
 }
 
+VisualElement *VisualElement::recursiveFindItemWithLayer(VisualElement *element, int32 layer) {
+	if (element->_layer == layer)
+		return element;
+
+	for (const Common::SharedPtr<Structural> &child : element->getChildren()) {
+		if (child->isElement()) {
+			Element *childElement = static_cast<Element *>(child.get());
+			if (childElement->isVisual()) {
+				VisualElement *result = recursiveFindItemWithLayer(static_cast<VisualElement *>(childElement), layer);
+				if (result)
+					return result;
+			}
+		}
+	}
+
+	return nullptr;
+}
+
 bool NonVisualElement::isVisual() const {
 	return false;
 }
@@ -5685,6 +6085,37 @@ Modifier::~Modifier() {
 #endif
 }
 
+bool Modifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "parent") {
+		result.setObject(_parent);
+		return true;
+	}
+	if (attrib == "subsection") {
+		RuntimeObject *scan = _parent.lock().get();
+		while (scan) {
+			if (scan->isSubsection()) {
+				result.setObject(scan->getSelfReference());
+				return true;
+			}
+
+			if (scan->isStructural())
+				scan = static_cast<Structural *>(scan)->getParent();
+			else if (scan->isModifier())
+				scan = static_cast<Modifier *>(scan)->getParent().lock().get();
+			else
+				break;
+		}
+
+		return false;
+	}
+
+	return false;
+
+}
+
+
+
+
 void Modifier::materialize(Runtime *runtime, ObjectLinkingScope *outerScope) {
 	ObjectLinkingScope innerScope;
 	innerScope.setParent(outerScope);
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 6e4997126fb..363923903d1 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -1050,6 +1050,34 @@ struct ProjectResources {
 	virtual ~ProjectResources();
 };
 
+class CursorGraphic {
+public:
+	virtual ~CursorGraphic();
+
+	virtual Graphics::Cursor *getCursor() const = 0;
+};
+
+class MacCursorGraphic : public CursorGraphic {
+public:
+	explicit MacCursorGraphic(const Common::SharedPtr<Graphics::MacCursor> &macCursor);
+
+	Graphics::Cursor *getCursor() const override;
+
+private:
+	Common::SharedPtr<Graphics::MacCursor> _macCursor;
+};
+
+class WinCursorGraphic : public CursorGraphic {
+public:
+	explicit WinCursorGraphic(const Common::SharedPtr<Graphics::WinCursorGroup> &winCursorGroup, Graphics::Cursor *cursor);
+
+	Graphics::Cursor *getCursor() const override;
+
+private:
+	Common::SharedPtr<Graphics::WinCursorGroup> _winCursorGroup;
+	Graphics::Cursor *_cursor;
+};
+
 class CursorGraphicCollection {
 public:
 	CursorGraphicCollection();
@@ -1058,11 +1086,10 @@ public:
 	void addWinCursorGroup(uint32 cursorGroupID, const Common::SharedPtr<Graphics::WinCursorGroup> &cursorGroup);
 	void addMacCursor(uint32 cursorID, const Common::SharedPtr<Graphics::MacCursor> &cursor);
 
-private:
-	Common::HashMap<uint32, Graphics::Cursor *> _cursorGraphics;
+	Common::SharedPtr<CursorGraphic> getGraphicByID(uint32 id) const;
 
-	Common::Array<Common::SharedPtr<Graphics::WinCursorGroup> > _winCursorGroups;
-	Common::Array<Common::SharedPtr<Graphics::MacCursor> > _macCursors;
+private:
+	Common::HashMap<uint32, Common::SharedPtr<CursorGraphic> > _cursorGraphics;
 };
 
 enum ProjectPlatform {
@@ -1088,6 +1115,7 @@ public:
 	const Common::SharedPtr<ProjectResources> &getResources() const;
 
 	void setCursorGraphics(const Common::SharedPtr<CursorGraphicCollection> &cursorGraphics);
+	const Common::SharedPtr<CursorGraphicCollection> &getCursorGraphics() const;
 
 	void setLanguage(const Common::Language &language);
 	const Common::Language &getLanguage() const;
@@ -1408,6 +1436,8 @@ public:
 	void onKeyboardEvent(const Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt);
 
 	const Point16 &getCachedMousePosition() const;
+	void setModifierCursorOverride(uint32 cursorID);
+	void clearModifierCursorOverride();
 
 	Common::RandomSource *getRandom() const;
 	WorldManagerInterface *getWorldManagerInterface() const;
@@ -1488,7 +1518,7 @@ private:
 
 	static bool isStructuralMouseInteractive(Structural *structural);
 	static bool isModifierMouseInteractive(Modifier *modifier);
-	static void recursiveFindMouseCollision(Structural *&bestResult, int &bestLayer, Structural *candidate, int32 relativeX, int32 relativeY);
+	static void recursiveFindMouseCollision(Structural *&bestResult, int &bestLayer, int &bestStackHeight, Structural *candidate, int stackHeight, int32 relativeX, int32 relativeY);
 
 	void queueEventAsLowLevelSceneStateTransitionAction(const Event &evt, Structural *root, bool cascade, bool relay);
 
@@ -1504,8 +1534,9 @@ private:
 	VThreadState consumeMessageTask(const ConsumeMessageTaskData &data);
 	VThreadState consumeCommandTask(const ConsumeCommandTaskData &data);
 	VThreadState updateMouseStateTask(const UpdateMouseStateTaskData &data);
-	VThreadState updateMousePositionTask1(const UpdateMousePositionTaskData &data);
-	VThreadState updateMousePositionTask2(const UpdateMousePositionTaskData &data);
+	VThreadState updateMousePositionTask(const UpdateMousePositionTaskData &data);
+
+	void updateMainWindowCursor();
 
 	Common::Array<VolumeState> _volumes;
 	Common::SharedPtr<ProjectDescription> _queuedProjectDesc;
@@ -1554,21 +1585,26 @@ private:
 	ISaveUIProvider *_saveProvider;
 	ILoadUIProvider *_loadProvider;
 
-	Graphics::Cursor *_lastFrameCursor;
-	Common::SharedPtr<Graphics::Cursor> _defaultCursor;
+	Common::SharedPtr<CursorGraphic> _lastFrameCursor;
+	Common::SharedPtr<CursorGraphic> _defaultCursor;
 
 	Common::WeakPtr<Window> _mouseFocusWindow;
-	Common::WeakPtr<Window> _keyFocusWindow;
 	bool _mouseFocusFlags[Actions::kMouseButtonCount];
 
+	Common::WeakPtr<Window> _keyFocusWindow;
+
 	ProjectPlatform _platform;
 
 	Common::SharedPtr<SystemInterface> _systemInterface;
 	Common::SharedPtr<WorldManagerInterface> _worldManagerInterface;
 	Common::SharedPtr<AssetManagerInterface> _assetManagerInterface;
 
+	// The cached mouse position is updated at frame end
 	Point16 _cachedMousePosition;
 
+	// The real mouse position is updated all the time (even when suspended)
+	Point16 _realMousePosition;
+
 	// Mouse control is tracked in two ways: Mouse over is detected with mouse movement AND when
 	// "refreshCursor" is set on the world manager, it indicates the frontmost object that
 	// responds to any mouse event.  The mouse tracking object is the object that was clicked.
@@ -1578,6 +1614,9 @@ private:
 	Common::WeakPtr<Structural> _mouseTrackingObject;
 	bool _trackedMouseOutside;
 
+	uint32 _modifierOverrideCursorID;
+	bool _haveModifierOverrideCursor;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	Common::SharedPtr<Debugger> _debugger;
 #endif
@@ -1721,6 +1760,9 @@ public:
 	Structural *getParent() const;
 	void setParent(Structural *parent);
 
+	// Helper that finds the scene containing the structural object, or itself if it is the scene
+	VisualElement *findScene();
+
 	const Common::String &getName() const;
 
 	const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const override;
@@ -1922,6 +1964,8 @@ public:
 
 	const char *findAuthorMessageName(uint32 id) const;
 
+	const Common::SharedPtr<CursorGraphicCollection> &getCursorGraphics() const;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Project"; }
 #endif
@@ -2015,6 +2059,8 @@ private:
 
 	Common::Array<Common::SharedPtr<PlugIn> > _plugIns;
 	Common::SharedPtr<ProjectResources> _resources;
+	Common::SharedPtr<CursorGraphicCollection> _cursorGraphics;
+
 	ObjectLinkingScope _structuralScope;
 	ObjectLinkingScope _modifierScope;
 
@@ -2113,6 +2159,7 @@ protected:
 	MiniscriptInstructionOutcome scriptSetVisibility(MiniscriptThread *thread, const DynamicValue &result);
 	MiniscriptInstructionOutcome scriptSetWidth(MiniscriptThread *thread, const DynamicValue &dest);
 	MiniscriptInstructionOutcome scriptSetHeight(MiniscriptThread *thread, const DynamicValue &dest);
+	MiniscriptInstructionOutcome scriptSetLayer(MiniscriptThread *thread, const DynamicValue &dest);
 
 	void offsetTranslate(int32 xDelta, int32 yDelta, bool cachedOriginOnly);
 
@@ -2123,6 +2170,8 @@ protected:
 
 	VThreadState changeVisibilityTask(const ChangeFlagTaskData &taskData);
 
+	static VisualElement *recursiveFindItemWithLayer(VisualElement *element, int32 layer);
+
 	bool _directToScreen;
 	bool _visible;
 	Rect16 _rect;
@@ -2167,6 +2216,8 @@ public:
 	Modifier();
 	virtual ~Modifier();
 
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
+
 	void materialize(Runtime *runtime, ObjectLinkingScope *outerScope);
 
 	virtual bool isAlias() const;


Commit: 6ed7aae75df95316bc6ae6c9b3c9776302c61a68
    https://github.com/scummvm/scummvm/commit/6ed7aae75df95316bc6ae6c9b3c9776302c61a68
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix "not" operator, fix If Messenger

Changed paths:
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/miniscript.h
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/mtropolis.h
    engines/mtropolis/notes/obsidian.txt
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h
    engines/mtropolis/saveload.cpp


diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 822705acb13..8414a3a2203 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -852,8 +852,6 @@ MiniscriptInstructionOutcome Not::execute(MiniscriptThread *thread) const {
 	DynamicValue &value = thread->getStackValueFromTop(0).value;
 	value.setBool(!miniscriptEvaluateTruth(value));
 
-	thread->popValues(1);
-
 	return kMiniscriptInstructionOutcomeContinue;
 }
 
@@ -1808,6 +1806,16 @@ void MiniscriptThread::jumpOffset(size_t offset) {
 	_currentInstruction += offset - 1;
 }
 
+bool MiniscriptThread::evaluateTruthOfResult(bool &isTrue) {
+	if (_stack.size() != 1) {
+		this->error("Miniscript program didn't return a result");
+		return false;
+	}
+
+	isTrue = miniscriptEvaluateTruth(_stack[0].value);
+	return true;
+}
+
 VThreadState MiniscriptThread::resumeTask(const ResumeTaskData &data) {
 	return data.thread->resume(data);
 }
diff --git a/engines/mtropolis/miniscript.h b/engines/mtropolis/miniscript.h
index ee628553215..dd51a7bc730 100644
--- a/engines/mtropolis/miniscript.h
+++ b/engines/mtropolis/miniscript.h
@@ -403,6 +403,8 @@ public:
 
 	void jumpOffset(size_t offset);
 
+	bool evaluateTruthOfResult(bool &isTrue);
+
 private:
 	struct ResumeTaskData {
 		Common::SharedPtr<MiniscriptThread> thread;
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 3a566c18584..78021dcec78 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -38,6 +38,16 @@ private:
 	RuntimeObject *_object;
 };
 
+class CompoundVarLoader : public ISaveReader {
+public:
+	explicit CompoundVarLoader(RuntimeObject *object);
+
+	bool readSave(Common::ReadStream *stream) override;
+
+private:
+	RuntimeObject *_object;
+};
+
 CompoundVarSaver::CompoundVarSaver(RuntimeObject *object) : _object(object) {
 }
 
@@ -54,6 +64,29 @@ bool CompoundVarSaver::writeSave(Common::WriteStream *stream) {
 	return !stream->err();
 }
 
+CompoundVarLoader::CompoundVarLoader(RuntimeObject *object) : _object(object) {
+}
+
+bool CompoundVarLoader::readSave(Common::ReadStream *stream) {
+	if (_object == nullptr || !_object->isModifier())
+		return false;
+
+	Modifier *modifier = static_cast<Modifier *>(_object);
+	Common::SharedPtr<ModifierSaveLoad> saveLoad = modifier->getSaveLoad();
+	if (!saveLoad)
+		return false;
+
+	if (!saveLoad->load(modifier, stream))
+		return false;
+
+	if (stream->err())
+		return false;
+
+	saveLoad->commitLoad();
+
+	return true;
+}
+
 
 
 
@@ -260,7 +293,8 @@ VThreadState SaveAndRestoreModifier::consumeMessage(Runtime *runtime, const Comm
 		runtime->getSaveProvider()->promptSave(&saver);
 		return kVThreadReturn;
 	} else if (_restoreWhen.respondsTo(msg->getEvent())) {
-		error("Restores not implemented yet");
+		CompoundVarLoader loader(obj);
+		runtime->getLoadProvider()->promptLoad(&loader);
 		return kVThreadReturn;
 	}
 
@@ -600,6 +634,24 @@ bool IfMessengerModifier::load(ModifierLoaderContext &context, const Data::IfMes
 	return true;
 }
 
+bool IfMessengerModifier::respondsToEvent(const Event &evt) const {
+	return _when.respondsTo(evt);
+}
+
+VThreadState IfMessengerModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	if (_when.respondsTo(msg->getEvent())) {
+		Common::SharedPtr<MiniscriptThread> thread(new MiniscriptThread(runtime, msg, _program, _references, this));
+
+		EvaluateAndSendTaskData *evalAndSendData = runtime->getVThread().pushTask("IfMessengerModifier::evaluateAndSendTask", this, &IfMessengerModifier::evaluateAndSendTask);
+		evalAndSendData->thread = thread;
+		evalAndSendData->runtime = runtime;
+
+		MiniscriptThread::runOnVThread(runtime->getVThread(), thread);
+	}
+
+	return kVThreadReturn;
+}
+
 Common::SharedPtr<Modifier> IfMessengerModifier::shallowClone() const {
 	IfMessengerModifier *clonePtr = new IfMessengerModifier(*this);
 	Common::SharedPtr<Modifier> clone(clonePtr);
@@ -609,6 +661,29 @@ Common::SharedPtr<Modifier> IfMessengerModifier::shallowClone() const {
 
 	return clone;
 }
+void IfMessengerModifier::linkInternalReferences(ObjectLinkingScope *scope) {
+	_sendSpec.linkInternalReferences(scope);
+	_references->linkInternalReferences(scope);
+}
+
+void IfMessengerModifier::visitInternalReferences(IStructuralReferenceVisitor *visitor) {
+	_sendSpec.visitInternalReferences(visitor);
+	_references->visitInternalReferences(visitor);
+}
+
+
+VThreadState IfMessengerModifier::evaluateAndSendTask(const EvaluateAndSendTaskData &taskData) {
+	MiniscriptThread *thread = taskData.thread.get();
+
+	bool isTrue = false;
+	if (!thread->evaluateTruthOfResult(isTrue))
+		return kVThreadError;
+
+	if (isTrue)
+		_sendSpec.sendFromMessenger(taskData.runtime, this);
+
+	return kVThreadReturn;
+}
 
 TimerMessengerModifier::~TimerMessengerModifier() {
 	if (_scheduledEvent)
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 2ace965ffb7..63d733cd7de 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -337,12 +337,25 @@ class IfMessengerModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::IfMessengerModifier &data);
 
+	bool respondsToEvent(const Event &evt) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "If Messenger Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
+	struct EvaluateAndSendTaskData {
+		Common::SharedPtr<MiniscriptThread> thread;
+		Runtime *runtime;
+	};
+
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	void linkInternalReferences(ObjectLinkingScope *scope) override;
+	void visitInternalReferences(IStructuralReferenceVisitor *visitor) override;
+
+	VThreadState evaluateAndSendTask(const EvaluateAndSendTaskData &taskData);
 
 	Event _when;
 	MessengerSendSpec _sendSpec;
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index 70a03a07727..caf1906784e 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -270,7 +270,7 @@ Common::Error MTropolisEngine::run() {
 	int preferredHeight = 768;
 	ColorDepthMode preferredColorDepthMode = kColorDepthMode8Bit;
 
-	_runtime.reset(new Runtime(_system, this, nullptr));
+	_runtime.reset(new Runtime(_system, this, this));
 
 	if (_gameDescription->gameID == GID_OBSIDIAN && _gameDescription->desc.platform == Common::kPlatformWindows) {
 		preferredWidth = 640;
diff --git a/engines/mtropolis/mtropolis.h b/engines/mtropolis/mtropolis.h
index 2a3ec76d025..0e9936320c8 100644
--- a/engines/mtropolis/mtropolis.h
+++ b/engines/mtropolis/mtropolis.h
@@ -42,7 +42,7 @@ namespace MTropolis {
 class Runtime;
 class RuntimeObject;
 
-class MTropolisEngine : public ::Engine, public ISaveUIProvider {
+class MTropolisEngine : public ::Engine, public ISaveUIProvider, public ILoadUIProvider {
 protected:
 	// Engine APIs
 	Common::Error run() override;
@@ -60,7 +60,7 @@ public:
 	Common::Platform getPlatform() const;
 
 	bool promptSave(ISaveWriter *writer) override;
-	//bool promptLoad(ISaveReader *reader) override;
+	bool promptLoad(ISaveReader *reader) override;
 
 public:
 	void handleEvents();
diff --git a/engines/mtropolis/notes/obsidian.txt b/engines/mtropolis/notes/obsidian.txt
index 42a2964564a..07404858b03 100644
--- a/engines/mtropolis/notes/obsidian.txt
+++ b/engines/mtropolis/notes/obsidian.txt
@@ -1,3 +1,8 @@
 MIDI behavior:
 
 Uses a special MIDI file "ANO&Vol.mid" which triggers AllNoteOff on all channels.
+
+
+Navigation mapping:
+	current  fwright right       rear       left fwleft
+	+0       +1      +2    +3    +4   +5    +6   +7
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 558bcfad505..f497cf0eaff 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -2849,15 +2849,10 @@ VThreadState MessageDispatch::continuePropagating(Runtime *runtime) {
 
 				// Post to the message action itself to VThread
 				if (responds) {
-					if (modifier->getStaticGUID() == 0x5b965)
-					{
-						int n = 0;
-					}
 					debug(3, "Modifier %x '%s' consumed message (%i,%i)", modifier->getStaticGUID(), modifier->getName().c_str(), _msg->getEvent().eventType, _msg->getEvent().eventInfo);
 					runtime->postConsumeMessageTask(modifier, _msg);
+					return kVThreadReturn;
 				}
-
-				return kVThreadReturn;
 			} break;
 		case PropagationStack::kStageSendToModifierContainer: {
 				IModifierContainer *container = stackTop.ptr.modifierContainer;
@@ -5801,6 +5796,9 @@ bool VisualElement::readAttribute(MiniscriptThread *thread, DynamicValue &result
 	} else if (attrib == "position") {
 		result.setPoint(Point16::create(_rect.left, _rect.top));
 		return true;
+	} else if (attrib == "centerposition") {
+		result.setPoint(getCenterPosition());
+		return true;
 	} else if (attrib == "width") {
 		result.setInt(_rect.right - _rect.left);
 		return true;
@@ -5828,6 +5826,9 @@ MiniscriptInstructionOutcome VisualElement::writeRefAttribute(MiniscriptThread *
 	} else if (attrib == "position") {
 		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetPosition>::create(this, writeProxy);
 		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "centerposition") {
+		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetCenterPosition>::create(this, writeProxy);
+		return kMiniscriptInstructionOutcomeContinue;
 	} else if (attrib == "width") {
 		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetWidth>::create(this, writeProxy);
 		return kMiniscriptInstructionOutcomeContinue;
@@ -5922,6 +5923,21 @@ MiniscriptInstructionOutcome VisualElement::scriptSetPosition(MiniscriptThread *
 	return kMiniscriptInstructionOutcomeFailed;
 }
 
+MiniscriptInstructionOutcome VisualElement::scriptSetCenterPosition(MiniscriptThread *thread, const DynamicValue &value) {
+	if (value.getType() == DynamicValueTypes::kPoint) {
+		const Point16 &destPoint = value.getPoint();
+		const Point16 &srcPoint = getCenterPosition();
+		int32 xDelta = destPoint.x - srcPoint.x;
+		int32 yDelta = destPoint.y - srcPoint.y;
+
+		if (xDelta != 0 || yDelta != 0)
+			offsetTranslate(xDelta, yDelta, false);
+
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+	return kMiniscriptInstructionOutcomeFailed;
+}
+
 MiniscriptInstructionOutcome VisualElement::scriptSetWidth(MiniscriptThread *thread, const DynamicValue &value) {
 	int32 asInteger = 0;
 	if (!value.roundToInt(asInteger))
@@ -5982,6 +5998,10 @@ void VisualElement::offsetTranslate(int32 xDelta, int32 yDelta, bool cachedOrigi
 	}
 }
 
+Point16 VisualElement::getCenterPosition() const {
+	return Point16::create((_rect.left + _rect.right) / 2, (_rect.top + _rect.bottom) / 2);
+}
+
 VThreadState VisualElement::changeVisibilityTask(const ChangeFlagTaskData &taskData) {
 	if (_visible != taskData.desiredFlag) {
 		_visible = taskData.desiredFlag;
@@ -6065,7 +6085,7 @@ bool ModifierSaveLoad::load(Modifier *modifier, Common::ReadStream *stream) {
 
 	if (nameLen > 0) {
 		stream->read(&checkName[0], nameLen);
-		if (stream->err() || !memcmp(&checkName[0], name.c_str(), nameLen))
+		if (stream->err() || memcmp(&checkName[0], name.c_str(), nameLen))
 			return false;
 	}
 
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 363923903d1..9f86578de57 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -2156,6 +2156,7 @@ protected:
 
 	MiniscriptInstructionOutcome scriptSetDirect(MiniscriptThread *thread, const DynamicValue &dest);
 	MiniscriptInstructionOutcome scriptSetPosition(MiniscriptThread *thread, const DynamicValue &dest);
+	MiniscriptInstructionOutcome scriptSetCenterPosition(MiniscriptThread *thread, const DynamicValue &dest);
 	MiniscriptInstructionOutcome scriptSetVisibility(MiniscriptThread *thread, const DynamicValue &result);
 	MiniscriptInstructionOutcome scriptSetWidth(MiniscriptThread *thread, const DynamicValue &dest);
 	MiniscriptInstructionOutcome scriptSetHeight(MiniscriptThread *thread, const DynamicValue &dest);
@@ -2163,6 +2164,8 @@ protected:
 
 	void offsetTranslate(int32 xDelta, int32 yDelta, bool cachedOriginOnly);
 
+	Point16 getCenterPosition() const;
+
 	struct ChangeFlagTaskData {
 		bool desiredFlag;
 		Runtime *runtime;
diff --git a/engines/mtropolis/saveload.cpp b/engines/mtropolis/saveload.cpp
index 6cdc6bced9e..0995f18f47c 100644
--- a/engines/mtropolis/saveload.cpp
+++ b/engines/mtropolis/saveload.cpp
@@ -29,11 +29,10 @@
 namespace MTropolis {
 
 bool MTropolisEngine::promptSave(ISaveWriter *writer) {
-	GUI::SaveLoadChooser *dialog;
 	Common::String desc;
 	int slot;
 
-	dialog = new GUI::SaveLoadChooser(_("Save game:"), _("Save"), true);
+	Common::SharedPtr<GUI::SaveLoadChooser> dialog(new GUI::SaveLoadChooser(_("Save game:"), _("Save"), true));
 
 	slot = dialog->runModalWithCurrentTarget();
 	desc = dialog->getResultString();
@@ -43,8 +42,6 @@ bool MTropolisEngine::promptSave(ISaveWriter *writer) {
 		desc = dialog->createDefaultSaveDescription(slot);
 	}
 
-	delete dialog;
-
 	if (slot < 0)
 		return true;
 
@@ -58,4 +55,26 @@ bool MTropolisEngine::promptSave(ISaveWriter *writer) {
 	return true;
 }
 
+bool MTropolisEngine::promptLoad(ISaveReader *reader) {
+	Common::String desc;
+	int slot;
+
+	{
+		Common::SharedPtr<GUI::SaveLoadChooser> dialog(new GUI::SaveLoadChooser(_("Restore game:"), _("Restore"), false));
+		slot = dialog->runModalWithCurrentTarget();
+	}
+
+	if (slot < 0)
+		return true;
+
+	Common::String saveFileName = getSaveStateName(slot);
+	Common::SharedPtr<Common::InSaveFile> in(_saveFileMan->openForLoading(saveFileName));
+	if (!reader->readSave(in.get())) {
+		warning("An error occurred while reading file '%s'", saveFileName.c_str());
+		return false;
+	}
+
+	return true;
+}
+
 } // End of namespace MTropolis


Commit: da5f851633f83c966686de297f311abc84610954
    https://github.com/scummvm/scummvm/commit/da5f851633f83c966686de297f311abc84610954
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix corrupted lists in saves

Changed paths:
    engines/mtropolis/plugin/standard.cpp


diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index c3dc430fab3..1abe659cec9 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -994,7 +994,7 @@ Common::SharedPtr<Modifier> ListVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new ListVariableModifier(*this));
 }
 
-ListVariableModifier::SaveLoad::SaveLoad(ListVariableModifier *modifier) : _modifier(modifier), _list(new DynamicList()) {
+ListVariableModifier::SaveLoad::SaveLoad(ListVariableModifier *modifier) : _modifier(modifier), _list(_modifier->_list) {
 }
 
 void ListVariableModifier::SaveLoad::commitLoad() const {
@@ -1046,6 +1046,8 @@ void ListVariableModifier::SaveLoad::saveInternal(Common::WriteStream *stream) c
 }
 
 bool ListVariableModifier::SaveLoad::loadInternal(Common::ReadStream *stream) {
+	_list.reset(new DynamicList());
+
 	uint32 typeCode = stream->readUint32BE();
 	uint32 size = stream->readUint32BE();
 


Commit: db48ad664086095f492c1b8403ad8d3b06ab401a
    https://github.com/scummvm/scummvm/commit/db48ad664086095f492c1b8403ad8d3b06ab401a
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: C++11 compile fixes, debug inspector base work

Changed paths:
    engines/mtropolis/debug.cpp
    engines/mtropolis/debug.h
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h
    engines/mtropolis/vthread.cpp
    engines/mtropolis/vthread.h


diff --git a/engines/mtropolis/debug.cpp b/engines/mtropolis/debug.cpp
index 521ded89fa3..d64b0289c40 100644
--- a/engines/mtropolis/debug.cpp
+++ b/engines/mtropolis/debug.cpp
@@ -778,6 +778,7 @@ void DebugSceneTreeWindow::toolRenderSurface(int32 subAreaWidth, int32 subAreaHe
 		bool isSelected = entry.uiState.selected;
 		if (isSelected)
 			_toolSurface->fillRect(Common::Rect(textX - 1, rowTopY, textX + strWidth + 1, rowBottomY), blackColor);
+
 		font->drawString(_toolSurface.get(), name, textX, y, strWidth, isSelected ? whiteColor : blackColor, Graphics::kTextAlignLeft, 0, true);
 
 		if (entry.hasChildren) {
@@ -871,6 +872,16 @@ void DebugSceneTreeWindow::toolOnMouseDown(int32 x, int32 y, int mouseButton) {
 		treeEntry.uiState.expanded = !treeEntry.uiState.expanded;
 		_forceRender = true;
 		return;
+	} else if (x >= kBaseLeftPadding + treeEntry.level * kPerLevelSpacing) {
+		if (!treeEntry.uiState.selected) {
+			for (SceneTreeEntry &clearTreeEntry : _tree)
+				clearTreeEntry.uiState.selected = false;
+
+			treeEntry.uiState.selected = true;
+			setDirty();
+
+			_debugger->tryInspectObject(treeEntry.object.lock().get());
+		}
 	}
 }
 
@@ -922,6 +933,115 @@ uint32 DebugSceneTreeWindow::getColorForObject(const RuntimeObject *object, cons
 	}
 }
 
+
+class DebugInspectorWindow : public DebugToolWindowBase, private IDebugInspectionReport {
+public:
+	DebugInspectorWindow(Debugger *debugger, const WindowParameters &windowParams);
+
+	void update() override;
+	void toolRenderSurface(int32 subAreaWidth, int32 subAreaHeight) override;
+
+private:
+	static const int kRowHeight = 14;
+
+	struct InspectorLabeledRow {
+		Common::String label;
+		Common::String text;
+	};
+
+	struct InspectorUnlabeledRow {
+		Common::String str;
+	};
+
+	bool declareStatic(const char *name) override;
+	void declareStaticContents(const Common::String &data) override;
+	void declareDynamic(const char *name, const Common::String &data) override;
+	void declareLoose(const char *name, const Common::String &data) override;
+
+	Common::SharedPtr<DebugInspector> _inspector;
+
+	Common::Array<InspectorLabeledRow> _labeledRow;
+	Common::Array<InspectorUnlabeledRow> _unlabeledRow;
+
+	int32 _maxLabelWidth;
+	size_t _declLabeledRow;
+	size_t _declUnlabeledRow;
+};
+
+DebugInspectorWindow::DebugInspectorWindow(Debugger *debugger, const WindowParameters &windowParams)
+	: DebugToolWindowBase(kDebuggerToolInspector, "Inspector", debugger, windowParams), _maxLabelWidth(0), _declLabeledRow(0), _declUnlabeledRow(0) {
+}
+
+void DebugInspectorWindow::update() {
+	const Common::SharedPtr<DebugInspector> inspector = _debugger->getInspector();
+
+	bool inspectorChanged = false;
+	if (inspector != _inspector) {
+		_maxLabelWidth = 0;
+		_labeledRow.clear();
+		_unlabeledRow.clear();
+
+		_inspector = inspector;
+		inspectorChanged = true;
+	}
+
+	_declLabeledRow = 0;
+	_declUnlabeledRow = 0;
+
+	if (inspector == nullptr || inspector->getDebuggable() == nullptr) {
+		_unlabeledRow.resize(0);
+		_unlabeledRow[0].str = "No object selected";
+
+		_labeledRow.clear();
+	} else {
+		size_t oldNumLabeled = _unlabeledRow.size();
+		inspector->getDebuggable()->debugInspect(this);
+
+		_unlabeledRow.resize(_declUnlabeledRow);
+	}
+}
+
+bool DebugInspectorWindow::declareStatic(const char *name) {
+	if (_labeledRow.size() <= _declLabeledRow) {
+		InspectorLabeledRow newRow;
+		newRow.label = name;
+		_labeledRow.push_back(newRow);
+
+		return true;
+	} else {
+		_declLabeledRow++;
+		return false;
+	}
+}
+
+void DebugInspectorWindow::declareStaticContents(const Common::String &data) {
+	assert(_declLabeledRow + 1 == _labeledRow.size());
+	_labeledRow[_declLabeledRow].text = data;
+
+	_declLabeledRow++;
+}
+
+void DebugInspectorWindow::declareDynamic(const char *name, const Common::String &data) {
+	if (_declLabeledRow == _labeledRow.size()) {
+		InspectorLabeledRow row;
+		row.label = name;
+		_labeledRow.push_back(row);
+	}
+	_labeledRow[_declLabeledRow].text = data;
+}
+
+void DebugInspectorWindow::declareLoose(const char *name, const Common::String &data) {
+	if (_declLabeledRow == _labeledRow.size()) {
+		InspectorLabeledRow row;
+		row.label = name;
+		_labeledRow.push_back(row);
+	}
+	_labeledRow[_declLabeledRow].text = data;
+}
+
+void DebugInspectorWindow::toolRenderSurface(int32 subAreaWidth, int32 subAreaHeight) {
+}
+
 // Step through ("debugger") window
 class DebugStepThroughWindow : public DebugToolWindowBase {
 public:
@@ -1028,6 +1148,24 @@ void DebugToolsWindow::onMouseDown(int32 x, int32 y, int mouseButton) {
 	_debugger->openToolWindow(static_cast<DebuggerTool>(tool));
 }
 
+Debuggable::Debuggable() {
+}
+
+Debuggable::Debuggable(const Debuggable &other) : _inspector(other._inspector) {
+	if (_inspector)
+		_inspector->changePrimaryInstance(this);
+}
+
+Debuggable::~Debuggable() {
+	if (_inspector)
+		_inspector->onDestroyed(this);
+}
+
+const Common::SharedPtr<DebugInspector> &Debuggable::debugGetInspector() {
+	if (!_inspector)
+		_inspector.reset(new DebugInspector(this));
+	return _inspector;
+}
 
 DebugInspector::DebugInspector(IDebuggable *debuggable) {
 }
@@ -1035,14 +1173,20 @@ DebugInspector::DebugInspector(IDebuggable *debuggable) {
 DebugInspector::~DebugInspector() {
 }
 
-void DebugInspector::onDestroyed() {
-	_debuggable = nullptr;
+void DebugInspector::onDestroyed(IDebuggable *debuggable) {
+	if (_instance == debuggable)
+		_instance = nullptr;
 }
 
-void DebugInspector::onDebuggableRelocated(IDebuggable *debuggable) {
-	_debuggable = debuggable;
+void DebugInspector::changePrimaryInstance(IDebuggable *instance) {
+	_instance = instance;
 }
 
+IDebuggable *DebugInspector::getDebuggable() const {
+	return _instance;
+}
+
+
 DebugPrimaryTaskList::DebugPrimaryTaskList(const Common::String &name) : _name(name) {
 }
 
@@ -1313,6 +1457,28 @@ void Debugger::closeToolWindow(DebuggerTool tool) {
 	_toolWindows[tool].reset();
 }
 
+void Debugger::inspectObject(IDebuggable *debuggable) {
+	_inspector = debuggable->debugGetInspector();
+
+	if (!_toolWindows[kDebuggerToolInspector])
+		openToolWindow(kDebuggerToolInspector);
+}
+
+void Debugger::tryInspectObject(RuntimeObject *object) {
+	if (!object)
+		return;
+
+	if (object->isStructural())
+		inspectObject(static_cast<Structural *>(object));
+	else if (object->isModifier())
+		inspectObject(static_cast<Modifier *>(object));
+}
+
+const Common::SharedPtr<DebugInspector>& Debugger::getInspector() const {
+	return _inspector;
+}
+
+
 void Debugger::scanStructuralStatus(Structural *structural, Common::HashMap<Common::String, SupportStatus> &unfinishedModifiers, Common::HashMap<Common::String, SupportStatus> &unfinishedElements) {
 	for (Common::Array<Common::SharedPtr<Structural>>::const_iterator it = structural->getChildren().begin(), itEnd = structural->getChildren().end(); it != itEnd; ++it) {
 		scanStructuralStatus(it->get(), unfinishedModifiers, unfinishedElements);
@@ -1336,7 +1502,7 @@ void Debugger::scanModifierStatus(Modifier *modifier, Common::HashMap<Common::St
 	scanDebuggableStatus(modifier, unfinishedModifiers);
 }
 
-void Debugger::scanDebuggableStatus(IDebuggable* debuggable, Common::HashMap<Common::String, SupportStatus>& unfinished) {
+void Debugger::scanDebuggableStatus(IDebuggable *debuggable, Common::HashMap<Common::String, SupportStatus> &unfinished) {
 	SupportStatus supportStatus = debuggable->debugGetSupportStatus();
 	if (supportStatus != kSupportStatusDone)
 		unfinished[Common::String(debuggable->debugGetTypeName())] = supportStatus;
diff --git a/engines/mtropolis/debug.h b/engines/mtropolis/debug.h
index 5ee6ffbc937..f69a93bcb63 100644
--- a/engines/mtropolis/debug.h
+++ b/engines/mtropolis/debug.h
@@ -42,7 +42,9 @@ class Runtime;
 class Window;
 class Structural;
 class Modifier;
+class DebugInspector;
 class DebugToolWindowBase;
+class RuntimeObject;
 
 struct IDebuggable;
 
@@ -52,17 +54,56 @@ enum SupportStatus {
 	kSupportStatusDone,
 };
 
+struct IDebugInspectionReport {
+	// Attempts to declare a row with static contents.  If this returns true, then declareStaticContents must be called.
+	virtual bool declareStatic(const char *name) = 0;
+
+	// Declares the contents of a static row
+	virtual void declareStaticContents(const Common::String &data) = 0;
+
+	// Declares the contents of a dynamic row
+	virtual void declareDynamic(const char *name, const Common::String &data) = 0;
+
+	// Declares the contents of a loose row
+	virtual void declareLoose(const char *name, const Common::String &data) = 0;
+};
+
+struct IDebuggable {
+	virtual SupportStatus debugGetSupportStatus() const = 0;
+	virtual const char *debugGetTypeName() const = 0;
+	virtual const Common::String &debugGetName() const = 0;
+	virtual const Common::SharedPtr<DebugInspector> &debugGetInspector() = 0;
+	virtual void debugInspect(IDebugInspectionReport *report) const = 0;
+};
+
+// The debug inspector link goes inside of a debuggable, it contains an inspector and
+// will notify the inspector of the object's destruction
+class Debuggable : public IDebuggable {
+public:
+	Debuggable();
+	Debuggable(const Debuggable &other);
+	~Debuggable();
+
+private:
+	const Common::SharedPtr<DebugInspector> &debugGetInspector() override;
+
+	Debuggable &operator=(const Debuggable &other);
+
+	Common::SharedPtr<DebugInspector> _inspector;
+};
+
 class DebugInspector {
 public:
 	explicit DebugInspector(IDebuggable *debuggable);
 	virtual ~DebugInspector();
 
-	virtual void onDestroyed();
+	void onDestroyed(IDebuggable *instance);
+	void changePrimaryInstance(IDebuggable *instance);
 
-	void onDebuggableRelocated(IDebuggable *debuggable);
+	IDebuggable *getDebuggable() const;
 
 private:
-	IDebuggable *_debuggable;
+	IDebuggable *_instance;
 };
 
 class DebugPrimaryTaskList {
@@ -123,6 +164,11 @@ public:
 	void openToolWindow(DebuggerTool tool);
 	void closeToolWindow(DebuggerTool tool);
 
+	void inspectObject(IDebuggable *debuggable);
+	void tryInspectObject(RuntimeObject *object);
+
+	const Common::SharedPtr<DebugInspector> &getInspector() const;
+
 private:
 	Debugger();
 
@@ -141,19 +187,19 @@ private:
 	Common::SharedPtr<Window> _toolsWindow;
 	Common::SharedPtr<DebugToolWindowBase> _toolWindows[kDebuggerToolCount];
 	Common::Array<ToastNotification> _toastNotifications;
+	Common::SharedPtr<DebugInspector> _inspector;
 };
 
-#endif /* !MTROPOLIS_DEBUG_ENABLE */
+#else
 
 struct IDebuggable {
-#ifdef MTROPOLIS_DEBUG_ENABLE
-	virtual SupportStatus debugGetSupportStatus() const = 0;
-	virtual const char *debugGetTypeName() const = 0;
-	virtual const Common::String &debugGetName() const = 0;
-	virtual Common::SharedPtr<DebugInspector> debugGetInspector() const = 0;
-#endif
 };
 
+struct Debuggable : public IDebuggable {
+};
+
+#endif /* !MTROPOLIS_DEBUG_ENABLE */
+
 } // End of namespace MTropolis
 
 #endif /* MTROPOLIS_DEBUG_H */
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 8414a3a2203..0d939becabd 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -364,7 +364,7 @@ bool MiniscriptParser::parse(const Data::MiniscriptProgram &program, Common::Sha
 	}
 
 	programData.resize(programDataSize + maxAlignment - 1);
-	uintptr_t programDataAddress = reinterpret_cast<uintptr_t>(&programData[0]);
+	uintptr programDataAddress = reinterpret_cast<uintptr>(&programData[0]);
 
 	size_t baseOffset = 0;
 	if (programDataAddress % maxAlignment != 0)
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index f497cf0eaff..4ebba180cac 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -948,13 +948,13 @@ void DynamicList::initFromOther(const DynamicList &other) {
 	}
 }
 
-MiniscriptInstructionOutcome DynamicList::WriteProxyInterface::write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const {
+MiniscriptInstructionOutcome DynamicList::WriteProxyInterface::write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr ptrOrOffset) const {
 	if (!static_cast<DynamicList *>(objectRef)->setAtIndex(ptrOrOffset, value))
 		return kMiniscriptInstructionOutcomeFailed;
 	return kMiniscriptInstructionOutcomeContinue;
 }
 
-MiniscriptInstructionOutcome DynamicList::WriteProxyInterface::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const {
+MiniscriptInstructionOutcome DynamicList::WriteProxyInterface::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const {
 	DynamicList *list = static_cast<DynamicList *>(objectRef);
 	bool succeeded = false;
 	switch (list->getType()) {
@@ -988,7 +988,7 @@ MiniscriptInstructionOutcome DynamicList::WriteProxyInterface::refAttrib(Miniscr
 	return kMiniscriptInstructionOutcomeFailed;
 }
 
-MiniscriptInstructionOutcome DynamicList::WriteProxyInterface::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const {
+MiniscriptInstructionOutcome DynamicList::WriteProxyInterface::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const {
 	DynamicList *list = static_cast<DynamicList *>(objectRef);
 	switch (list->getType()) {
 	case DynamicValueTypes::kList: {
@@ -1495,7 +1495,7 @@ void DynamicValue::initFromOther(const DynamicValue &other) {
 	_type = other._type;
 }
 
-MiniscriptInstructionOutcome DynamicValueWriteStringHelper::write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const {
+MiniscriptInstructionOutcome DynamicValueWriteStringHelper::write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr ptrOrOffset) const {
 	Common::String &dest = *static_cast<Common::String *>(objectRef);
 	switch (value.getType()) {
 	case DynamicValueTypes::kString:
@@ -1506,11 +1506,11 @@ MiniscriptInstructionOutcome DynamicValueWriteStringHelper::write(MiniscriptThre
 	}
 }
 
-MiniscriptInstructionOutcome DynamicValueWriteStringHelper::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const {
+MiniscriptInstructionOutcome DynamicValueWriteStringHelper::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const {
 	return kMiniscriptInstructionOutcomeFailed;
 }
 
-MiniscriptInstructionOutcome DynamicValueWriteStringHelper::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const {
+MiniscriptInstructionOutcome DynamicValueWriteStringHelper::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const {
 	return kMiniscriptInstructionOutcomeFailed;
 }
 
@@ -1522,16 +1522,16 @@ void DynamicValueWriteStringHelper::create(Common::String *strValue, DynamicValu
 
 DynamicValueWriteStringHelper DynamicValueWriteStringHelper::_instance;
 
-MiniscriptInstructionOutcome DynamicValueWriteObjectHelper::write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const {
+MiniscriptInstructionOutcome DynamicValueWriteObjectHelper::write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr ptrOrOffset) const {
 	thread->error("Can't write to read-only object value");
 	return kMiniscriptInstructionOutcomeFailed;
 }
 
-MiniscriptInstructionOutcome DynamicValueWriteObjectHelper::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const {
+MiniscriptInstructionOutcome DynamicValueWriteObjectHelper::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const {
 	return static_cast<RuntimeObject *>(objectRef)->writeRefAttribute(thread, proxy, attrib);
 }
 
-MiniscriptInstructionOutcome DynamicValueWriteObjectHelper::refAttribIndexed(MiniscriptThread* thread, DynamicValueWriteProxy& proxy, void* objectRef, uintptr_t ptrOrOffset, const Common::String& attrib, const DynamicValue& index) const {
+MiniscriptInstructionOutcome DynamicValueWriteObjectHelper::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const {
 	return static_cast<RuntimeObject *>(objectRef)->writeRefAttributeIndexed(thread, proxy, attrib, index);
 }
 
@@ -2299,10 +2299,6 @@ Structural::Structural() : _parent(nullptr), _paused(false), _loop(false) {
 }
 
 Structural::~Structural() {
-#ifdef MTROPOLIS_DEBUG_ENABLE
-	if (_debugInspector)
-		_debugInspector->onDestroyed();
-#endif
 }
 
 ProjectPresentationSettings::ProjectPresentationSettings() : width(640), height(480), bitsPerPixel(8) {
@@ -2531,10 +2527,6 @@ void Structural::materializeSelfAndDescendents(Runtime *runtime, ObjectLinkingSc
 	setRuntimeGUID(runtime->allocateRuntimeGUID());
 
 	materializeDescendents(runtime, outerScope);
-
-#ifdef MTROPOLIS_DEBUG_ENABLE
-	_debugInspector.reset(this->debugCreateInspector());
-#endif
 }
 
 void Structural::materializeDescendents(Runtime *runtime, ObjectLinkingScope *outerScope) {
@@ -2616,13 +2608,13 @@ const Common::String &Structural::debugGetName() const {
 	return _name;
 }
 
-Common::SharedPtr<DebugInspector> Structural::debugGetInspector() const {
-	return _debugInspector;
+void Structural::debugInspect(IDebugInspectionReport *report) const {
+	if (report->declareStatic("type"))
+		report->declareStaticContents(debugGetTypeName());
+	if (report->declareStatic("guid"))
+		report->declareStaticContents(Common::String::format("%x", getStaticGUID()));
 }
 
-DebugInspector *Structural::debugCreateInspector() {
-	return new DebugInspector(this);
-}
 #endif /* MTROPOLIS_DEBUG_ENABLE */
 
 void Structural::linkInternalReferences(ObjectLinkingScope *scope) {
@@ -6093,16 +6085,9 @@ bool ModifierSaveLoad::load(Modifier *modifier, Common::ReadStream *stream) {
 }
 
 Modifier::Modifier() : _parent(nullptr) {
-#ifdef MTROPOLIS_DEBUG_ENABLE
-	_debugger = nullptr;
-#endif
 }
 
 Modifier::~Modifier() {
-#ifdef MTROPOLIS_DEBUG_ENABLE
-	if (_debugInspector)
-		_debugInspector->onDestroyed();
-#endif
 }
 
 bool Modifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
@@ -6148,11 +6133,6 @@ void Modifier::materialize(Runtime *runtime, ObjectLinkingScope *outerScope) {
 
 	linkInternalReferences(outerScope);
 	setRuntimeGUID(runtime->allocateRuntimeGUID());
-
-#ifdef MTROPOLIS_DEBUG_ENABLE
-	_debugger = runtime->debugGetDebugger();
-	_debugInspector.reset(this->debugCreateInspector());
-#endif
 }
 
 bool Modifier::isAlias() const {
@@ -6258,14 +6238,13 @@ const Common::String &Modifier::debugGetName() const {
 	return _name;
 }
 
-Common::SharedPtr<DebugInspector> Modifier::debugGetInspector() const {
-	return _debugInspector;
+void Modifier::debugInspect(IDebugInspectionReport *report) const {
+	if (report->declareStatic("type"))
+		report->declareStaticContents(debugGetTypeName());
+	if (report->declareStatic("guid"))
+		report->declareStaticContents(Common::String::format("%x", getStaticGUID()));
 }
 
-DebugInspector *Modifier::debugCreateInspector() {
-	return new DebugInspector(this);
-
-}
 #endif /* MTROPOLIS_DEBUG_ENABLE */
 
 bool VariableModifier::isVariable() const {
@@ -6289,17 +6268,17 @@ DynamicValueWriteProxy VariableModifier::createWriteProxy() {
 	return proxy;
 }
 
-MiniscriptInstructionOutcome VariableModifier::WriteProxyInterface::write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const {
+MiniscriptInstructionOutcome VariableModifier::WriteProxyInterface::write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr ptrOrOffset) const {
 	if (!static_cast<VariableModifier *>(objectRef)->varSetValue(thread, dest))
 		return kMiniscriptInstructionOutcomeFailed;
 	return kMiniscriptInstructionOutcomeContinue;
 }
 
-MiniscriptInstructionOutcome VariableModifier::WriteProxyInterface::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &dest, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const {
+MiniscriptInstructionOutcome VariableModifier::WriteProxyInterface::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &dest, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const {
 	return static_cast<VariableModifier *>(objectRef)->writeRefAttribute(thread, dest, attrib);
 }
 
-MiniscriptInstructionOutcome VariableModifier::WriteProxyInterface::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &dest, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const {
+MiniscriptInstructionOutcome VariableModifier::WriteProxyInterface::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &dest, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const {
 	return static_cast<VariableModifier *>(objectRef)->writeRefAttributeIndexed(thread, dest, attrib, index);
 }
 
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 9f86578de57..3b9a4b9f7ee 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -459,19 +459,19 @@ struct DynamicValue;
 struct DynamicList;
 
 struct IDynamicValueReadInterface {
-	virtual MiniscriptInstructionOutcome read(MiniscriptThread *thread, DynamicValue &dest, const void *objectRef, uintptr_t ptrOrOffset) const = 0;
-	virtual MiniscriptInstructionOutcome readAttrib(MiniscriptThread *thread, DynamicValue &dest, const void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const = 0;
-	virtual MiniscriptInstructionOutcome readAttribIndexed(MiniscriptThread *thread, DynamicValue &dest, const void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const = 0;
+	virtual MiniscriptInstructionOutcome read(MiniscriptThread *thread, DynamicValue &dest, const void *objectRef, uintptr ptrOrOffset) const = 0;
+	virtual MiniscriptInstructionOutcome readAttrib(MiniscriptThread *thread, DynamicValue &dest, const void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const = 0;
+	virtual MiniscriptInstructionOutcome readAttribIndexed(MiniscriptThread *thread, DynamicValue &dest, const void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const = 0;
 };
 
 struct IDynamicValueWriteInterface {
-	virtual MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const = 0;
-	virtual MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const = 0;
-	virtual MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const = 0;
+	virtual MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr ptrOrOffset) const = 0;
+	virtual MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const = 0;
+	virtual MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const = 0;
 };
 
 struct DynamicValueReadProxyPOD {
-	uintptr_t ptrOrOffset;
+	uintptr ptrOrOffset;
 	const void *objectRef;
 	IDynamicValueReadInterface *ifc;
 };
@@ -482,7 +482,7 @@ struct DynamicValueReadProxy {
 };
 
 struct DynamicValueWriteProxyPOD {
-	uintptr_t ptrOrOffset;
+	uintptr ptrOrOffset;
 	void *objectRef;
 	IDynamicValueWriteInterface *ifc;
 };
@@ -737,9 +737,9 @@ struct DynamicList {
 
 private:
 	struct WriteProxyInterface : public IDynamicValueWriteInterface {
-		MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const override;
-		MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override;
-		MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
+		MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr ptrOrOffset) const override;
+		MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const override;
+		MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
 
 		static WriteProxyInterface _instance;
 	};
@@ -845,7 +845,7 @@ private:
 
 template<class TFloat>
 struct DynamicValueWriteFloatHelper : public IDynamicValueWriteInterface {
-	MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const override {
+	MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr ptrOrOffset) const override {
 		TFloat &dest = *static_cast<TFloat *>(objectRef);
 		switch (value.getType()) {
 		case DynamicValueTypes::kFloat:
@@ -858,10 +858,10 @@ struct DynamicValueWriteFloatHelper : public IDynamicValueWriteInterface {
 			return kMiniscriptInstructionOutcomeFailed;
 		}
 	}
-	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override {
+	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const override {
 		return kMiniscriptInstructionOutcomeFailed;
 	}
-	MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override {
+	MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override {
 		return kMiniscriptInstructionOutcomeFailed;
 	}
 
@@ -880,7 +880,7 @@ DynamicValueWriteFloatHelper<TFloat> DynamicValueWriteFloatHelper<TFloat>::_inst
 
 template<class TInteger>
 struct DynamicValueWriteIntegerHelper : public IDynamicValueWriteInterface {
-	MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const override {
+	MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr ptrOrOffset) const override {
 		TInteger &dest = *static_cast<TInteger *>(objectRef);
 		switch (value.getType()) {
 		case DynamicValueTypes::kFloat:
@@ -893,10 +893,10 @@ struct DynamicValueWriteIntegerHelper : public IDynamicValueWriteInterface {
 			return kMiniscriptInstructionOutcomeFailed;
 		}
 	}
-	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override {
+	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const override {
 		return kMiniscriptInstructionOutcomeFailed;
 	}
-	MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override {
+	MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override {
 		return kMiniscriptInstructionOutcomeFailed;
 	}
 
@@ -914,9 +914,9 @@ template<class TInteger>
 DynamicValueWriteIntegerHelper<TInteger> DynamicValueWriteIntegerHelper<TInteger>::_instance;
 
 struct DynamicValueWriteStringHelper : public IDynamicValueWriteInterface {
-	MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const override;
-	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override;
-	MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
+	MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr ptrOrOffset) const override;
+	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const override;
+	MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
 
 	static void create(Common::String *strValue, DynamicValueWriteProxy &proxy);
 
@@ -926,13 +926,13 @@ private:
 
 template<class TClass, MiniscriptInstructionOutcome (TClass::*TWriteMethod)(MiniscriptThread *thread, const DynamicValue &dest)>
 struct DynamicValueWriteFuncHelper : public IDynamicValueWriteInterface {
-	MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const override {
+	MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr ptrOrOffset) const override {
 		return (static_cast<TClass *>(objectRef)->*TWriteMethod)(thread, dest);
 	}
-	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override {
+	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const override {
 		return kMiniscriptInstructionOutcomeFailed;
 	}
-	MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override {
+	MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override {
 		return kMiniscriptInstructionOutcomeFailed;
 	}
 
@@ -950,9 +950,9 @@ template<class TClass, MiniscriptInstructionOutcome (TClass::*TWriteMethod)(Mini
 DynamicValueWriteFuncHelper<TClass, TWriteMethod> DynamicValueWriteFuncHelper<TClass, TWriteMethod>::_instance;
 
 struct DynamicValueWriteObjectHelper : public IDynamicValueWriteInterface {
-	MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr_t ptrOrOffset) const override;
-	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override;
-	MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
+	MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr ptrOrOffset) const override;
+	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const override;
+	MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
 
 	static void create(RuntimeObject *obj, DynamicValueWriteProxy &proxy);
 
@@ -1738,7 +1738,7 @@ private:
 	int _masterVolume;
 };
 
-class Structural : public RuntimeObject, public IModifierContainer, public IMessageConsumer, public IDebuggable {
+class Structural : public RuntimeObject, public IModifierContainer, public IMessageConsumer, public Debuggable {
 public:
 	Structural();
 	virtual ~Structural();
@@ -1784,9 +1784,7 @@ public:
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	SupportStatus debugGetSupportStatus() const override;
 	const Common::String &debugGetName() const override;
-	Common::SharedPtr<DebugInspector> debugGetInspector() const override;
-
-	virtual DebugInspector *debugCreateInspector();
+	void debugInspect(IDebugInspectionReport *report) const override;
 #endif
 
 protected:
@@ -1816,10 +1814,6 @@ protected:
 	// "loop" appears to have been made available on everything in 1.2.  Obsidian depends on it
 	// being available for sound indexes to be properly set up.
 	bool _loop;
-
-#ifdef MTROPOLIS_DEBUG_ENABLE
-	Common::SharedPtr<DebugInspector> _debugInspector;
-#endif
 };
 
 struct ProjectPresentationSettings {
@@ -2214,7 +2208,7 @@ protected:
 	virtual bool loadInternal(Common::ReadStream *stream) = 0;
 };
 
-class Modifier : public RuntimeObject, public IMessageConsumer, public IDebuggable {
+class Modifier : public RuntimeObject, public IMessageConsumer, public Debuggable {
 public:
 	Modifier();
 	virtual ~Modifier();
@@ -2262,9 +2256,7 @@ public:
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	SupportStatus debugGetSupportStatus() const override;
 	const Common::String &debugGetName() const override;
-	Common::SharedPtr<DebugInspector> debugGetInspector() const override;
-
-	virtual DebugInspector *debugCreateInspector();
+	void debugInspect(IDebugInspectionReport *report) const override;
 #endif
 
 protected:
@@ -2279,11 +2271,6 @@ protected:
 	Common::String _name;
 	ModifierFlags _modifierFlags;
 	Common::WeakPtr<RuntimeObject> _parent;
-
-#ifdef MTROPOLIS_DEBUG_ENABLE
-	Common::SharedPtr<DebugInspector> _debugInspector;
-	Debugger *_debugger;
-#endif
 };
 
 class VariableModifier : public Modifier {
@@ -2299,9 +2286,9 @@ public:
 
 private:
 	struct WriteProxyInterface : public IDynamicValueWriteInterface {
-		MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr_t ptrOrOffset) const override;
-		MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib) const override;
-		MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr_t ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
+		MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr ptrOrOffset) const override;
+		MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const override;
+		MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
 
 		static WriteProxyInterface _instance;
 	};
diff --git a/engines/mtropolis/vthread.cpp b/engines/mtropolis/vthread.cpp
index edb7ee0a4c3..3623fc7b8e3 100644
--- a/engines/mtropolis/vthread.cpp
+++ b/engines/mtropolis/vthread.cpp
@@ -33,6 +33,9 @@ VThreadTaskData::~VThreadTaskData() {
 void VThreadTaskData::debugInit(const char *name) {
 	_debugName = name;
 }
+
+void VThreadTaskData::debugInspect(IDebugInspectionReport *report) const {
+}
 #endif
 
 VThread::VThread() : _faultID(nullptr), _stackUnalignedBase(nullptr), _stackAlignedBase(nullptr), _size(0), _alignment(1), _used(0) {
@@ -79,7 +82,7 @@ void VThread::reserveFrame(size_t size, size_t alignment, void *&outFramePtr, vo
 
 	bool needToReallocate = false;
 	if (alignment > _alignment || frameAlignment > _alignment) {
-		if ((reinterpret_cast<uintptr_t>(_stackAlignedBase) & dataAlignmentMask) != 0) {
+		if ((reinterpret_cast<uintptr>(_stackAlignedBase) & dataAlignmentMask) != 0) {
 			needToReallocate = true;
 		}
 	}
@@ -110,7 +113,7 @@ void VThread::reserveFrame(size_t size, size_t alignment, void *&outFramePtr, vo
 			maxAlignment = frameAlignment;
 
 		void *unalignedBase = malloc(offsetOfEndOfFrame + maxAlignment - 1);
-		size_t alignPadding = maxAlignment - (reinterpret_cast<uintptr_t>(unalignedBase) % maxAlignment);
+		size_t alignPadding = maxAlignment - (reinterpret_cast<uintptr>(unalignedBase) % maxAlignment);
 		if (alignPadding == maxAlignment)
 			alignPadding = 0;
 
diff --git a/engines/mtropolis/vthread.h b/engines/mtropolis/vthread.h
index 0638405a788..d1aa686518e 100644
--- a/engines/mtropolis/vthread.h
+++ b/engines/mtropolis/vthread.h
@@ -44,7 +44,7 @@ struct VThreadFaultIdentifierSingleton {
 template<typename T>
 VThreadFaultIdentifier VThreadFaultIdentifierSingleton<T>::_identifier;
 
-class VThreadTaskData : public IDebuggable {
+class VThreadTaskData : public Debuggable {
 public:
 	VThreadTaskData();
 	virtual ~VThreadTaskData();
@@ -61,9 +61,7 @@ protected:
 	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 	const char *debugGetTypeName() const override { return "Task"; }
 	const Common::String &debugGetName() const override { return _debugName; }
-	Common::SharedPtr<DebugInspector> debugGetInspector() const override { return _debugInspector; }
-
-	Common::SharedPtr<DebugInspector> _debugInspector;
+	void debugInspect(IDebugInspectionReport *report) const override;
 
 	Common::String _debugName;
 #endif
@@ -84,10 +82,7 @@ class VThreadMethodData : public VThreadTaskData {
 public:
 	VThreadMethodData(const VThreadFaultIdentifier *faultID, TClass *target, VThreadState (TClass::*method)(const TData &data));
 	VThreadMethodData(const VThreadMethodData &other);
-
-#if __cplusplus >= 201103L
 	VThreadMethodData(VThreadMethodData &&other);
-#endif
 
 	VThreadState destructAndRunTask() override;
 	void relocateTo(void *newPosition) override;
@@ -109,9 +104,7 @@ public:
 	explicit VThreadFunctionData(const VThreadFaultIdentifier *faultID, VThreadState (*func)(const TData &data));
 	VThreadFunctionData(const VThreadFunctionData &other);
 
-#if __cplusplus >= 201103L
 	VThreadFunctionData(VThreadFunctionData &&other);
-#endif
 
 	VThreadState destructAndRunTask() override;
 	void relocateTo(void *newPosition) override;
@@ -173,22 +166,14 @@ VThreadMethodData<TClass, TData>::VThreadMethodData(const VThreadMethodData& oth
 	: _faultID(other._faultID), _target(other._target), _method(other._method), _data(other._data) {
 }
 
-#if __cplusplus >= 201103L
-
 template<typename TClass, typename TData>
 VThreadMethodData<TClass, TData>::VThreadMethodData(VThreadMethodData &&other)
-	: _faultID(other.faultID), _target(other._target), _method(other._method), _data(static_cast<TData &&>(*static_cast<TData *>(other._data))) {
+	: _faultID(other._faultID), _target(other._target), _method(other._method), _data(static_cast<TData &&>(other._data)) {
 }
 
-#endif
-
 template<typename TClass, typename TData>
 VThreadState VThreadMethodData<TClass, TData>::destructAndRunTask() {
-#if __cplusplus >= 201103L
 	TData data(static_cast<TData &&>(_data));
-#else
-	TData data(_data);
-#endif
 
 	TClass *target = _target;
 	VThreadState (TClass::*method)(const TData &) = _method;
@@ -202,18 +187,7 @@ template<typename TClass, typename TData>
 void VThreadMethodData<TClass, TData>::relocateTo(void *newPosition) {
 	void *adjustedPtr = static_cast<VThreadMethodData<TClass, TData> *>(static_cast<VThreadTaskData *>(newPosition));
 
-#if __cplusplus >= 201103L
-	VThreadMethodData<TClass, TData> *relocated = new (adjustedPtr) VThreadMethodData<TClass, TData>(static_cast<VThreadMethodData<TClass, TData> &&>(*this));
-#else
-	VThreadMethodData<TClass, TData> *relocated = new (adjustedPtr) VThreadMethodData<TClass, TData>(*this);
-#endif
-
-#ifdef MTROPOLIS_DEBUG_ENABLE
-	if (relocated->_debugInspector)
-		relocated->_debugInspector->onDebuggableRelocated(this);
-#else
-	(void)relocated;
-#endif
+	new (adjustedPtr) VThreadMethodData<TClass, TData>(static_cast<VThreadMethodData<TClass, TData> &&>(*this));
 }
 
 template<typename TClass, typename TData>
@@ -236,22 +210,14 @@ VThreadFunctionData<TData>::VThreadFunctionData(const VThreadFunctionData &other
 	: _faultID(other._faultID), _func(other._func), _data(other._data) {
 }
 
-#if __cplusplus >= 201103L
-
 template<typename TData>
 VThreadFunctionData<TData>::VThreadFunctionData(VThreadFunctionData &&other)
 	: _faultID(other._faultID), _func(other._func), _data(static_cast<TData &&>(other._data)) {
 }
 
-#endif
-
 template<typename TData>
 VThreadState VThreadFunctionData<TData>::destructAndRunTask() {
-#if __cplusplus >= 201103L
 	TData data(static_cast<TData &&>(_data));
-#else
-	TData data(_data);
-#endif
 
 	VThreadState (*func)(const TData &) = _func;
 
@@ -264,18 +230,7 @@ template<typename TData>
 void VThreadFunctionData<TData>::relocateTo(void *newPosition) {
 	void *adjustedPtr = static_cast<VThreadFunctionData<TData> *>(static_cast<VThreadTaskData *>(newPosition));
 
-#if __cplusplus >= 201103L
-	VThreadFunctionData<TData> *relocated = new (adjustedPtr) VThreadFunctionData<TData>(static_cast<VThreadFunctionData<TData> &&>(*this));
-#else
-	VThreadFunctionData<TData> *relocated = new (adjustedPtr) VThreadFunctionData<TData>(*this);
-#endif
-
-#ifdef MTROPOLIS_DEBUG_ENABLE
-	if (relocated->_debugInspector)
-		relocated->_debugInspector->onDebuggableRelocated(this);
-#else
-	(void)relocated;
-#endif
+	new (adjustedPtr) VThreadFunctionData<TData>(static_cast<VThreadFunctionData<TData> &&>(*this));
 }
 
 template<typename TData>


Commit: 92cd3de76416c0edf4ba9b45fd10b3d2576a96b0
    https://github.com/scummvm/scummvm/commit/92cd3de76416c0edf4ba9b45fd10b3d2576a96b0
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Get rid of some more standard types and values

Changed paths:
    engines/mtropolis/data.cpp
    engines/mtropolis/runtime.cpp


diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index 359311e1e70..d7291f4e755 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -205,7 +205,7 @@ bool DataReader::readF64(double &value) {
 
 bool DataReader::read(void *dest, size_t size) {
 	while (size > 0) {
-		uint32 thisChunkSize = UINT32_MAX;
+		uint32 thisChunkSize = 0xffffffffu;
 		if (size < thisChunkSize) {
 			thisChunkSize = static_cast<uint32>(size);
 		}
@@ -260,18 +260,11 @@ bool DataReader::seek(int64 pos) {
 }
 
 bool DataReader::skip(size_t count) {
-	while (count > 0) {
-		uint64 thisChunkSize = INT64_MAX;
-		if (count < thisChunkSize) {
-			thisChunkSize = static_cast<uint64>(count);
-		}
-
+	if (count > 0) {
 		if (!_stream.seek(static_cast<int64>(count), SEEK_CUR)) {
 			checkErrorAndReset();
 			return false;
 		}
-
-		count -= static_cast<size_t>(thisChunkSize);
 	}
 	return true;
 }
@@ -393,10 +386,10 @@ double XPFloat::toDouble() const {
 		if (exponent > 2046) {
 			// Too big, set to largest finite magnitude
 			exponent = 2046;
-			workMantissa = (static_cast<uint64_t>(1) << 52) - 1u;
+			workMantissa = (static_cast<uint64>(1) << 52) - 1u;
 		} else if (exponent < 0) {
 			// Subnormal number
-			workMantissa |= (static_cast<uint64_t>(1) << 52);
+			workMantissa |= (static_cast<uint64>(1) << 52);
 			if (exponent < -52) {
 				workMantissa = 0;
 				exponent = 0;
@@ -407,7 +400,7 @@ double XPFloat::toDouble() const {
 		}
 	}
 
-	uint64_t recombined = (static_cast<uint64_t>(sign) << 63) | (static_cast<uint64_t>(exponent) << 52) | static_cast<uint64_t>(workMantissa);
+	uint64 recombined = (static_cast<uint64>(sign) << 63) | (static_cast<uint64>(exponent) << 52) | static_cast<uint64>(workMantissa);
 
 	double d;
 	memcpy(&d, &recombined, 8);
@@ -1764,7 +1757,7 @@ DataReadErrorCode TextAsset::load(DataReader &reader) {
 			return kDataReadErrorReadFailed;
 
 		if (reader.getProjectFormat() == kProjectFormatMacintosh) {
-			uint16_t numFormattingSpans;
+			uint16 numFormattingSpans;
 			if (!reader.readU16(numFormattingSpans))
 				return kDataReadErrorReadFailed;
 
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 4ebba180cac..0738841e3df 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -813,7 +813,7 @@ size_t DynamicList::getSize() const {
 bool DynamicList::dynamicValueToIndex(size_t &outIndex, const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kFloat) {
 		double rounded = floor(value.getFloat() + 0.5);
-		if (!isfinite(rounded) || rounded < 1.0 || rounded > UINT32_MAX)
+		if (!isfinite(rounded) || rounded < 1.0 || rounded > 0xffffffffu)
 			return false;
 
 		outIndex = static_cast<size_t>(rounded - 1.0);
@@ -4962,7 +4962,7 @@ void Project::loadFromDescription(const ProjectDescription& desc) {
 	openSegmentStream(0);
 
 	Common::SeekableReadStream *baseStream = _segments[0].weakStream;
-	uint16_t startValue = baseStream->readUint16LE();
+	uint16 startValue = baseStream->readUint16LE();
 
 	if (startValue == 1) {
 		// Windows format


Commit: d6b1074f523ba71a92873181fe65ec82272a4c65
    https://github.com/scummvm/scummvm/commit/d6b1074f523ba71a92873181fe65ec82272a4c65
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Debug inspector drawing

Changed paths:
    engines/mtropolis/debug.cpp
    engines/mtropolis/debug.h


diff --git a/engines/mtropolis/debug.cpp b/engines/mtropolis/debug.cpp
index d64b0289c40..26e97975028 100644
--- a/engines/mtropolis/debug.cpp
+++ b/engines/mtropolis/debug.cpp
@@ -983,13 +983,14 @@ void DebugInspectorWindow::update() {
 
 		_inspector = inspector;
 		inspectorChanged = true;
+		setDirty();
 	}
 
 	_declLabeledRow = 0;
 	_declUnlabeledRow = 0;
 
 	if (inspector == nullptr || inspector->getDebuggable() == nullptr) {
-		_unlabeledRow.resize(0);
+		_unlabeledRow.resize(1);
 		_unlabeledRow[0].str = "No object selected";
 
 		_labeledRow.clear();
@@ -998,6 +999,8 @@ void DebugInspectorWindow::update() {
 		inspector->getDebuggable()->debugInspect(this);
 
 		_unlabeledRow.resize(_declUnlabeledRow);
+
+		setDirty();
 	}
 }
 
@@ -1040,6 +1043,60 @@ void DebugInspectorWindow::declareLoose(const char *name, const Common::String &
 }
 
 void DebugInspectorWindow::toolRenderSurface(int32 subAreaWidth, int32 subAreaHeight) {
+	const Graphics::PixelFormat fmt = _debugger->getRuntime()->getRenderPixelFormat();
+
+	uint32 whiteColor = fmt.RGBToColor(255, 255, 255);
+	uint32 blackColor = fmt.RGBToColor(0, 0, 0);
+
+	const size_t numLabeledRows = _labeledRow.size();
+	const size_t numUnlabeledRows = _unlabeledRow.size();
+
+	int32 renderHeight = (_labeledRow.size() + _unlabeledRow.size()) * kRowHeight;
+	int32 renderWidth = subAreaWidth;
+
+	if (!_toolSurface || renderWidth != _toolSurface->w || renderHeight != _toolSurface->h) {
+		_toolSurface.reset();
+		_toolSurface.reset(new Graphics::ManagedSurface(renderWidth, renderHeight, fmt));
+	}
+
+	const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
+
+	if (_maxLabelWidth == 0) {
+		for (const InspectorLabeledRow &row : _labeledRow) {
+			int width = font->getStringWidth(row.label);
+			if (width > _maxLabelWidth)
+				_maxLabelWidth = width;
+		}
+	}
+
+	_toolSurface->fillRect(Common::Rect(0, 0, renderWidth, renderHeight), whiteColor);
+
+	for (size_t i = 0; i < numLabeledRows; i++) {
+		const InspectorLabeledRow &row = _labeledRow[i];
+
+		int32 startY = i * kRowHeight;
+
+		int32 labelX = 4;
+		int32 labelW = renderWidth - labelX;
+		if (labelW > 1)
+			font->drawString(_toolSurface.get(), row.label, labelX, startY + 2, labelW, blackColor);
+
+		int32 valueX = _maxLabelWidth + 8;
+		int32 valueW = renderWidth - valueX;
+		if (valueW > 1)
+			font->drawString(_toolSurface.get(), row.text, valueX, startY + 2, valueW, blackColor, Graphics::kTextAlignLeft, 0, true);
+	}
+
+	for (size_t i = 0; i < numUnlabeledRows; i++) {
+		const InspectorUnlabeledRow &row = _unlabeledRow[i];
+
+		int32 startY = (i + numLabeledRows) * kRowHeight;
+
+		int32 contentsX = 4;
+		int32 contentsW = renderWidth - contentsX;
+		if (contentsW > 1)
+			font->drawString(_toolSurface.get(), row.str, contentsX, startY + 2, contentsW, blackColor);
+	}
 }
 
 // Step through ("debugger") window
@@ -1086,7 +1143,6 @@ void DebugStepThroughWindow::update() {
 }
 
 void DebugStepThroughWindow::toolRenderSurface(int32 subAreaWidth, int32 subAreaHeight) {
-
 	const Graphics::PixelFormat fmt = _debugger->getRuntime()->getRenderPixelFormat();
 
 	uint32 whiteColor = fmt.RGBToColor(255, 255, 255);
@@ -1151,9 +1207,14 @@ void DebugToolsWindow::onMouseDown(int32 x, int32 y, int mouseButton) {
 Debuggable::Debuggable() {
 }
 
-Debuggable::Debuggable(const Debuggable &other) : _inspector(other._inspector) {
-	if (_inspector)
+Debuggable::Debuggable(const Debuggable &other) : _inspector(nullptr) {
+}
+
+Debuggable::Debuggable(Debuggable &&other) : _inspector(other._inspector) {
+	if (_inspector) {
 		_inspector->changePrimaryInstance(this);
+		other._inspector.reset();
+	}
 }
 
 Debuggable::~Debuggable() {
@@ -1167,7 +1228,7 @@ const Common::SharedPtr<DebugInspector> &Debuggable::debugGetInspector() {
 	return _inspector;
 }
 
-DebugInspector::DebugInspector(IDebuggable *debuggable) {
+DebugInspector::DebugInspector(IDebuggable *debuggable) : _instance(debuggable) {
 }
 
 DebugInspector::~DebugInspector() {
@@ -1439,7 +1500,7 @@ void Debugger::openToolWindow(DebuggerTool tool) {
 		windowRef.reset(new DebugSceneTreeWindow(this, WindowParameters(_runtime, 32, 32, 250, 120, _runtime->getRenderPixelFormat())));
 		break;
 	case kDebuggerToolInspector:
-		windowRef.reset(new DebugToolWindowBase(kDebuggerToolInspector, "Inspector", this, WindowParameters(_runtime, 32, 32, 100, 320, _runtime->getRenderPixelFormat())));
+		windowRef.reset(new DebugInspectorWindow(this, WindowParameters(_runtime, 32, 32, 100, 320, _runtime->getRenderPixelFormat())));
 		break;
 	case kDebuggerToolStepThrough:
 		windowRef.reset(new DebugStepThroughWindow(this, WindowParameters(_runtime, 32, 32, 100, 320, _runtime->getRenderPixelFormat())));
@@ -1459,9 +1520,6 @@ void Debugger::closeToolWindow(DebuggerTool tool) {
 
 void Debugger::inspectObject(IDebuggable *debuggable) {
 	_inspector = debuggable->debugGetInspector();
-
-	if (!_toolWindows[kDebuggerToolInspector])
-		openToolWindow(kDebuggerToolInspector);
 }
 
 void Debugger::tryInspectObject(RuntimeObject *object) {
diff --git a/engines/mtropolis/debug.h b/engines/mtropolis/debug.h
index f69a93bcb63..4ee2fbc0da9 100644
--- a/engines/mtropolis/debug.h
+++ b/engines/mtropolis/debug.h
@@ -82,13 +82,12 @@ class Debuggable : public IDebuggable {
 public:
 	Debuggable();
 	Debuggable(const Debuggable &other);
+	Debuggable(Debuggable &&other);
 	~Debuggable();
 
 private:
 	const Common::SharedPtr<DebugInspector> &debugGetInspector() override;
 
-	Debuggable &operator=(const Debuggable &other);
-
 	Common::SharedPtr<DebugInspector> _inspector;
 };
 


Commit: 3a9c8def131d773e30b523a718c5d7aeac6d78e9
    https://github.com/scummvm/scummvm/commit/3a9c8def131d773e30b523a718c5d7aeac6d78e9
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Debug inspector

Changed paths:
    engines/mtropolis/debug.cpp
    engines/mtropolis/debug.h
    engines/mtropolis/elements.cpp
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/debug.cpp b/engines/mtropolis/debug.cpp
index 26e97975028..61b90054d34 100644
--- a/engines/mtropolis/debug.cpp
+++ b/engines/mtropolis/debug.cpp
@@ -956,7 +956,7 @@ private:
 	bool declareStatic(const char *name) override;
 	void declareStaticContents(const Common::String &data) override;
 	void declareDynamic(const char *name, const Common::String &data) override;
-	void declareLoose(const char *name, const Common::String &data) override;
+	void declareLoose(const Common::String &data) override;
 
 	Common::SharedPtr<DebugInspector> _inspector;
 
@@ -1033,13 +1033,15 @@ void DebugInspectorWindow::declareDynamic(const char *name, const Common::String
 	_labeledRow[_declLabeledRow].text = data;
 }
 
-void DebugInspectorWindow::declareLoose(const char *name, const Common::String &data) {
+void DebugInspectorWindow::declareLoose(const Common::String &data) {
 	if (_declLabeledRow == _labeledRow.size()) {
-		InspectorLabeledRow row;
-		row.label = name;
-		_labeledRow.push_back(row);
-	}
-	_labeledRow[_declLabeledRow].text = data;
+		InspectorUnlabeledRow row;
+		row.str = data;
+		_unlabeledRow.push_back(row);
+	} else
+		_unlabeledRow[_declLabeledRow].str = data;
+
+	_declUnlabeledRow++;
 }
 
 void DebugInspectorWindow::toolRenderSurface(int32 subAreaWidth, int32 subAreaHeight) {
diff --git a/engines/mtropolis/debug.h b/engines/mtropolis/debug.h
index 4ee2fbc0da9..46a64a92f1e 100644
--- a/engines/mtropolis/debug.h
+++ b/engines/mtropolis/debug.h
@@ -65,7 +65,7 @@ struct IDebugInspectionReport {
 	virtual void declareDynamic(const char *name, const Common::String &data) = 0;
 
 	// Declares the contents of a loose row
-	virtual void declareLoose(const char *name, const Common::String &data) = 0;
+	virtual void declareLoose(const Common::String &data) = 0;
 };
 
 struct IDebuggable {
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index e4ac70e5628..2598142a1aa 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -151,12 +151,24 @@ void MovieElement::deactivate() {
 }
 
 void MovieElement::render(Window *window) {
+	int framesDecodedThisFrame = 0;
 	while (_videoDecoder->needsUpdate()) {
-		_displayFrame = _videoDecoder->decodeNextFrame();
-		if (_playEveryFrame)
-			break;
+		const Graphics::Surface *decodedFrame = _videoDecoder->decodeNextFrame();
+		framesDecodedThisFrame++;
+
+		// GNARLY HACK: QuickTimeDecoder doesn't return true for endOfVideo or false for needsUpdate until it
+		// tries decoding past the end, so we're assuming that the decoded frame memory stays valid until we
+		// actually have a new frame and continuing to use it.
+		if (decodedFrame) {
+			_displayFrame = decodedFrame;
+			if (_playEveryFrame)
+				break;
+		}
 	}
 
+	if (framesDecodedThisFrame > 1)
+		debug(1, "Perf warning: %i video frames decoded in one frame", framesDecodedThisFrame);
+
 	if (_displayFrame) {
 		Graphics::ManagedSurface *target = window->getSurface().get();
 		Common::Rect srcRect(0, 0, _displayFrame->w, _displayFrame->h);
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 0d939becabd..3fd733fc82c 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -1078,7 +1078,7 @@ MiniscriptInstructionOutcome BuiltinFunc::executeRectToPolar(MiniscriptThread *t
 	double angle = atan2(pt.x, pt.y);
 	double magnitude = sqrt(pt.x * pt.x + pt.y * pt.y);
 
-	returnValue->setVector(AngleMagVector::create(angle, magnitude));
+	returnValue->setVector(AngleMagVector::createRadians(angle, magnitude));
 
 	return kMiniscriptInstructionOutcomeContinue;
 }
@@ -1093,8 +1093,8 @@ MiniscriptInstructionOutcome BuiltinFunc::executePolarToRect(MiniscriptThread *t
 
 	const AngleMagVector &vec = inputDynamicValue.getVector();
 
-	double x = cos(vec.angleRadians) * vec.magnitude;
-	double y = sin(vec.angleRadians) * vec.magnitude;
+	double x = cos(vec.angleDegrees * (M_PI / 180.0)) * vec.magnitude;
+	double y = sin(vec.angleDegrees * (M_PI / 180.0)) * vec.magnitude;
 
 	returnValue->setPoint(Point16::create(static_cast<int16>(round(x)), static_cast<int16>(round(y))));
 
@@ -1368,7 +1368,7 @@ MiniscriptInstructionOutcome GetChild::readRValueAttrib(MiniscriptThread *thread
 
 	case DynamicValueTypes::kVector:
 		if (attrib == "angle")
-			valueSrcDest.setFloat(valueSrcDest.getVector().angleRadians * (180.0 / M_PI));
+			valueSrcDest.setFloat(valueSrcDest.getVector().angleDegrees);
 		else if (attrib == "magnitude")
 			valueSrcDest.setFloat(valueSrcDest.getVector().magnitude);
 		else {
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 78021dcec78..918c1def102 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -1253,6 +1253,14 @@ void BooleanVariableModifier::varGetValue(MiniscriptThread *thread, DynamicValue
 	dest.setBool(_value);
 }
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+void BooleanVariableModifier::debugInspect(IDebugInspectionReport *report) const {
+	VariableModifier::debugInspect(report);
+
+	report->declareDynamic("value", _value ? "true" : "false");
+}
+#endif
+
 Common::SharedPtr<Modifier> BooleanVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new BooleanVariableModifier(*this));
 }
@@ -1306,6 +1314,14 @@ void IntegerVariableModifier::varGetValue(MiniscriptThread *thread, DynamicValue
 	dest.setInt(_value);
 }
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+void IntegerVariableModifier::debugInspect(IDebugInspectionReport *report) const {
+	VariableModifier::debugInspect(report);
+
+	report->declareDynamic("value", Common::String::format("%i", _value));
+}
+#endif
+
 Common::SharedPtr<Modifier> IntegerVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new IntegerVariableModifier(*this));
 }
@@ -1358,6 +1374,14 @@ void IntegerRangeVariableModifier::varGetValue(MiniscriptThread *thread, Dynamic
 	dest.setIntRange(_range);
 }
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+void IntegerRangeVariableModifier::debugInspect(IDebugInspectionReport *report) const {
+	VariableModifier::debugInspect(report);
+
+	report->declareDynamic("value", _range.toString());
+}
+#endif
+
 Common::SharedPtr<Modifier> IntegerRangeVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new IntegerRangeVariableModifier(*this));
 }
@@ -1389,7 +1413,7 @@ bool VectorVariableModifier::load(ModifierLoaderContext &context, const Data::Ve
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
 
-	_vector.angleRadians = data.vector.angleRadians.toDouble();
+	_vector.angleDegrees = data.vector.angleRadians.toDouble() * (180 / M_PI);
 	_vector.magnitude = data.vector.magnitude.toDouble();
 
 	return true;
@@ -1417,7 +1441,7 @@ bool VectorVariableModifier::readAttribute(MiniscriptThread *thread, DynamicValu
 		result.setFloat(_vector.magnitude);
 		return true;
 	} else if (attrib == "angle") {
-		_vector.scriptGetAngleDegrees(result);
+		result.setFloat(_vector.angleDegrees);
 		return true;
 	}
 
@@ -1429,13 +1453,20 @@ MiniscriptInstructionOutcome VectorVariableModifier::writeRefAttribute(Miniscrip
 		DynamicValueWriteFloatHelper<double>::create(&_vector.magnitude, result);
 		return kMiniscriptInstructionOutcomeContinue;
 	} else if (attrib == "angle") {
-		DynamicValueWriteFuncHelper<AngleMagVector, &AngleMagVector::scriptSetAngleDegrees>::create(&_vector, result);
+		DynamicValueWriteFloatHelper<double>::create(&_vector.angleDegrees, result);
 		return kMiniscriptInstructionOutcomeContinue;
 	}
 
 	return writeRefAttribute(thread, result, attrib);
 }
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+void VectorVariableModifier::debugInspect(IDebugInspectionReport *report) const {
+	VariableModifier::debugInspect(report);
+
+	report->declareDynamic("value", _vector.toString());
+}
+#endif
 
 Common::SharedPtr<Modifier> VectorVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new VectorVariableModifier(*this));
@@ -1450,12 +1481,12 @@ void VectorVariableModifier::SaveLoad::commitLoad() const {
 }
 
 void VectorVariableModifier::SaveLoad::saveInternal(Common::WriteStream *stream) const {
-	stream->writeDoubleBE(_vector.angleRadians);
+	stream->writeDoubleBE(_vector.angleDegrees);
 	stream->writeDoubleBE(_vector.magnitude);
 }
 
 bool VectorVariableModifier::SaveLoad::loadInternal(Common::ReadStream *stream) {
-	_vector.angleRadians = stream->readDoubleBE();
+	_vector.angleDegrees = stream->readDoubleBE();
 	_vector.magnitude = stream->readDoubleBE();
 
 	if (stream->err())
@@ -1491,6 +1522,40 @@ void PointVariableModifier::varGetValue(MiniscriptThread *thread, DynamicValue &
 	dest.setPoint(_value);
 }
 
+bool PointVariableModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "x") {
+		result.setInt(_value.x);
+		return true;
+	}
+	if (attrib == "y") {
+		result.setInt(_value.y);
+		return true;
+	}
+
+	return VariableModifier::readAttribute(thread, result, attrib);
+}
+
+MiniscriptInstructionOutcome PointVariableModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
+	if (attrib == "x") {
+		DynamicValueWriteIntegerHelper<int16>::create(&_value.x, writeProxy);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+	if (attrib == "y") {
+		DynamicValueWriteIntegerHelper<int16>::create(&_value.y, writeProxy);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	return writeRefAttribute(thread, writeProxy, attrib);
+}
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
+void PointVariableModifier::debugInspect(IDebugInspectionReport *report) const {
+	VariableModifier::debugInspect(report);
+
+	report->declareDynamic("value", _value.toString());
+}
+#endif
+
 Common::SharedPtr<Modifier> PointVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new PointVariableModifier(*this));
 }
@@ -1546,6 +1611,14 @@ void FloatingPointVariableModifier::varGetValue(MiniscriptThread *thread, Dynami
 	dest.setFloat(_value);
 }
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+void FloatingPointVariableModifier::debugInspect(IDebugInspectionReport *report) const {
+	VariableModifier::debugInspect(report);
+
+	report->declareDynamic("value", Common::String::format("%g", _value));
+}
+#endif
+
 Common::SharedPtr<Modifier> FloatingPointVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new FloatingPointVariableModifier(*this));
 }
@@ -1597,6 +1670,14 @@ void StringVariableModifier::varGetValue(MiniscriptThread *thread, DynamicValue
 	dest.setString(_value);
 }
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+void StringVariableModifier::debugInspect(IDebugInspectionReport *report) const {
+	VariableModifier::debugInspect(report);
+
+	report->declareDynamic("value", _value);
+}
+#endif
+
 Common::SharedPtr<Modifier> StringVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new StringVariableModifier(*this));
 }
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 63d733cd7de..2d24fc49b04 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -673,6 +673,7 @@ public:
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Boolean Variable Modifier"; }
 	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
+	void debugInspect(IDebugInspectionReport *report) const override;
 #endif
 
 private:
@@ -706,6 +707,7 @@ public:
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Integer Variable Modifier"; }
 	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
+	void debugInspect(IDebugInspectionReport *report) const override;
 #endif
 
 private:
@@ -739,6 +741,7 @@ public:
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Integer Range Variable Modifier"; }
 	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
+	void debugInspect(IDebugInspectionReport *report) const override;
 #endif
 
 private:
@@ -775,6 +778,7 @@ public:
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Vector Variable Modifier"; }
 	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
+	void debugInspect(IDebugInspectionReport *report) const override;
 #endif
 
 private:
@@ -805,9 +809,13 @@ public:
 	bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override;
 	void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override;
 
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Point Variable Modifier"; }
 	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
+	void debugInspect(IDebugInspectionReport *report) const override;
 #endif
 
 private:
@@ -841,6 +849,7 @@ public:
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Floating Point Variable Modifier"; }
 	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
+	void debugInspect(IDebugInspectionReport *report) const override;
 #endif
 
 private:
@@ -874,6 +883,7 @@ public:
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "String Variable Modifier"; }
 	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
+	void debugInspect(IDebugInspectionReport *report) const override;
 #endif
 
 private:
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index caf1906784e..98927c738d0 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -441,6 +441,7 @@ Common::Error MTropolisEngine::run() {
 #endif
 
 	bool paused = false;
+	int frameCounter = 0;
 
 	while (!shouldQuit()) {
 		handleEvents();
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 1abe659cec9..7d72c6f2893 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -105,7 +105,7 @@ MidiFilePlayer::~MidiFilePlayer() {
 }
 
 MidiFilePlayerImpl::MidiFilePlayerImpl(MidiDriver_BASE *outputDriver, const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, uint32 baseTempo, uint8 volume)
-	: _file(file), _outputDriver(outputDriver), _parser(nullptr) {
+	: _file(file), _outputDriver(outputDriver), _parser(nullptr), _volume(255) {
 	Common::SharedPtr<MidiParser> parser(MidiParser::createParser_SMF());
 
 	if (file->contents.size() != 0 && parser->loadMusic(&file->contents[0], file->contents.size())) {
@@ -491,6 +491,15 @@ void ObjectReferenceVariableModifier::varGetValue(MiniscriptThread *thread, Dyna
 	dest.setObject(this->getSelfReference());
 }
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+void ObjectReferenceVariableModifier::debugInspect(IDebugInspectionReport *report) const {
+	VariableModifier::debugInspect(report);
+
+	report->declareDynamic("path", _objectPath);
+	report->declareDynamic("fullPath", _fullPath);
+}
+#endif
+
 Common::SharedPtr<Modifier> ObjectReferenceVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new ObjectReferenceVariableModifier(*this));
 }
@@ -985,6 +994,62 @@ MiniscriptInstructionOutcome ListVariableModifier::writeRefAttributeIndexed(Mini
 	return kMiniscriptInstructionOutcomeFailed;
 }
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+void ListVariableModifier::debugInspect(IDebugInspectionReport *report) const {
+	VariableModifier::debugInspect(report);
+
+	size_t listSize = _list->getSize();
+
+	for (size_t i = 0; i < listSize; i++) {
+		int cardinal = i + 1;
+		switch (_list->getType()) {
+		case DynamicValueTypes::kInteger:
+			report->declareLoose(Common::String::format("[%i] = %i", cardinal, _list->getInt()[i]));
+			break;
+		case DynamicValueTypes::kFloat:
+			report->declareLoose(Common::String::format("[%i] = %g", cardinal, _list->getFloat()[i]));
+			break;
+		case DynamicValueTypes::kPoint:
+			report->declareLoose(Common::String::format("[%i] = ", cardinal) + _list->getPoint()[i].toString());
+			break;
+		case DynamicValueTypes::kIntegerRange:
+			report->declareLoose(Common::String::format("[%i] = ", cardinal) + _list->getIntRange()[i].toString());
+			break;
+		case DynamicValueTypes::kBoolean:
+			report->declareLoose(Common::String::format("[%i] = %s", cardinal, _list->getBool()[i] ? "true" : "false"));
+			break;
+		case DynamicValueTypes::kVector:
+			report->declareLoose(Common::String::format("[%i] = ", cardinal) + _list->getVector()[i].toString());
+			break;
+		case DynamicValueTypes::kLabel:
+			report->declareLoose(Common::String::format("[%i] = Label?", cardinal));
+			break;
+		case DynamicValueTypes::kEvent:
+			report->declareLoose(Common::String::format("[%i] = Event?", cardinal));
+			break;
+		case DynamicValueTypes::kVariableReference:
+			report->declareLoose(Common::String::format("[%i] = VarRef?", cardinal));
+			break;
+		case DynamicValueTypes::kIncomingData:
+			report->declareLoose(Common::String::format("[%i] = IncomingData??", cardinal));
+			break;
+		case DynamicValueTypes::kString:
+			report->declareLoose(Common::String::format("[%i] = ", cardinal) + _list->getString()[i]);
+			break;
+		case DynamicValueTypes::kList:
+			report->declareLoose(Common::String::format("[%i] = List", cardinal));
+			break;
+		case DynamicValueTypes::kObject:
+			report->declareLoose(Common::String::format("[%i] = Object?", cardinal));
+			break;
+		default:
+			report->declareLoose(Common::String::format("[%i] = <BAD TYPE>", cardinal));
+			break;
+		}
+	}
+}
+#endif
+
 ListVariableModifier::ListVariableModifier(const ListVariableModifier &other) {
 	if (other._list)
 		_list = other._list->clone();
@@ -1032,7 +1097,7 @@ void ListVariableModifier::SaveLoad::saveInternal(Common::WriteStream *stream) c
 			} break;
 		case DynamicValueTypes::kVector: {
 				const AngleMagVector &vec = _list->getVector()[i];
-				stream->writeDoubleBE(vec.angleRadians);
+				stream->writeDoubleBE(vec.angleDegrees);
 				stream->writeDoubleBE(vec.magnitude);
 			} break;
 		case DynamicValueTypes::kBoolean:
@@ -1095,7 +1160,7 @@ bool ListVariableModifier::SaveLoad::loadInternal(Common::ReadStream *stream) {
 			} break;
 		case DynamicValueTypes::kVector: {
 				AngleMagVector vec;
-				vec.angleRadians = stream->readDoubleBE();
+				vec.angleDegrees = stream->readDoubleBE();
 				vec.magnitude = stream->readDoubleBE();
 				val.setVector(vec);
 			} break;
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index ae80117b609..d5a11dd0462 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -131,6 +131,8 @@ public:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Object Reference Variable Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
+	void debugInspect(IDebugInspectionReport *report) const override;
 #endif
 
 private:
@@ -249,6 +251,8 @@ public:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "List Variable Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
+	void debugInspect(IDebugInspectionReport *report) const override;
 #endif
 
 private:
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 0738841e3df..a5b5bc87901 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -290,6 +290,10 @@ MiniscriptInstructionOutcome Point16::refAttrib(MiniscriptThread *thread, Dynami
 	return kMiniscriptInstructionOutcomeFailed;
 }
 
+Common::String Point16::toString() const {
+	return Common::String::format("(%i,%i)", x, y);
+}
+
 bool Rect16::load(const Data::Rect &rect) {
 	top = rect.top;
 	left = rect.left;
@@ -322,6 +326,11 @@ bool IntRange::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy
 	return false;
 }
 
+Common::String IntRange::toString() const {
+	return Common::String::format("(%i thru %i)", min, max);
+}
+
+
 bool Label::load(const Data::Label &label) {
 	id = label.labelID;
 	superGroupID = label.superGroupID;
@@ -366,7 +375,7 @@ void DynamicListDefaultSetter::defaultSet(bool &value) {
 }
 
 void DynamicListDefaultSetter::defaultSet(AngleMagVector &value) {
-	value.angleRadians = 0.0;
+	value.angleDegrees = 0.0;
 	value.magnitude = 0.0;
 }
 
@@ -1834,34 +1843,10 @@ bool VarReference::resolveSingleModifier(Modifier *modifier, Common::WeakPtr<Run
 	return false;
 }
 
-MiniscriptInstructionOutcome AngleMagVector::scriptSetAngleDegrees(MiniscriptThread *thread, const DynamicValue &value) {
-	double degrees = 0.0;
-	switch (value.getType())
-	{
-	case DynamicValueTypes::kInteger:
-		degrees = static_cast<double>(value.getInt());
-		break;
-	case DynamicValueTypes::kFloat:
-		degrees = value.getFloat();
-		break;
-	default:
-		return kMiniscriptInstructionOutcomeFailed;
-	}
-
-	angleRadians = degrees * (M_PI / 180.0);
-
-	return kMiniscriptInstructionOutcomeContinue;
-	;
-}
-
-void AngleMagVector::scriptGetAngleDegrees(DynamicValue &value) const {
-	value.setFloat(angleRadians * (180.0 / M_PI));
-}
-
 
 MiniscriptInstructionOutcome AngleMagVector::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib) {
 	if (attrib == "angle") {
-		DynamicValueWriteFuncHelper<AngleMagVector, &AngleMagVector::scriptSetAngleDegrees>::create(this, proxy);
+		DynamicValueWriteFloatHelper<double>::create(&angleDegrees, proxy);
 		return kMiniscriptInstructionOutcomeContinue;
 	}
 	if (attrib == "magnitude") {
@@ -1872,6 +1857,10 @@ MiniscriptInstructionOutcome AngleMagVector::refAttrib(MiniscriptThread *thread,
 	return kMiniscriptInstructionOutcomeFailed;
 }
 
+Common::String AngleMagVector::toString() const {
+	return Common::String::format("(%g deg %g mag)", angleDegrees, magnitude);
+}
+
 void IPlugInModifierRegistrar::registerPlugInModifier(const char *name, const IPlugInModifierFactoryAndDataFactory *loaderFactory) {
 	return this->registerPlugInModifier(name, loaderFactory, loaderFactory);
 }
@@ -4133,7 +4122,7 @@ void Runtime::sendMessageOnVThread(const Common::SharedPtr<MessageDispatch> &dis
 			valueStr = Common::String::format("(%i thru %i)", payload.getIntRange().min, payload.getIntRange().max);
 			break;
 		case DynamicValueTypes::kVector:
-			valueStr = Common::String::format("(%g deg %g mag)", payload.getVector().angleRadians * (180.0 / M_PI), payload.getVector().magnitude);
+			valueStr = Common::String::format("(%g deg %g mag)", payload.getVector().angleDegrees, payload.getVector().magnitude);
 			break;
 		case DynamicValueTypes::kString:
 			valueStr = "'" + payload.getString() + "'";
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 3b9a4b9f7ee..38e895a39a0 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -287,6 +287,7 @@ struct Point16 {
 	}
 
 	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib);
+	Common::String toString() const;
 };
 
 struct Rect16 {
@@ -333,6 +334,7 @@ struct IntRange {
 	}
 
 	bool refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib);
+	Common::String toString() const;
 };
 
 struct Label {
@@ -415,28 +417,33 @@ struct ObjectReference {
 };
 
 struct AngleMagVector {
-	double angleRadians;
+	double angleDegrees; // These are stored as radians in the data but scripts treat them as degrees so it's just pointless constantly doing conversion...
 	double magnitude;
 
 	inline bool operator==(const AngleMagVector &other) const {
-		return angleRadians == other.angleRadians && magnitude == other.magnitude;
+		return angleDegrees == other.angleDegrees && magnitude == other.magnitude;
 	}
 
 	inline bool operator!=(const AngleMagVector &other) const {
 		return !((*this) == other);
 	}
 
-	inline static AngleMagVector create(double angleRadians, double magnitude) {
+	inline static AngleMagVector createRadians(double angleRadians, double magnitude) {
 		AngleMagVector result;
-		result.angleRadians = angleRadians;
+		result.angleDegrees = angleRadians * (180.0 / M_PI);
 		result.magnitude = magnitude;
 		return result;
 	}
 
-	MiniscriptInstructionOutcome scriptSetAngleDegrees(MiniscriptThread *thread, const DynamicValue &value);
-	void scriptGetAngleDegrees(DynamicValue &value) const;
+	inline static AngleMagVector createDegrees(double angleDegrees, double magnitude) {
+		AngleMagVector result;
+		result.angleDegrees = angleDegrees;
+		result.magnitude = magnitude;
+		return result;
+	}
 
 	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib);
+	Common::String toString() const;
 };
 
 struct ColorRGB8 {


Commit: f27703204bcb9136b94f51be9c5528126c200144
    https://github.com/scummvm/scummvm/commit/f27703204bcb9136b94f51be9c5528126c200144
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix incorrect text bitmap size check

Changed paths:
    engines/mtropolis/assets.cpp


diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index a1da773a159..4063f7a56cb 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -473,10 +473,13 @@ bool TextAsset::load(AssetLoaderContext &context, const Data::TextAsset &data) {
 			return false;
 
 		_bitmapData.reset(new Graphics::Surface());
-		uint16 pitch = (data.pitchBigEndian[0] << 8) + data.pitchBigEndian[1];
+
 		uint16 width = _bitmapRect.getWidth();
 		uint16 height = _bitmapRect.getHeight();
-		if (static_cast<uint32>(pitch * width) != data.bitmapSize) {
+
+		uint16 pitch = (data.pitchBigEndian[0] << 8) + data.pitchBigEndian[1];
+
+		if (static_cast<uint32>(pitch * height) != data.bitmapSize) {
 			// Pitch is normally aligned to 4 bytes, so if this fails, maybe compute it that way?
 			warning("Pre-rendered text bitmap pitch didn't compute to bitmap size correctly, maybe it's wrong?");
 			return false;


Commit: 32640bcd88130f85553f4092b2a43b33d139fcea
    https://github.com/scummvm/scummvm/commit/32640bcd88130f85553f4092b2a43b33d139fcea
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add sounds, refactor media playback to start after scene transition.

Changed paths:
    engines/mtropolis/assets.cpp
    engines/mtropolis/assets.h
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/notes/notes.txt
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index 4063f7a56cb..29f139d6ed9 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -24,6 +24,10 @@
 
 #include "graphics/surface.h"
 
+#include "audio/audiostream.h"
+
+#include "common/endian.h"
+
 namespace MTropolis {
 
 Asset::Asset() : _assetID(0) {
@@ -50,36 +54,93 @@ AssetType ColorTableAsset::getAssetType() const {
 	return kAssetTypeColorTable;
 }
 
+
+CachedAudio::CachedAudio() {
+}
+
+bool CachedAudio::loadFromStream(const AudioMetadata &metadata, Common::ReadStream *stream, size_t size) {
+	_data.resize(size);
+	if (size > 0) {
+		stream->read(&_data[0], size);
+		if (stream->err())
+			return false;
+
+		if (metadata.encoding == AudioMetadata::kEncodingUncompressed && metadata.bitsPerSample == 16) {
+			int16 *samples = reinterpret_cast<int16 *>(&_data[0]);
+			size_t numSamples = _data.size() / 2;
+
+			if (metadata.isBigEndian) {
+				for (size_t i = 0; i < numSamples; i++)
+					samples[i] = FROM_BE_16(samples[i]);
+			} else {
+				for (size_t i = 0; i < numSamples; i++)
+					samples[i] = FROM_LE_16(samples[i]);
+			}
+		}
+
+		return true;
+	}
+	return true;
+}
+
+const void *CachedAudio::getData() const {
+	if (_data.size() == 0)
+		return nullptr;
+	return &_data[0];
+}
+
+size_t CachedAudio::getSize() const {
+	return _data.size();
+}
+
+size_t CachedAudio::getNumSamples(const AudioMetadata &metadata) const {
+	switch (metadata.encoding) {
+	case AudioMetadata::kEncodingMace6:
+		return _data.size() * 6 / metadata.channels;
+	case AudioMetadata::kEncodingMace3:
+		return _data.size() * 3 / metadata.channels;
+	case AudioMetadata::kEncodingUncompressed:
+		return _data.size() / (metadata.channels * metadata.bitsPerSample / 8);
+	default:
+		return 0;
+	}
+}
+
 bool AudioAsset::load(AssetLoaderContext &context, const Data::AudioAsset &data) {
 	_assetID = data.assetID;
-	_sampleRate = data.sampleRate1;
-	_bitsPerSample = data.bitsPerSample;
+
+	_metadata.reset(new AudioMetadata());
+	_metadata->sampleRate = data.sampleRate1;
+	_metadata->bitsPerSample = data.bitsPerSample;
+
+	_streamIndex = context.streamIndex;
 
 	switch (data.encoding1) {
 	case 0:
-		_encoding = kEncodingUncompressed;
+		_metadata->encoding = AudioMetadata::kEncodingUncompressed;
 		break;
 	case 3:
-		_encoding = kEncodingMace3;
+		_metadata->encoding = AudioMetadata::kEncodingMace3;
 		break;
 	case 4:
-		_encoding = kEncodingMace6;
+		_metadata->encoding = AudioMetadata::kEncodingMace6;
 		break;
 	default:
 		return false;
 	}
 
-	_channels = data.channels;
+	_metadata->channels = data.channels;
 	// Hours Minutes Seconds Hundredths -> msec
 	// Maximum is 0x37a4f52e so this fits in 30 bits
-	_durationMSec = ((((data.codedDuration[0] * 60u) + data.codedDuration[1]) * 60u + data.codedDuration[2]) * 100u + data.codedDuration[3]) * 10u;
+	_metadata->durationMSec = ((((data.codedDuration[0] * 60u) + data.codedDuration[1]) * 60u + data.codedDuration[2]) * 100u + data.codedDuration[3]) * 10u;
 	_filePosition = data.filePosition;
 	_size = data.size;
-	_cuePoints.resize(data.cuePoints.size());
+	_metadata->cuePoints.resize(data.cuePoints.size());
+	_metadata->isBigEndian = data.isBigEndian;
 
-	for (size_t i = 0; i < _cuePoints.size(); i++) {
-		_cuePoints[i].cuePointID = data.cuePoints[i].cuePointID;
-		_cuePoints[i].position = data.cuePoints[i].position;
+	for (size_t i = 0; i < data.cuePoints.size(); i++) {
+		_metadata->cuePoints[i].cuePointID = data.cuePoints[i].cuePointID;
+		_metadata->cuePoints[i].position = data.cuePoints[i].position;
 	}
 
 	return true;
@@ -89,6 +150,41 @@ AssetType AudioAsset::getAssetType() const {
 	return kAssetTypeAudio;
 }
 
+size_t AudioAsset::getStreamIndex() const {
+	return _streamIndex;
+}
+
+const Common::SharedPtr<AudioMetadata> &AudioAsset::getMetadata() const {
+	return _metadata;
+}
+
+
+const Common::SharedPtr<CachedAudio> &AudioAsset::loadAndCacheAudio(Runtime *runtime) {
+	if (_audioCache)
+		return _audioCache;
+
+	size_t streamIndex = getStreamIndex();
+	int segmentIndex = runtime->getProject()->getSegmentForStreamIndex(streamIndex);
+	runtime->getProject()->openSegmentStream(segmentIndex);
+	Common::SeekableReadStream *stream = runtime->getProject()->getStreamForSegment(segmentIndex);
+
+	if (!stream || !stream->seek(_filePosition)) {
+		warning("Audio asset failed to load, couldn't seek to position");
+		return _audioCache;
+	}
+
+	Common::SharedPtr<CachedAudio> audio(new CachedAudio());
+	if (!audio->loadFromStream(*_metadata, stream, _size)) {
+		warning("Audio asset failed to load, couldn't read data");
+		return _audioCache;
+	}
+
+	_audioCache.reset();
+	_audioCache = audio;
+
+	return _audioCache;
+}
+
 bool MovieAsset::load(AssetLoaderContext &context, const Data::MovieAsset &data) {
 	_assetID = data.assetID;
 	_moovAtomPos = data.moovAtomPos;
diff --git a/engines/mtropolis/assets.h b/engines/mtropolis/assets.h
index 90b600bd59f..a4e9d787b5c 100644
--- a/engines/mtropolis/assets.h
+++ b/engines/mtropolis/assets.h
@@ -26,9 +26,14 @@
 #include "mtropolis/runtime.h"
 #include "mtropolis/render.h"
 
+namespace Audio {
+} // End of namespace Audio
+
 namespace MTropolis {
 
 struct AssetLoaderContext;
+struct AudioMetadata;
+class AudioPlayer;
 
 class ColorTableAsset : public Asset {
 public:
@@ -39,8 +44,22 @@ private:
 	ColorRGB8 _colors[256];
 };
 
-class AudioAsset : public Asset {
+class CachedAudio {
 public:
+	CachedAudio();
+
+	bool loadFromStream(const AudioMetadata &metadata, Common::ReadStream *stream, size_t size);
+
+	const void *getData() const;
+	size_t getSize() const;
+
+	size_t getNumSamples(const AudioMetadata &metadata) const;
+
+private:
+	Common::Array<uint8> _data;
+};
+
+struct AudioMetadata {
 	struct CuePoint {
 		uint32 position;
 		uint32 cuePointID;
@@ -52,19 +71,34 @@ public:
 		kEncodingMace6,
 	};
 
+	Encoding encoding;
+	uint32 durationMSec;
+	uint16 sampleRate;
+	uint8 channels;
+	uint8 bitsPerSample;
+	bool isBigEndian;
+
+	Common::Array<CuePoint> cuePoints;
+};
+
+class AudioAsset : public Asset {
+public:
 	bool load(AssetLoaderContext &context, const Data::AudioAsset &data);
 	AssetType getAssetType() const override;
 
+	size_t getStreamIndex() const;
+	const Common::SharedPtr<AudioMetadata> &getMetadata() const;
+
+	const Common::SharedPtr<CachedAudio> &loadAndCacheAudio(Runtime *runtime);
+
 private:
-	uint16 _sampleRate;
-	uint8 _bitsPerSample;
-	Encoding _encoding;
-	uint8 _channels;
-	uint32 _durationMSec;
 	uint32 _filePosition;
 	uint32 _size;
 
-	Common::Array<CuePoint> _cuePoints;
+	size_t _streamIndex;
+
+	Common::SharedPtr<CachedAudio> _audioCache;
+	Common::SharedPtr<AudioMetadata> _metadata;
 };
 
 class MovieAsset : public Asset {
diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index d7291f4e755..a6c22b54d3d 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -1567,9 +1567,11 @@ DataReadErrorCode AudioAsset::load(DataReader &reader) {
 
 	haveMacPart = false;
 	haveWinPart = false;
+	isBigEndian = false;
 
 	if (reader.getProjectFormat() == Data::ProjectFormat::kProjectFormatMacintosh) {
 		haveMacPart = true;
+		isBigEndian = true;
 
 		if (!reader.readBytes(platform.mac.unknown4) || !reader.readU16(sampleRate1) || !reader.readBytes(platform.mac.unknown5)
 			|| !reader.readU8(bitsPerSample) || !reader.readU8(encoding1) || !reader.readU8(channels)
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index f0916d22c9e..afcc49275dd 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -1498,6 +1498,7 @@ struct AudioAsset : public DataObject {
 
 	bool haveMacPart;
 	bool haveWinPart;
+	bool isBigEndian;
 	PlatformPart platform;
 
 protected:
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 2598142a1aa..de20d8b2939 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -32,6 +32,130 @@
 
 namespace MTropolis {
 
+
+// Audio player, this does not support requeueing.  If the sound exhausts, then you must create a
+// new audio player.  In particular, since time is being tracked separately, if the loop status
+// changes when the timer thinks the sound should still be playing, but the sound has actually
+// exhausted, then the sound needs to be requeued.
+class AudioPlayer : public Audio::AudioStream {
+public:
+	AudioPlayer(Audio::Mixer *mixer, byte volume, int8 balance, const Common::SharedPtr<AudioMetadata> &metadata, const Common::SharedPtr<CachedAudio> &audio, bool isLooping, size_t currentPos, size_t startPos, size_t endPos);
+	~AudioPlayer();
+	
+	int readBuffer(int16 *buffer, const int numSamples) override;
+	bool isStereo() const override;
+	int getRate() const override;
+	bool endOfData() const override;
+
+	void sendToMixer(Audio::Mixer *mixer, byte volume, int8 balance);
+	void stop();
+
+private:
+	Common::Mutex _mutex;
+
+	Common::SharedPtr<AudioMetadata> _metadata;
+	Common::SharedPtr<CachedAudio> _audio;
+	Audio::SoundHandle _handle;
+	bool _isLooping;
+	bool _exhausted;
+	size_t _currentPos;
+	size_t _startPos;
+	size_t _endPos;
+	Audio::Mixer *_mixer;
+};
+
+AudioPlayer::AudioPlayer(Audio::Mixer *mixer, byte volume, int8 balance, const Common::SharedPtr<AudioMetadata> &metadata, const Common::SharedPtr<CachedAudio> &audio, bool isLooping, size_t currentPos, size_t startPos, size_t endPos)
+	: _metadata(metadata), _audio(audio), _isLooping(isLooping), _currentPos(currentPos), _startPos(startPos), _endPos(endPos), _exhausted(false), _mixer(nullptr) {
+	if (_startPos >= _endPos) {
+		// ???
+		_exhausted = true;
+		_isLooping = false;
+	}
+	if (_currentPos < _startPos)
+		_currentPos = _startPos;
+
+	if (!_exhausted) {
+		_mixer = mixer;
+		mixer->playStream(Audio::Mixer::kPlainSoundType, &_handle, this, -1, volume, balance, DisposeAfterUse::NO);
+	}
+}
+
+AudioPlayer::~AudioPlayer() {
+	stop();
+}
+
+int AudioPlayer::readBuffer(int16 *buffer, const int numSamplesTimesChannelCount) {
+	Common::StackLock lock(_mutex);
+
+	int samplesRead = 0;
+	if (_exhausted)
+		return 0;
+
+	uint8 numChannels = _metadata->channels;
+
+	size_t numSamples = numSamplesTimesChannelCount / numChannels;
+
+	while (numSamples > 0) {
+		size_t samplesAvailable = _endPos - _currentPos;
+		if (samplesAvailable == 0) {
+			if (_isLooping) {
+				_currentPos = _startPos;
+				continue;
+			} else {
+				_exhausted = true;
+				break;
+			}
+		}
+
+		size_t numSamplesThisIteration = numSamples;
+		if (numSamplesThisIteration > samplesAvailable)
+			numSamplesThisIteration = samplesAvailable;
+
+		size_t numSampleValues = numSamplesThisIteration * numChannels;
+		// TODO: Support more formats
+		if (_metadata->bitsPerSample == 8 && _metadata->encoding == AudioMetadata::kEncodingUncompressed) {
+			const uint8 *inSamples = static_cast<const uint8 *>(_audio->getData()) + _currentPos * numChannels;
+			for (int i = 0; i < numSampleValues; i++)
+				buffer[i] = (inSamples[i] - 0x80) * 256;
+		} else if (_metadata->bitsPerSample == 16 && _metadata->encoding == AudioMetadata::kEncodingUncompressed) {
+			const int16 *inSamples = static_cast<const int16 *>(_audio->getData()) + _currentPos * numChannels;
+			memcpy(buffer, inSamples, sizeof(int16) * numSampleValues);
+		}
+
+		buffer += numSampleValues;
+		numSamples -= numSamplesThisIteration;
+
+		samplesRead += numSamplesThisIteration * numChannels;
+		_currentPos += numSamplesThisIteration;
+	}
+
+	return samplesRead;
+}
+
+bool AudioPlayer::isStereo() const {
+	return _metadata->channels == 2;
+}
+
+int AudioPlayer::getRate() const {
+	return _metadata->sampleRate;
+}
+
+bool AudioPlayer::endOfData() const {
+	return _exhausted;
+}
+
+void AudioPlayer::sendToMixer(Audio::Mixer *mixer, byte volume, int8 balance) {
+	mixer->playStream(Audio::Mixer::kPlainSoundType, &_handle, this, -1, volume, balance, DisposeAfterUse::NO);
+}
+
+void AudioPlayer::stop() {
+	if (_mixer)
+		_mixer->stopHandle(_handle);
+
+	_exhausted = true;
+	_mixer = nullptr;
+}
+
 GraphicElement::GraphicElement() : _cacheBitmap(false) {
 }
 
@@ -53,14 +177,15 @@ void GraphicElement::render(Window *window) {
 
 MovieElement::MovieElement()
 	: _cacheBitmap(false), _reversed(false), _haveFiredAtFirstCel(false), _haveFiredAtLastCel(false)
-	, _alternate(false), _playEveryFrame(false), _assetID(0), _runtime(nullptr), _displayFrame(nullptr) {
+	, _alternate(false), _playEveryFrame(false), _assetID(0), _runtime(nullptr), _displayFrame(nullptr)
+	, _shouldPlayIfNotPaused(true), _needsReset(true), _currentPlayState(kMediaStateStopped) {
 }
 
 MovieElement::~MovieElement() {
 	if (_unloadSignaller)
 		_unloadSignaller->removeReceiver(this);
-	if (_postRenderSignaller)
-		_postRenderSignaller->removeReceiver(this);
+	if (_playMediaSignaller)
+		_playMediaSignaller->removeReceiver(this);
 }
 
 bool MovieElement::load(ElementLoaderContext &context, const Data::MovieElement &data) {
@@ -87,12 +212,13 @@ VThreadState MovieElement::consumeCommand(Runtime *runtime, const Common::Shared
 		ChangeFlagTaskData *becomeVisibleTaskData = runtime->getVThread().pushTask("MovieElement::changeVisibilityTask", static_cast<VisualElement *>(this), &MovieElement::changeVisibilityTask);
 		becomeVisibleTaskData->desiredFlag = true;
 		becomeVisibleTaskData->runtime = runtime;
+
+		return kVThreadReturn;
 	}
 
-	return kVThreadReturn;
+	return Structural::consumeCommand(runtime, msg);
 }
 
-
 void MovieElement::activate() {
 	Project *project = _runtime->getProject();
 	Common::SharedPtr<Asset> asset = project->getAssetByID(_assetID).lock();
@@ -129,7 +255,7 @@ void MovieElement::activate() {
 		_videoDecoder.reset();
 
 	_unloadSignaller = project->notifyOnSegmentUnload(segmentIndex, this);
-	_postRenderSignaller = project->notifyOnPostRender(this);
+	_playMediaSignaller = project->notifyOnPlayMedia(this);
 
 	if (!_paused && _visible) {
 		StartPlayingTaskData *startPlayingTaskData = _runtime->getVThread().pushTask("MovieElement::startPlayingTask", this, &MovieElement::startPlayingTask);
@@ -142,9 +268,9 @@ void MovieElement::deactivate() {
 		_unloadSignaller->removeReceiver(this);
 		_unloadSignaller.reset();
 	}
-	if (_postRenderSignaller) {
-		_postRenderSignaller->removeReceiver(this);
-		_postRenderSignaller.reset();
+	if (_playMediaSignaller) {
+		_playMediaSignaller->removeReceiver(this);
+		_playMediaSignaller.reset();
 	}
 
 	_videoDecoder.reset();
@@ -152,14 +278,30 @@ void MovieElement::deactivate() {
 
 void MovieElement::render(Window *window) {
 	int framesDecodedThisFrame = 0;
+
+	if (_needsReset) {
+		// TODO: Seek elsewhere
+		_videoDecoder->seekToFrame(0);
+		const Graphics::Surface *decodedFrame = _videoDecoder->decodeNextFrame();
+		if (decodedFrame) {
+			_displayFrame = decodedFrame;
+			framesDecodedThisFrame++;
+		}
+
+		_needsReset = false;
+	}
+
 	while (_videoDecoder->needsUpdate()) {
+		if (_playEveryFrame && framesDecodedThisFrame > 0)
+			break;
+
 		const Graphics::Surface *decodedFrame = _videoDecoder->decodeNextFrame();
-		framesDecodedThisFrame++;
 
 		// GNARLY HACK: QuickTimeDecoder doesn't return true for endOfVideo or false for needsUpdate until it
 		// tries decoding past the end, so we're assuming that the decoded frame memory stays valid until we
 		// actually have a new frame and continuing to use it.
 		if (decodedFrame) {
+			framesDecodedThisFrame++;
 			_displayFrame = decodedFrame;
 			if (_playEveryFrame)
 				break;
@@ -177,9 +319,33 @@ void MovieElement::render(Window *window) {
 	}
 }
 
-void MovieElement::onPostRender(Runtime *runtime, Project *project) {
+void MovieElement::playMedia(Runtime *runtime, Project *project) {
 	if (_videoDecoder) {
-		if (_videoDecoder->isPlaying() && _videoDecoder->endOfVideo()) {
+		if (_shouldPlayIfNotPaused) {
+			if (_paused) {
+				// Goal state is paused
+				if (_videoDecoder->isPlaying()) {
+					_videoDecoder->pauseVideo(true);
+					_currentPlayState = kMediaStatePaused;
+				}
+			} else {
+				// Goal state is playing
+				if (_videoDecoder->isPaused())
+					_videoDecoder->pauseVideo(false);
+				if (!_videoDecoder->isPlaying())
+					_videoDecoder->start();
+
+				_currentPlayState = kMediaStatePlaying;
+			}
+		} else {
+			// Goal state is stopped
+			if (_videoDecoder->isPlaying())
+				_videoDecoder->stop();
+
+			_currentPlayState = kMediaStateStopped;
+		}
+
+		if (_currentPlayState == kMediaStatePlaying && _videoDecoder->endOfVideo()) {
 			if (_alternate) {
 				_reversed = !_reversed;
 				if (!_videoDecoder->setReverse(_reversed)) {
@@ -188,6 +354,7 @@ void MovieElement::onPostRender(Runtime *runtime, Project *project) {
 				}
 			} else {
 				_videoDecoder->stop();
+				_currentPlayState = kMediaStateStopped;
 			}
 
 			Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(_reversed ? EventIDs::kAtFirstCel : EventIDs::kAtLastCel, 0), DynamicValue(), getSelfReference()));
@@ -202,16 +369,13 @@ void MovieElement::onSegmentUnloaded(int segmentIndex) {
 }
 
 VThreadState MovieElement::startPlayingTask(const StartPlayingTaskData &taskData) {
-	if (_videoDecoder && !_videoDecoder->isPlaying()) {
-		EventIDs::EventID eventToSend = EventIDs::kPlay;
-		if (_paused) {
-			_paused = false;
-			eventToSend = EventIDs::kUnpause;
-		}
-
-		_videoDecoder->start();
+	if (_videoDecoder) {
+		// TODO: Frame ranges
+		_needsReset = true;
+		_shouldPlayIfNotPaused = true;
+		_paused = false;
 
-		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(eventToSend, 0), DynamicValue(), getSelfReference()));
+		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kPlay, 0), DynamicValue(), getSelfReference()));
 		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
 		taskData.runtime->sendMessageOnVThread(dispatch);
 	}
@@ -388,10 +552,12 @@ void TextLabelElement::deactivate() {
 void TextLabelElement::render(Window *window) {
 }
 
-SoundElement::SoundElement() {
+SoundElement::SoundElement() : _finishTime(0), _shouldPlayIfNotPaused(true), _needsReset(true) {
 }
 
 SoundElement::~SoundElement() {
+	if (_playMediaSignaller)
+		_playMediaSignaller->removeReceiver(this);
 }
 
 bool SoundElement::load(ElementLoaderContext &context, const Data::SoundElement &data) {
@@ -436,10 +602,100 @@ MiniscriptInstructionOutcome SoundElement::writeRefAttribute(MiniscriptThread *t
 	return NonVisualElement::writeRefAttribute(thread, writeProxy, attrib);
 }
 
+VThreadState SoundElement::consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	if (Event::create(EventIDs::kPlay, 0).respondsTo(msg->getEvent())) {
+		StartPlayingTaskData *startPlayingTaskData = runtime->getVThread().pushTask("SoundElement::startPlayingTask", this, &SoundElement::startPlayingTask);
+		startPlayingTaskData->runtime = runtime;
+
+		return kVThreadReturn;
+	}
+
+	return Structural::consumeCommand(runtime, msg);
+}
+
 void SoundElement::activate() {
+	Project *project = _runtime->getProject();
+	Common::SharedPtr<Asset> asset = project->getAssetByID(_assetID).lock();
+
+	if (!asset) {
+		warning("Sound element references asset %i but the asset isn't loaded!", _assetID);
+		return;
+	}
+
+	if (asset->getAssetType() != kAssetTypeAudio) {
+		warning("Sound element assigned an asset that isn't audio");
+		return;
+	}
+
+	_cachedAudio = static_cast<AudioAsset *>(asset.get())->loadAndCacheAudio(_runtime);
+	_metadata = static_cast<AudioAsset *>(asset.get())->getMetadata();
+
+	_playMediaSignaller = project->notifyOnPlayMedia(this);
+
+	if (!_paused) {
+		StartPlayingTaskData *startPlayingTaskData = _runtime->getVThread().pushTask("SoundElement::startPlayingTask", this, &SoundElement::startPlayingTask);
+		startPlayingTaskData->runtime = _runtime;
+	}
 }
 
+
 void SoundElement::deactivate() {
+	if (_playMediaSignaller) {
+		_playMediaSignaller->removeReceiver(this);
+		_playMediaSignaller.reset();
+	}
+
+	_metadata.reset();
+	_cachedAudio.reset();
+	_player.reset();
+}
+
+void SoundElement::playMedia(Runtime *runtime, Project *project) {
+	if (_shouldPlayIfNotPaused) {
+		if (_paused) {
+			// Goal state is paused
+			// TODO: Track pause time
+			_player.reset();
+		} else {
+			// Goal state is playing
+			if (_needsReset) {
+				// TODO: Reset to start time
+				_player.reset();
+				_needsReset = false;
+			}
+
+			if (!_player) {
+				_finishTime = _runtime->getPlayTime() + _metadata->durationMSec;
+
+				_player.reset();
+
+				int normalizedVolume = (_leftVolume + _rightVolume) * 255 / 2;
+				int normalizedBalance = _balance * 127 / 100;
+
+				// TODO: Support ranges
+				size_t numSamples = _cachedAudio->getNumSamples(*_metadata);
+				_player.reset(new AudioPlayer(_runtime->getAudioMixer(), normalizedVolume, normalizedBalance, _metadata, _cachedAudio, _loop, 0, 0, numSamples));
+			}
+
+			// TODO: Check cue points and queue them here
+			
+			if (!_loop && _runtime->getPlayTime() >= _finishTime) {
+				// Don't throw out the handle - It can still be playing but we just treat it like it's not.
+				// If it has anything left, then we let it finish and avoid clipping the sound, but we need
+				// to know that the handle is still here so we can actually stop it if the element is
+				// destroyed, since the stream is tied to the CachedAudio.
+
+				Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kStop, 0), DynamicValue(), getSelfReference()));
+				Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+				runtime->queueMessage(dispatch);
+
+				_shouldPlayIfNotPaused = false;
+			}
+		}
+	} else {
+		// Goal state is stopped
+		_player.reset();
+	}
 }
 
 MiniscriptInstructionOutcome SoundElement::scriptSetLoop(MiniscriptThread *thread, const DynamicValue &value) {
@@ -495,4 +751,16 @@ void SoundElement::setBalance(int16 balance) {
 	setVolume((_leftVolume + _rightVolume) / 2);
 }
 
+VThreadState SoundElement::startPlayingTask(const StartPlayingTaskData &taskData) {
+	_paused = false;
+	_shouldPlayIfNotPaused = true;
+	_needsReset = true;
+
+	Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kPlay, 0), DynamicValue(), getSelfReference()));
+	Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+	taskData.runtime->sendMessageOnVThread(dispatch);
+
+	return kVThreadReturn;
+}
+
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index a4c320db581..3e5a8b06583 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -26,6 +26,8 @@
 #include "mtropolis/runtime.h"
 #include "mtropolis/render.h"
 
+#include "audio/mixer.h"
+
 namespace Video {
 
 class VideoDecoder;
@@ -34,9 +36,18 @@ class VideoDecoder;
 
 namespace MTropolis {
 
+class AudioPlayer;
+class CachedAudio;
 class CachedImage;
+struct AudioMetadata;
 struct ElementLoaderContext;
 
+enum MediaState {
+	kMediaStatePlaying,
+	kMediaStateStopped,
+	kMediaStatePaused,
+};
+
 class GraphicElement : public VisualElement {
 public:
 	GraphicElement();
@@ -55,7 +66,7 @@ private:
 	bool _cacheBitmap;
 };
 
-class MovieElement : public VisualElement, public ISegmentUnloadSignalReceiver, public IPostRenderSignalReceiver {
+class MovieElement : public VisualElement, public ISegmentUnloadSignalReceiver, public IPlayMediaSignalReceiver {
 public:
 	MovieElement();
 	~MovieElement();
@@ -68,7 +79,7 @@ public:
 	void deactivate() override;
 
 	void render(Window *window) override;
-	void onPostRender(Runtime *runtime, Project *project) override;
+	void playMedia(Runtime *runtime, Project *project) override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Movie Element"; }
@@ -90,13 +101,16 @@ private:
 	bool _reversed;
 	bool _haveFiredAtLastCel;
 	bool _haveFiredAtFirstCel;
+	bool _shouldPlayIfNotPaused;
+	bool _needsReset;	// If true, then the video position was reset by a seek or stop and decoding must be restarted even if the target state is the same as the play state.
+	MediaState _currentPlayState;
 	uint32 _assetID;
 
 	Common::SharedPtr<Video::VideoDecoder> _videoDecoder;
 	const Graphics::Surface *_displayFrame;
 
 	Common::SharedPtr<SegmentUnloadSignaller> _unloadSignaller;
-	Common::SharedPtr<PostRenderSignaller> _postRenderSignaller;
+	Common::SharedPtr<PlayMediaSignaller> _playMediaSignaller;
 
 	Runtime *_runtime;
 };
@@ -198,7 +212,7 @@ private:
 	Runtime *_runtime;
 };
 
-class SoundElement : public NonVisualElement {
+class SoundElement : public NonVisualElement, public IPlayMediaSignalReceiver {
 public:
 	SoundElement();
 	~SoundElement();
@@ -208,9 +222,13 @@ public:
 	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
 	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
 
+	VThreadState consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg);
+
 	void activate() override;
 	void deactivate() override;
 
+	void playMedia(Runtime *runtime, Project *project) override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Sound Element"; }
 	SupportStatus debugGetSupportStatus() const override { return kSupportStatusPartial; }
@@ -221,6 +239,12 @@ private:
 	MiniscriptInstructionOutcome scriptSetVolume(MiniscriptThread *thread, const DynamicValue &value);
 	MiniscriptInstructionOutcome scriptSetBalance(MiniscriptThread *thread, const DynamicValue &value);
 
+	struct StartPlayingTaskData {
+		Runtime *runtime;
+	};
+
+	VThreadState startPlayingTask(const StartPlayingTaskData &taskData);
+
 	void setLoop(bool loop);
 	void setVolume(uint16 volume);
 	void setBalance(int16 balance);
@@ -230,6 +254,15 @@ private:
 	int16 _balance;
 	uint32 _assetID;
 
+	Common::SharedPtr<CachedAudio> _cachedAudio;
+	Common::SharedPtr<AudioMetadata> _metadata;
+	Common::SharedPtr<AudioPlayer> _player;
+	uint64 _finishTime;
+	bool _shouldPlayIfNotPaused;
+	bool _needsReset;
+
+	Common::SharedPtr<PlayMediaSignaller> _playMediaSignaller;
+
 	Runtime *_runtime;
 };
 
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index 98927c738d0..2ab4a8c77c1 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -270,7 +270,7 @@ Common::Error MTropolisEngine::run() {
 	int preferredHeight = 768;
 	ColorDepthMode preferredColorDepthMode = kColorDepthMode8Bit;
 
-	_runtime.reset(new Runtime(_system, this, this));
+	_runtime.reset(new Runtime(_system, _mixer, this, this));
 
 	if (_gameDescription->gameID == GID_OBSIDIAN && _gameDescription->desc.platform == Common::kPlatformWindows) {
 		preferredWidth = 640;
diff --git a/engines/mtropolis/notes/notes.txt b/engines/mtropolis/notes/notes.txt
index 332d5e5d293..394e28d4895 100644
--- a/engines/mtropolis/notes/notes.txt
+++ b/engines/mtropolis/notes/notes.txt
@@ -71,6 +71,16 @@ This has a bunch of confirmed broken cases:
 - Probably a bunch of other cases.
 
 
+Media play times:
+
+Sounds, QuickTime movies, and mToons can be commanded to play at any time during
+initial scene loads, but their play times start when the scene transition completes.
+This applies even when there is no scene transition because actions that occur
+prior to the drawing of the first frame may change the media's play state.
+Obsidian, for instance, starts with a bunch of sounds in a non-paused state and
+sets them all to paused via a script, so they must not ever play.
+
+
 Object reference liveness:
 
 A lot of things internally go off of an assumption that structural objects and
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index a5b5bc87901..e692af2199d 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -3158,8 +3158,8 @@ const Common::KeyState &KeyboardInputEvent::getKeyState() const {
 Runtime::SceneStackEntry::SceneStackEntry() {
 }
 
-Runtime::Runtime(OSystem *system, ISaveUIProvider *saveProvider, ILoadUIProvider *loadProvider)
-	: _system(system), _saveProvider(saveProvider), _loadProvider(loadProvider),
+Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, ISaveUIProvider *saveProvider, ILoadUIProvider *loadProvider)
+	: _system(system), _mixer(mixer), _saveProvider(saveProvider), _loadProvider(loadProvider),
 	_nextRuntimeGUID(1), _realDisplayMode(kColorDepthModeInvalid), _fakeDisplayMode(kColorDepthModeInvalid),
 	_displayWidth(1024), _displayHeight(768), _realTimeBase(0), _playTimeBase(0), _sceneTransitionState(kSceneTransitionStateNotTransitioning),
 	_lastFrameCursor(nullptr), _defaultCursor(new DefaultCursorGraphic()), _platform(kProjectPlatformUnknown),
@@ -4554,6 +4554,10 @@ ILoadUIProvider *Runtime::getLoadProvider() const {
 	return _loadProvider;
 }
 
+Audio::Mixer *Runtime::getAudioMixer() const {
+	return _mixer;
+}
+
 void Runtime::ensureMainWindowExists() {
 	// Maybe there's a better spot for this
 	if (_mainWindow.expired() && _project) {
@@ -4826,24 +4830,24 @@ const IPlugInModifierFactory *ProjectPlugInRegistry::findPlugInModifierFactory(c
 	return it->_value;
 }
 
-PostRenderSignaller::PostRenderSignaller() {
+PlayMediaSignaller::PlayMediaSignaller() {
 }
 
-PostRenderSignaller::~PostRenderSignaller() {
+PlayMediaSignaller::~PlayMediaSignaller() {
 }
 
-void PostRenderSignaller::onPostRender(Runtime *runtime, Project *project) {
+void PlayMediaSignaller::playMedia(Runtime *runtime, Project *project) {
 	const size_t numReceivers = _receivers.size();
 	for (size_t i = 0; i < numReceivers; i++) {
-		_receivers[i]->onPostRender(runtime, project);
+		_receivers[i]->playMedia(runtime, project);
 	}
 }
 
-void PostRenderSignaller::addReceiver(IPostRenderSignalReceiver *receiver) {
+void PlayMediaSignaller::addReceiver(IPlayMediaSignalReceiver *receiver) {
 	_receivers.push_back(receiver);
 }
 
-void PostRenderSignaller::removeReceiver(IPostRenderSignalReceiver* receiver) {
+void PlayMediaSignaller::removeReceiver(IPlayMediaSignalReceiver *receiver) {
 	for (size_t i = 0; i < _receivers.size(); i++) {
 		if (_receivers[i] == receiver) {
 			_receivers.remove_at(i);
@@ -4913,7 +4917,7 @@ Project::Segment::Segment() : weakStream(nullptr) {
 
 Project::Project(Runtime *runtime)
 	: _runtime(runtime), _projectFormat(Data::kProjectFormatUnknown), _isBigEndian(false),
-	  _haveGlobalObjectInfo(false), _haveProjectStructuralDef(false), _postRenderSignaller(new PostRenderSignaller()),
+	  _haveGlobalObjectInfo(false), _haveProjectStructuralDef(false), _playMediaSignaller(new PlayMediaSignaller()),
 	  _keyboardEventSignaller(new KeyboardEventSignaller()) {
 }
 
@@ -5196,12 +5200,12 @@ Common::SharedPtr<SegmentUnloadSignaller> Project::notifyOnSegmentUnload(int seg
 }
 
 void Project::onPostRender() {
-	_postRenderSignaller->onPostRender(_runtime, this);
+	_playMediaSignaller->playMedia(_runtime, this);
 }
 
-Common::SharedPtr<PostRenderSignaller> Project::notifyOnPostRender(IPostRenderSignalReceiver *receiver) {
-	_postRenderSignaller->addReceiver(receiver);
-	return _postRenderSignaller;
+Common::SharedPtr<PlayMediaSignaller> Project::notifyOnPlayMedia(IPlayMediaSignalReceiver *receiver) {
+	_playMediaSignaller->addReceiver(receiver);
+	return _playMediaSignaller;
 }
 
 void Project::onKeyboardEvent(Runtime *runtime, const Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt) {
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 38e895a39a0..5a62dbcdcbc 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -40,6 +40,12 @@
 
 class OSystem;
 
+namespace Audio {
+
+class Mixer;
+
+} // End of namespace Audio
+
 namespace Common {
 
 class RandomSource;
@@ -1368,7 +1374,7 @@ private:
 
 class Runtime {
 public:
-	explicit Runtime(OSystem *system, ISaveUIProvider *saveProvider, ILoadUIProvider *loadProvider);
+	explicit Runtime(OSystem *system, Audio::Mixer *mixer, ISaveUIProvider *saveProvider, ILoadUIProvider *loadProvider);
 
 	bool runFrame();
 	void drawFrame();
@@ -1454,6 +1460,8 @@ public:
 	ISaveUIProvider *getSaveProvider() const;
 	ILoadUIProvider *getLoadProvider() const;
 
+	Audio::Mixer *getAudioMixer() const;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	void debugSetEnabled(bool enabled);
 	void debugBreak();
@@ -1589,6 +1597,7 @@ private:
 
 	Scheduler _scheduler;
 	OSystem *_system;
+	Audio::Mixer *_mixer;
 	ISaveUIProvider *_saveProvider;
 	ILoadUIProvider *_loadProvider;
 
@@ -1881,21 +1890,21 @@ private:
 	Common::HashMap<Common::String, const IPlugInModifierFactory *> _factoryRegistry;
 };
 
-struct IPostRenderSignalReceiver {
-	virtual void onPostRender(Runtime *runtime, Project *project) = 0;
+struct IPlayMediaSignalReceiver {
+	virtual void playMedia(Runtime *runtime, Project *project) = 0;
 };
 
-class PostRenderSignaller {
+class PlayMediaSignaller {
 public:
-	PostRenderSignaller();
-	~PostRenderSignaller();
+	PlayMediaSignaller();
+	~PlayMediaSignaller();
 
-	void onPostRender(Runtime *runtime, Project *project);
-	void addReceiver(IPostRenderSignalReceiver *receiver);
-	void removeReceiver(IPostRenderSignalReceiver *receiver);
+	void playMedia(Runtime *runtime, Project *project);
+	void addReceiver(IPlayMediaSignalReceiver *receiver);
+	void removeReceiver(IPlayMediaSignalReceiver *receiver);
 
 private:
-	Common::Array<IPostRenderSignalReceiver *> _receivers;
+	Common::Array<IPlayMediaSignalReceiver *> _receivers;
 };
 
 struct ISegmentUnloadSignalReceiver {
@@ -1955,13 +1964,13 @@ public:
 	void openSegmentStream(int segmentIndex);
 	void closeSegmentStream(int segmentIndex);
 	Common::SeekableReadStream *getStreamForSegment(int segmentIndex);
-	Common::SharedPtr<SegmentUnloadSignaller> notifyOnSegmentUnload(int segmentIndex, ISegmentUnloadSignalReceiver *receiver);
 
 	void onPostRender();
-	Common::SharedPtr<PostRenderSignaller> notifyOnPostRender(IPostRenderSignalReceiver *receiver);
-
 	void onKeyboardEvent(Runtime *runtime, const Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt);
+
+	Common::SharedPtr<SegmentUnloadSignaller> notifyOnSegmentUnload(int segmentIndex, ISegmentUnloadSignalReceiver *receiver);
 	Common::SharedPtr<KeyboardEventSignaller> notifyOnKeyboardEvent(IKeyboardEventReceiver *receiver);
+	Common::SharedPtr<PlayMediaSignaller> notifyOnPlayMedia(IPlayMediaSignalReceiver *receiver);
 
 	const char *findAuthorMessageName(uint32 id) const;
 
@@ -2065,7 +2074,7 @@ private:
 	ObjectLinkingScope _structuralScope;
 	ObjectLinkingScope _modifierScope;
 
-	Common::SharedPtr<PostRenderSignaller> _postRenderSignaller;
+	Common::SharedPtr<PlayMediaSignaller> _playMediaSignaller;
 	Common::SharedPtr<KeyboardEventSignaller> _keyboardEventSignaller;
 
 	Runtime *_runtime;


Commit: 9c16acd00eec5ccbffd3ebefb3558ad6001ad25e
    https://github.com/scummvm/scummvm/commit/9c16acd00eec5ccbffd3ebefb3558ad6001ad25e
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: mToon support, more support for weird attribs.

Changed paths:
    engines/mtropolis/assets.cpp
    engines/mtropolis/assets.h
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h


diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index 29f139d6ed9..9d06c5fb450 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -106,6 +106,473 @@ size_t CachedAudio::getNumSamples(const AudioMetadata &metadata) const {
 	}
 }
 
+CachedMToon::CachedMToon() {
+}
+
+bool CachedMToon::loadFromStream(const Common::SharedPtr<MToonMetadata> &metadata, Common::ReadStream *stream, size_t size) {
+	_metadata = metadata;
+
+	Common::Array<uint8> data;
+	data.resize(size);
+	if (size > 0) {
+		stream->read(&data[0], size);
+		if (stream->err())
+			return false;
+	}
+
+	if (metadata->codecID == kMToonRLECodecID)
+		loadRLEFrames(data);
+
+	if (!metadata->temporalCompression)
+		decompressNonTemporalFrames(data);
+
+	return true;
+}
+
+void CachedMToon::decompressNonTemporalFrames(const Common::Array<uint8> &data) {
+	size_t numFrames = _metadata->frames.size();
+
+	_ntSurfaces.resize(numFrames);
+	_optimizedNTSurfaces.resize(numFrames);
+
+	for (size_t i = 0; i < numFrames; i++) {
+		if (_metadata->codecID == kMToonRLECodecID) {
+			decompressRLEFrame(i);
+		} else {
+			loadUncompressedFrame(data, i);
+		}
+	}
+
+	_dataRLE8.clear();
+	_dataRLE16.clear();
+	_dataRLE32.clear();
+}
+
+template<class TFrame, class TNumber, uint32 TLiteralMask, uint32 TTransparentRowSkipMask>
+static bool decompressMToonRLE(const TFrame &frame, Graphics::Surface &surface) {
+	assert(sizeof(TNumber) == surface.format.bytesPerPixel);
+
+	const Common::Array<TNumber> &coefsArray = frame.data;
+
+	size_t size = coefsArray.size();
+	if (size == 0)
+		return false;
+
+	const TNumber *coefs = &coefsArray[0];
+
+	size_t x = 0;
+	size_t y = 0;
+	size_t w = surface.w;
+	size_t h = surface.h;
+
+	if (w != frame.width || h != frame.height)
+		return false;
+
+	TNumber *rowData = static_cast<TNumber *>(surface.getBasePtr(0, 0));
+
+	for (;;) {
+		if (size == 0)
+			return false;
+		const TNumber rleCode = coefs[0];
+		coefs++;
+		size--;
+
+		size_t remainingInRow = w - x;
+
+		if (rleCode == 0) {
+			if (size == 0)
+				return false;
+			const TNumber transparentCountCode = coefs[0];
+			coefs++;
+			size--;
+
+			if (transparentCountCode & TTransparentRowSkipMask) {
+				// Vertical skip
+				y += (transparentCountCode - TTransparentRowSkipMask);
+				x = 0;
+				if (y < h) {
+					rowData = static_cast<TNumber *>(surface.getBasePtr(0, y));
+					continue;
+				} else {
+					break;
+				}
+			} else {
+				// Horizontal skip
+				const size_t horizontalSkip = transparentCountCode;
+				if (horizontalSkip > remainingInRow)
+					return false;
+				x += horizontalSkip;
+			}
+		} else if (rleCode & TLiteralMask) {
+			// Literals
+			const size_t numLiterals = (rleCode - TLiteralMask);
+			if (numLiterals > size || numLiterals > remainingInRow)
+				return false;
+			memcpy(rowData + x, coefs, sizeof(TNumber) * numLiterals);
+			coefs += numLiterals;
+			size -= numLiterals;
+			x += numLiterals;
+		} else {
+			// Literals
+			const size_t numCopies = rleCode;
+			if (numCopies > remainingInRow || size == 0)
+				return false;
+			const TNumber repeatedValue = coefs[0];
+			for (size_t i = 0; i < numCopies; i++)
+				rowData[x + i] = repeatedValue;
+			coefs++;
+			size--;
+			x += numCopies;
+		}
+
+		if (x == w) {
+			y++;
+			if (y < h) {
+				rowData = static_cast<TNumber *>(surface.getBasePtr(0, y));
+				continue;
+			} else {
+				break;
+			}
+		}
+	}
+
+	return true;
+}
+
+void CachedMToon::decompressRLEFrameToImage(size_t frameIndex, Graphics::Surface &surface) {
+	assert(surface.format == _rleOptimizedFormat);
+
+	const MToonMetadata::FrameDef &frameDef = _metadata->frames[frameIndex];
+
+	int32 originX = frameDef.rect.left;
+	int32 originY = frameDef.rect.top;
+
+	bool decompressedOK = false;
+	if (_rleOptimizedFormat.bytesPerPixel == 32) {
+		decompressedOK = decompressMToonRLE<Rle32Frame, uint32, 0x80000000u, 0x80000000u>(_dataRLE32[frameIndex], surface);
+	} else if (_rleOptimizedFormat.bytesPerPixel == 16) {
+		decompressedOK = decompressMToonRLE<Rle16Frame, uint16, 0x8000u, 0x8000u>(_dataRLE16[frameIndex], surface);
+	} else if (_rleOptimizedFormat.bytesPerPixel == 8) {
+		decompressedOK = decompressMToonRLE<Rle8Frame, uint8, 0x80u, 0u>(_dataRLE8[frameIndex], surface);
+	} else
+		error("Unknown mToon encoding");
+
+	if (!decompressedOK)
+		warning("mToon RLE frame decompression failed");
+}
+
+void CachedMToon::loadRLEFrames(const Common::Array<uint8> &data) {
+	size_t numFrames = _metadata->frames.size();
+	uint16 bpp = _metadata->bitsPerPixel;
+
+	if (bpp == 8)
+		_dataRLE8.resize(numFrames);
+	else if (bpp == 16)
+		_dataRLE16.resize(numFrames);
+	else
+		error("Unsupported RLE encoding");
+
+	for (size_t i = 0; i < numFrames; i++) {
+		const MToonMetadata::FrameDef &frameDef = _metadata->frames[i];
+
+		RleFrame *rleFrame = nullptr;
+		if (bpp == 8)
+			rleFrame = &_dataRLE8[0];
+		else if (bpp == 16)
+			rleFrame = &_dataRLE16[0];
+
+		size_t baseOffset = frameDef.dataOffset;
+
+		uint32 headerInts[5];
+		for (size_t hi = 0; hi < 5; hi++) {
+			uint32 unpacked = 0;
+			for (size_t b = 0; b < 4; b++)
+				unpacked = (unpacked << 8) + data[baseOffset + hi * 4 + b];
+			headerInts[hi] = unpacked;
+		}
+
+		rleFrame->isKeyframe = (headerInts[0] == kMToonRLEKeyframePrefix);
+		if (headerInts[1] == 0x01000001) {
+			if (bpp != 8)
+				error("Unknown mToon encoding");
+		} else if (headerInts[1] == 0x01000002) {
+			if (bpp != 16)
+				error("Unknown mToon encoding");
+		} else
+			error("Unknown mToon encoding");
+
+		rleFrame->version = headerInts[1];
+		rleFrame->width = headerInts[2];
+		rleFrame->height = headerInts[3];
+
+		uint32 frameDataSize = headerInts[4];
+
+		if (frameDataSize > 0) {
+			if (bpp == 8) {
+				_dataRLE8[i].data.resize(frameDataSize);
+				memcpy(&_dataRLE8[i].data[0], &data[baseOffset + 20], frameDataSize);
+			} else if (bpp == 16) {
+				uint32 numDWords = frameDataSize / 2;
+				_dataRLE16[i].data.resize(numDWords);
+				memcpy(&_dataRLE16[i].data[0], &data[baseOffset + 20], numDWords * 2);
+
+				uint16 *i16 = &_dataRLE16[i].data[0];
+				for (size_t swapIndex = 0; swapIndex < numDWords; swapIndex++)
+					i16[swapIndex] = FROM_BE_16(i16[swapIndex]);
+			} else
+				error("Unknown mToon encoding");
+		}
+	}
+
+	if (bpp == 8)
+		_rleInternalFormat = Graphics::PixelFormat::createFormatCLUT8();
+	else if (bpp == 16)
+		_rleInternalFormat = Graphics::PixelFormat(2, 5, 5, 5, 0, 10, 5, 0, 15);
+	else
+		error("Unknown mToon encoding");
+
+	_rleOptimizedFormat = _rleInternalFormat;
+}
+
+void CachedMToon::decompressRLEFrame(size_t frameIndex) {
+	Common::SharedPtr<Graphics::Surface> surface(new Graphics::Surface());
+
+	RleFrame *frame = nullptr;
+	if (_rleInternalFormat.bytesPerPixel == 1)
+		frame = &_dataRLE8[frameIndex];
+	else if (_rleInternalFormat.bytesPerPixel == 2)
+		frame = &_dataRLE16[frameIndex];
+	else
+		error("Unknown mToon encoding");
+
+	surface->create(frame->width, frame->height, _rleInternalFormat);
+
+	decompressRLEFrameToImage(frameIndex, *surface);
+
+	this->_ntSurfaces[frameIndex] = surface;
+}
+
+void CachedMToon::loadUncompressedFrame(const Common::Array<uint8> &data, size_t frameIndex) {
+	const MToonMetadata::FrameDef &frameDef = _metadata->frames[frameIndex];
+	uint16 stride = frameDef.decompressedBytesPerRow;
+
+	uint16 bpp = _metadata->bitsPerPixel;
+
+	Common::SharedPtr<Graphics::Surface> surface(new Graphics::Surface());
+	Graphics::PixelFormat pixFmt;
+
+	if (bpp == 1 || bpp == 2 || bpp == 4 || bpp == 8)
+		pixFmt = Graphics::PixelFormat::createFormatCLUT8();
+	else if (bpp == 16)
+		pixFmt = Graphics::PixelFormat(2, 5, 5, 5, 0, 10, 5, 0, 15);
+	else if (bpp == 32)
+		pixFmt = Graphics::PixelFormat(4, 8, 8, 8, 0, 0, 8, 16, 24);
+	else
+		error("Unknown mToon encoding");
+
+	size_t w = frameDef.rect.getWidth();
+	size_t h = frameDef.rect.getHeight();
+
+	surface->create(w, h, pixFmt);
+
+	for (size_t row = 0; row < h; row++) {
+		const uint8 *inData = &data[frameDef.dataOffset + row * stride];
+		void *outDataUntyped = nullptr;
+
+		if (_metadata->imageFormat == MToonMetadata::kImageFormatWindows)
+			outDataUntyped = surface->getBasePtr(0, h - 1 - row);
+		else if (_metadata->imageFormat == MToonMetadata::kImageFormatMac)
+			outDataUntyped = surface->getBasePtr(0, row);
+		else
+			error("Unimplemented mToon uncompressed image layout");
+
+		if (bpp == 1) {
+			for (size_t col = 0; col < w; col++)
+				static_cast<uint8 *>(outDataUntyped)[col] = (inData[col / 8] >> (7 - (col % 8))) & 1;
+		} else if (bpp == 2) {
+			for (size_t col = 0; col < w; col++)
+				static_cast<uint8 *>(outDataUntyped)[col] = (inData[col / 4] >> (6 - (col % 4) * 2)) & 3;
+		} else if (bpp == 4) {
+			for (size_t col = 0; col < w; col++)
+				static_cast<uint8 *>(outDataUntyped)[col] = (inData[col / 2] >> (4 - (col % 2) * 4)) & 15;
+		} else if (bpp == 8) {
+			for (size_t col = 0; col < w; col++)
+				static_cast<uint8 *>(outDataUntyped)[col] = inData[col];
+		} else if (bpp == 16) {
+			if (_metadata->imageFormat == MToonMetadata::kImageFormatMac) {
+				for (size_t col = 0; col < w; col++)
+					static_cast<uint16 *>(outDataUntyped)[col] = (inData[col * 2] << 8) | (inData[col * 2 + 1]);
+			} else if (_metadata->imageFormat == MToonMetadata::kImageFormatWindows) {
+				for (size_t col = 0; col < w; col++)
+					static_cast<uint16 *>(outDataUntyped)[col] = (inData[col * 2 + 1] << 8) | (inData[col * 2]);
+			}
+		} else if (bpp == 32) {
+			for (size_t col = 0; col < w; col++)
+				static_cast<uint32 *>(outDataUntyped)[col] = (0xff000000 | (inData[col * 4]) | (inData[col * 4 + 1] << 8) | (inData[col * 4 + 2] << 16));
+		}
+	}
+
+	_ntSurfaces[frameIndex] = surface;
+}
+
+template<class TSrcFrame, class TSrcNumber, uint32 TSrcLiteralMask, uint32 TSrcTransparentSkipMask, class TDestFrame, class TDestNumber, uint32 TDestLiteralMask, uint32 TDestTransparentSkipMask>
+void CachedMToon::rleReformat(const TSrcFrame &srcFrame, const Graphics::PixelFormat &srcFormatRef, TDestFrame &destFrame, const Graphics::PixelFormat &destFormatRef) {
+	const Graphics::PixelFormat srcFormat = srcFormatRef;
+	const Graphics::PixelFormat destFormat = destFormatRef;
+
+	size_t offset = 0;
+	destFrame.data.resize(srcFrame.data.size());
+	destFrame.width = srcFrame.width;
+	destFrame.height = srcFrame.height;
+	destFrame.isKeyframe = srcFrame.isKeyframe;
+	destFrame.version = srcFrame.version;
+
+	while (offset < srcFrame.data.size()) {
+		const uint32 rleCode = srcFrame.data[offset];
+		if (rleCode == 0) {
+			destFrame.data[offset] = 0;
+			offset++;
+
+			uint32 numTransparentCode = srcFrame.data[offset];
+			if (numTransparentCode & TSrcTransparentSkipMask)
+				destFrame.data[offset] = (numTransparentCode - TSrcTransparentSkipMask) + TDestTransparentSkipMask;
+			else
+				destFrame.data[offset] = numTransparentCode;
+			offset++;
+		} else if (rleCode & TSrcLiteralMask) {
+			uint32 numLiterals = rleCode - TSrcLiteralMask;
+
+			destFrame.data[offset] = numLiterals + TDestLiteralMask;
+			offset++;
+
+			while (numLiterals) {
+				uint8 a, r, g, b;
+				srcFormat.colorToARGB(srcFrame.data[offset], a, r, g, b);
+				destFrame.data[offset] = destFormat.ARGBToColor(a, r, g, b);
+				offset++;
+				numLiterals--;
+			}
+		} else {
+			destFrame.data[offset] = rleCode;
+			offset++;
+
+			uint8 a, r, g, b;
+			srcFormat.colorToARGB(srcFrame.data[offset], a, r, g, b);
+			destFrame.data[offset] = destFormat.ARGBToColor(a, r, g, b);
+			offset++;
+		}
+	}
+}
+
+void CachedMToon::optimize(Runtime *runtime) {
+	Graphics::PixelFormat renderFmt = runtime->getRenderPixelFormat();
+	if (!_metadata->temporalCompression) {
+		optimizeNonTemporal(renderFmt);
+	} else if (_metadata->codecID == kMToonRLECodecID) {
+		optimizeRLE(renderFmt);
+	}
+}
+
+void CachedMToon::optimizeNonTemporal(const Graphics::PixelFormat &targetFormatRef) {
+	const Graphics::PixelFormat targetFormat = targetFormatRef;
+
+	_optimizedNTSurfaces.resize(_ntSurfaces.size());
+
+	for (size_t i = 0; i < _ntSurfaces.size(); i++) {
+		Common::SharedPtr<Graphics::Surface> srcSurface = _ntSurfaces[i];
+		Common::SharedPtr<Graphics::Surface> &optimizedSurfRef = _optimizedNTSurfaces[i];
+
+		// FIXME: Aggregate these checks and merge into a single format field
+		if (optimizedSurfRef == nullptr || optimizedSurfRef->format != targetFormat) {
+			if (targetFormat.bytesPerPixel > 1 && srcSurface->format.bytesPerPixel > 1) {
+				if (targetFormat.bytesPerPixel == srcSurface->format.bytesPerPixel) {
+					srcSurface->convertToInPlace(targetFormat);
+					optimizedSurfRef = srcSurface;
+				} else {
+					optimizedSurfRef.reset();
+					optimizedSurfRef.reset(srcSurface->convertTo(targetFormat));
+				}
+			} else {
+				optimizedSurfRef = srcSurface;
+			}
+		}
+	}
+}
+
+void CachedMToon::optimizeRLE(const Graphics::PixelFormat &targetFormatRef) {
+	const Graphics::PixelFormat targetFormat = targetFormatRef;
+
+	if (targetFormat == _rleOptimizedFormat)
+		return;
+
+	if (_rleInternalFormat.bytesPerPixel != 2 && _rleInternalFormat.bytesPerPixel != 4)
+		return;	// Can't optimize
+
+	size_t numFrames = _metadata->frames.size();
+	for (size_t i = 0; i < numFrames; i++) {
+		if (_rleInternalFormat.bytesPerPixel == 2) {
+			if (targetFormat.bytesPerPixel == 4)
+				rleReformat<Rle16Frame, uint16, 0x8000u, 0x8000u, Rle32Frame, uint32, 0x80000000u, 0x80000000u>(_dataRLE16[i], _rleInternalFormat, _dataRLE32[i], targetFormat);
+			else if (targetFormat.bytesPerPixel == 2)
+				rleReformat<Rle16Frame, uint16, 0x8000u, 0x8000u, Rle16Frame, uint16, 0x8000u, 0x8000u>(_dataRLE16[i], _rleInternalFormat, _dataRLE16[i], targetFormat);
+		} else if (_rleInternalFormat.bytesPerPixel == 4) {
+			if (targetFormat.bytesPerPixel == 4)
+				rleReformat<Rle32Frame, uint32, 0x80000000u, 0x80000000u, Rle32Frame, uint32, 0x80000000u, 0x80000000u>(_dataRLE32[i], _rleInternalFormat, _dataRLE32[i], targetFormat);
+			else if (targetFormat.bytesPerPixel == 2)
+				rleReformat<Rle32Frame, uint32, 0x80000000u, 0x80000000u, Rle16Frame, uint16, 0x8000u, 0x8000u>(_dataRLE32[i], _rleInternalFormat, _dataRLE16[i], targetFormat);
+		}
+	}
+
+	if (_rleInternalFormat.bytesPerPixel == targetFormat.bytesPerPixel)
+		_rleInternalFormat = targetFormat;
+
+	_rleOptimizedFormat = targetFormat;
+}
+
+void CachedMToon::getOrRenderFrame(uint32 prevFrame, uint32 targetFrame, Common::SharedPtr<Graphics::Surface>& surface) const {
+	if (!_metadata->temporalCompression) {
+		surface = _optimizedNTSurfaces[targetFrame];
+	} else if (_metadata->codecID == kMToonRLECodecID) {
+		uint32 firstFrameToRender = 0;
+		uint32 backStopFrame = 0;
+
+		if (surface && surface->format != _rleOptimizedFormat)
+			surface.reset();
+
+		if (surface != nullptr) {
+			if (prevFrame == targetFrame)
+				return;
+			if (prevFrame < targetFrame)
+				backStopFrame = prevFrame;
+		}
+
+		firstFrameToRender = targetFrame;
+		while (firstFrameToRender > backStopFrame) {
+			if (_metadata->frames[firstFrameToRender].isKeyFrame)
+				break;
+			firstFrameToRender--;
+		}
+
+		if (!surface || surface->format != _rleOptimizedFormat) {
+			surface.reset(new Graphics::Surface());
+			surface->create(_metadata->rect.getWidth(), _metadata->rect.getHeight(), _rleOptimizedFormat);
+		}
+
+		for (size_t i = firstFrameToRender; i <= targetFrame; i++) {
+			if (_rleOptimizedFormat.bytesPerPixel == 1)
+				decompressMToonRLE<Rle8Frame, uint8, 0x80u, 0>(_dataRLE8[i], *surface);
+			else if (_rleOptimizedFormat.bytesPerPixel == 2)
+				decompressMToonRLE<Rle16Frame, uint16, 0x8000u, 0x8000u>(_dataRLE16[i], *surface);
+			else if (_rleOptimizedFormat.bytesPerPixel == 4)
+				decompressMToonRLE<Rle32Frame, uint32, 0x80000000u, 0x80000000u>(_dataRLE32[i], *surface);
+		}
+	}
+}
+
+const Common::SharedPtr<MToonMetadata>& CachedMToon::getMetadata() const {
+	return _metadata;
+}
+
 bool AudioAsset::load(AssetLoaderContext &context, const Data::AudioAsset &data) {
 	_assetID = data.assetID;
 
@@ -488,35 +955,41 @@ const Common::SharedPtr<CachedImage> &ImageAsset::loadAndCacheImage(Runtime *run
 }
 
 bool MToonAsset::load(AssetLoaderContext &context, const Data::MToonAsset &data) {
+	_streamIndex = context.streamIndex;
+	_assetID = data.assetID;
+
+	_metadata.reset(new MToonMetadata());
+
 	if (data.haveMacPart)
-		_imageFormat = kImageFormatMac;
+		_metadata->imageFormat = MToonMetadata::kImageFormatMac;
 	else if (data.haveWinPart)
-		_imageFormat = kImageFormatWindows;
+		_metadata->imageFormat = MToonMetadata::kImageFormatWindows;
 	else
 		return false;
 
 	_frameDataPosition = data.frameDataPosition;
 	_sizeOfFrameData = data.sizeOfFrameData;
 
-	if (!_rect.load(data.rect))
+	if (!_metadata->rect.load(data.rect))
 		return false;
 
-	_bitsPerPixel = data.bitsPerPixel;
-	_codecID = data.codecID;
+	_metadata->bitsPerPixel = data.bitsPerPixel;
+	_metadata->codecID = data.codecID;
 
-	_frames.resize(data.frames.size());
-	for (size_t i = 0; i < _frames.size(); i++) {
-		if (!_frames[i].load(context, data.frames[i]))
+	_metadata->frames.resize(data.frames.size());
+	for (size_t i = 0; i < data.frames.size(); i++) {
+		if (!_metadata->frames[i].load(context, data.frames[i]))
 			return false;
 	}
 
-	_frameRanges.resize(data.frameRangesPart.frameRanges.size());
-	for (size_t i = 0; i < _frameRanges.size(); i++) {
-		if (!_frameRanges[i].load(context, data.frameRangesPart.frameRanges[i]))
+	_metadata->frameRanges.resize(data.frameRangesPart.frameRanges.size());
+	for (size_t i = 0; i < data.frameRangesPart.frameRanges.size(); i++) {
+		if (!_metadata->frameRanges[i].load(context, data.frameRangesPart.frameRanges[i]))
 			return false;
 	}
 
-	_codecData = data.codecData;
+	_metadata->codecData = data.codecData;
+	_metadata->temporalCompression = ((data.encodingFlags & Data::MToonAsset::kEncodingFlag_TemporalCompression) != 0);
 
 	return true;
 }
@@ -525,7 +998,33 @@ AssetType MToonAsset::getAssetType() const {
 	return kAssetTypeMToon;
 }
 
-bool MToonAsset::FrameDef::load(AssetLoaderContext &context, const Data::MToonAsset::FrameDef &data) {
+const Common::SharedPtr<CachedMToon> &MToonAsset::loadAndCacheMToon(Runtime *runtime) {
+	if (_cachedMToon)
+		return _cachedMToon;
+
+	Common::SharedPtr<CachedMToon> cachedMToon(new CachedMToon());
+
+	size_t streamIndex = _streamIndex;
+	int segmentIndex = runtime->getProject()->getSegmentForStreamIndex(streamIndex);
+	runtime->getProject()->openSegmentStream(segmentIndex);
+	Common::SeekableReadStream *stream = runtime->getProject()->getStreamForSegment(segmentIndex);
+
+	if (!stream || !stream->seek(_frameDataPosition)) {
+		warning("Couldn't seek stream to mToon data");
+		return _cachedMToon;
+	}
+
+	if (!cachedMToon->loadFromStream(_metadata, stream, _sizeOfFrameData)) {
+		warning("mToon data failed to load");
+		return _cachedMToon;
+	}
+
+	_cachedMToon = cachedMToon;
+
+	return _cachedMToon;
+}
+
+bool MToonMetadata::FrameDef::load(AssetLoaderContext &context, const Data::MToonAsset::FrameDef &data) {
 	compressedSize = data.compressedSize;
 	dataOffset = data.dataOffset;
 	decompressedBytesPerRow = data.decompressedBytesPerRow;
@@ -537,7 +1036,7 @@ bool MToonAsset::FrameDef::load(AssetLoaderContext &context, const Data::MToonAs
 	return true;
 }
 
-bool MToonAsset::FrameRangeDef::load(AssetLoaderContext &context, const Data::MToonAsset::FrameRangeDef &data) {
+bool MToonMetadata::FrameRangeDef::load(AssetLoaderContext &context, const Data::MToonAsset::FrameRangeDef &data) {
 	name = data.name;
 	startFrame = data.startFrame;
 	endFrame = data.endFrame;
diff --git a/engines/mtropolis/assets.h b/engines/mtropolis/assets.h
index a4e9d787b5c..cd130ef6629 100644
--- a/engines/mtropolis/assets.h
+++ b/engines/mtropolis/assets.h
@@ -33,6 +33,7 @@ namespace MTropolis {
 
 struct AssetLoaderContext;
 struct AudioMetadata;
+struct MToonMetadata;
 class AudioPlayer;
 
 class ColorTableAsset : public Asset {
@@ -59,6 +60,104 @@ private:
 	Common::Array<uint8> _data;
 };
 
+struct MToonMetadata {
+	enum ImageFormat {
+		kImageFormatMac,
+		kImageFormatWindows,
+	};
+
+	struct FrameDef {
+		Rect16 rect;
+		uint32 dataOffset;
+		uint32 compressedSize;
+		uint32 decompressedSize;
+		uint16 decompressedBytesPerRow;
+		bool isKeyFrame;
+
+		bool load(AssetLoaderContext &context, const Data::MToonAsset::FrameDef &data);
+	};
+
+	struct FrameRangeDef {
+		uint32 startFrame;
+		uint32 endFrame;
+
+		Common::String name;
+
+		bool load(AssetLoaderContext &context, const Data::MToonAsset::FrameRangeDef &data);
+	};
+
+	ImageFormat imageFormat;
+
+	Rect16 rect;
+	uint16 bitsPerPixel;
+	uint32 codecID;
+	bool temporalCompression;
+
+	Common::Array<FrameDef> frames;
+	Common::Array<FrameRangeDef> frameRanges;
+	Common::Array<uint8> codecData;
+};
+
+class CachedMToon {
+public:
+	CachedMToon();
+
+	bool loadFromStream(const Common::SharedPtr<MToonMetadata> &metadata, Common::ReadStream *stream, size_t size);
+
+	void optimize(Runtime *runtime);
+
+	void getOrRenderFrame(uint32 prevFrame, uint32 targetFrame, Common::SharedPtr<Graphics::Surface> &surface) const;
+	const Common::SharedPtr<MToonMetadata> &getMetadata() const;
+
+private:
+	void optimizeNonTemporal(const Graphics::PixelFormat &targetFormat);
+	void optimizeRLE(const Graphics::PixelFormat &targetFormat);
+
+	struct RleFrame {
+		uint32 version;
+		uint32 width;
+		uint32 height;
+		bool isKeyframe;
+	};
+
+	struct Rle32Frame : public RleFrame {
+		Common::Array<uint32> data;
+	};
+
+	struct Rle16Frame : public RleFrame {
+		Common::Array<uint16> data;
+	};
+
+	struct Rle8Frame : public RleFrame {
+		Common::Array<uint8> data;
+	};
+
+	static const uint32 kMToonRLECodecID = 0x2e524c45;
+	static const uint32 kMToonRLEKeyframePrefix = 0x524c4520;
+	static const uint32 kMToonRLETemporalFramePrefix = 1;
+
+	void decompressNonTemporalFrames(const Common::Array<uint8> &data);
+	void decompressRLEFrameToImage(size_t frameIndex, Graphics::Surface &surface);
+	void loadRLEFrames(const Common::Array<uint8> &data);
+	void decompressRLEFrame(size_t frameIndex);
+	void loadUncompressedFrame(const Common::Array<uint8> &data, size_t frameIndex);
+
+	template<class TSrcFrame, class TSrcNumber, uint32 TSrcLiteralMask, uint32 TSrcTransparentSkipMask, class TDestFrame, class TDestNumber, uint32 TDestLiteralMask, uint32 TDestTransparentSkipMask>
+	void rleReformat(const TSrcFrame &srcFrame, const Graphics::PixelFormat &srcFormatRef, TDestFrame &destFrame, const Graphics::PixelFormat &destFormatRef);
+
+	Common::Array<Rle8Frame> _dataRLE8;
+	Common::Array<Rle16Frame> _dataRLE16;
+	Common::Array<Rle32Frame> _dataRLE32;
+
+	Common::Array<Common::SharedPtr<Graphics::Surface> > _ntSurfaces;
+	Common::Array<Common::SharedPtr<Graphics::Surface> > _optimizedNTSurfaces;
+
+	Graphics::PixelFormat _rleInternalFormat;
+	Graphics::PixelFormat _rleOptimizedFormat;
+
+	Common::SharedPtr<MToonMetadata> _metadata;
+};
+
 struct AudioMetadata {
 	struct CuePoint {
 		uint32 position;
@@ -177,44 +276,15 @@ public:
 	bool load(AssetLoaderContext &context, const Data::MToonAsset &data);
 	AssetType getAssetType() const override;
 
-	enum ImageFormat {
-		kImageFormatMac,
-		kImageFormatWindows,
-	};
-
-	struct FrameDef {
-		Rect16 rect;
-		uint32 dataOffset;
-		uint32 compressedSize;
-		uint32 decompressedSize;
-		uint16 decompressedBytesPerRow;
-		bool isKeyFrame;
-
-		bool load(AssetLoaderContext &context, const Data::MToonAsset::FrameDef &data);
-	};
-
-	struct FrameRangeDef {
-		uint32 startFrame;
-		uint32 endFrame;
-
-		Common::String name;
-
-		bool load(AssetLoaderContext &context, const Data::MToonAsset::FrameRangeDef &data);
-	};
+	const Common::SharedPtr<CachedMToon> &loadAndCacheMToon(Runtime *runtime);
 
 private:
-	ImageFormat _imageFormat;
-
 	uint32 _frameDataPosition;
 	uint32 _sizeOfFrameData;
+	size_t _streamIndex;
 
-	Rect16 _rect;
-	uint16 _bitsPerPixel;
-	uint32 _codecID;
-
-	Common::Array<FrameDef> _frames;
-	Common::Array<FrameRangeDef> _frameRanges;
-	Common::Array<uint8> _codecData;
+	Common::SharedPtr<MToonMetadata> _metadata;
+	Common::SharedPtr<CachedMToon> _cachedMToon;
 };
 
 class TextAsset : public Asset {
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index de20d8b2939..ddb142a3784 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -401,10 +401,22 @@ bool ImageElement::load(ElementLoaderContext &context, const Data::ImageElement
 }
 
 bool ImageElement::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "text") {
+		// Obsidian accesses this on an image element in the menus, and if it fails, the "save first" warning
+		// prompt buttons aren't layered correctly?
+		result.setString(_text);
+		return true;
+	}
+
 	return VisualElement::readAttribute(thread, result, attrib);
 }
 
 MiniscriptInstructionOutcome ImageElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
+	if (attrib == "text") {
+		DynamicValueWriteStringHelper::create(&_text, writeProxy);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
 	return VisualElement::writeRefAttribute(thread, writeProxy, attrib);
 }
 
@@ -438,7 +450,7 @@ void ImageElement::render(Window *window) {
 	}
 }
 
-MToonElement::MToonElement() : _cel1Based(1) {
+MToonElement::MToonElement() : _cel1Based(1), _renderedFrame(0), _flushPriority(0) {
 }
 
 MToonElement::~MToonElement() {
@@ -463,6 +475,9 @@ bool MToonElement::readAttribute(MiniscriptThread *thread, DynamicValue &result,
 	if (attrib == "cel") {
 		result.setInt(_cel1Based);
 		return true;
+	} else if (attrib == "flushpriority") {
+		result.setInt(_flushPriority);
+		return true;
 	}
 
 	return VisualElement::readAttribute(thread, result, attrib);
@@ -473,12 +488,31 @@ MiniscriptInstructionOutcome MToonElement::writeRefAttribute(MiniscriptThread *t
 		// TODO proper support
 		DynamicValueWriteIntegerHelper<uint32>::create(&_cel1Based, result);
 		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "flushpriority") {
+		DynamicValueWriteIntegerHelper<int32>::create(&_flushPriority, result);
+		return kMiniscriptInstructionOutcomeContinue;
+
 	}
 
 	return VisualElement::writeRefAttribute(thread, result, attrib);
 }
 
 void MToonElement::activate() {
+	Project *project = _runtime->getProject();
+	Common::SharedPtr<Asset> asset = project->getAssetByID(_assetID).lock();
+
+	if (!asset) {
+		warning("mToon element references asset %i but the asset isn't loaded!", _assetID);
+		return;
+	}
+
+	if (asset->getAssetType() != kAssetTypeMToon) {
+		warning("mToon element assigned an asset that isn't an mToon");
+		return;
+	}
+
+	_cachedMToon = static_cast<MToonAsset *>(asset.get())->loadAndCacheMToon(_runtime);
+	_metadata = _cachedMToon->getMetadata();
 }
 
 void MToonElement::deactivate() {
@@ -486,6 +520,21 @@ void MToonElement::deactivate() {
 }
 
 void MToonElement::render(Window *window) {
+	if (_cachedMToon) {
+		_cachedMToon->optimize(_runtime);
+		uint32 frame = 0;
+
+		if (_cel1Based < 1)
+			frame = 0;
+		else if (_cel1Based > _metadata->frames.size())
+			frame = _metadata->frames.size() - 1;
+		else
+			frame = _cel1Based - 1;
+
+		_cachedMToon->getOrRenderFrame(_renderedFrame, frame, _renderSurface);
+		_renderedFrame = frame;
+	}
+
 	if (_renderSurface) {
 		Common::Rect srcRect(_renderSurface->w, _renderSurface->h);
 		Common::Rect destRect(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y, _cachedAbsoluteOrigin.x + _rect.getWidth(), _cachedAbsoluteOrigin.y + _rect.getHeight());
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index 3e5a8b06583..5293e879df7 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -39,8 +39,10 @@ namespace MTropolis {
 class AudioPlayer;
 class CachedAudio;
 class CachedImage;
+class CachedMToon;
 struct AudioMetadata;
 struct ElementLoaderContext;
+struct MToonMetadata;
 
 enum MediaState {
 	kMediaStatePlaying,
@@ -141,6 +143,8 @@ private:
 
 	Common::SharedPtr<CachedImage> _cachedImage;
 
+	Common::String _text;	// ...???
+
 	Runtime *_runtime;
 };
 
@@ -173,9 +177,14 @@ private:
 	uint32 _assetID;
 	uint32 _rateTimes10000;
 	uint32 _cel1Based;
+	int32 _flushPriority;
 
 	Runtime *_runtime;
 	Common::SharedPtr<Graphics::Surface> _renderSurface;
+	uint32 _renderedFrame;
+
+	Common::SharedPtr<MToonMetadata> _metadata;
+	Common::SharedPtr<CachedMToon> _cachedMToon;
 };
 
 class TextLabelElement : public VisualElement {


Commit: 5ab54de82a86b77f3de42eb83e51d3c1fd9ce1ff
    https://github.com/scummvm/scummvm/commit/5ab54de82a86b77f3de42eb83e51d3c1fd9ce1ff
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix enough things for Obsidian forest intro to be completable (sometimes)

Changed paths:
  A engines/mtropolis/hacks.cpp
  A engines/mtropolis/hacks.h
    engines/mtropolis/assets.cpp
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/module.mk
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index 9d06c5fb450..2fdfb5c104a 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -248,11 +248,11 @@ void CachedMToon::decompressRLEFrameToImage(size_t frameIndex, Graphics::Surface
 	int32 originY = frameDef.rect.top;
 
 	bool decompressedOK = false;
-	if (_rleOptimizedFormat.bytesPerPixel == 32) {
+	if (_rleOptimizedFormat.bytesPerPixel == 4) {
 		decompressedOK = decompressMToonRLE<Rle32Frame, uint32, 0x80000000u, 0x80000000u>(_dataRLE32[frameIndex], surface);
-	} else if (_rleOptimizedFormat.bytesPerPixel == 16) {
+	} else if (_rleOptimizedFormat.bytesPerPixel == 2) {
 		decompressedOK = decompressMToonRLE<Rle16Frame, uint16, 0x8000u, 0x8000u>(_dataRLE16[frameIndex], surface);
-	} else if (_rleOptimizedFormat.bytesPerPixel == 8) {
+	} else if (_rleOptimizedFormat.bytesPerPixel == 1) {
 		decompressedOK = decompressMToonRLE<Rle8Frame, uint8, 0x80u, 0u>(_dataRLE8[frameIndex], surface);
 	} else
 		error("Unknown mToon encoding");
diff --git a/engines/mtropolis/hacks.cpp b/engines/mtropolis/hacks.cpp
new file mode 100644
index 00000000000..5b08440540b
--- /dev/null
+++ b/engines/mtropolis/hacks.cpp
@@ -0,0 +1,32 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mtropolis/hacks.h"
+
+#include "common/system.h"
+
+namespace MTropolis {
+
+Hacks::Hacks() {
+	memset(this, 0, sizeof(*this));
+}
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/hacks.h b/engines/mtropolis/hacks.h
new file mode 100644
index 00000000000..8f67a0e68ba
--- /dev/null
+++ b/engines/mtropolis/hacks.h
@@ -0,0 +1,39 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace MTropolis {
+
+struct Hacks {
+	Hacks();
+
+	// Workaround for bug in Obsidian:
+	// When opening the journal in the intro, a script checks if cGSt.cfst.binjournal is false and if so,
+	// sets cGSt.cfst.binjournal to true and then sets including setting cJournalConst.aksjournpath to the
+	// main journal scene path.  That scene path is used to resolve the scene to go to after clicking
+	// the "Continue" button on the warning that pops up.
+	//
+	// The problem is that cJournalConst uses a project name that doesn't match the retail data, and
+	// cJournalConst is unloaded if the player leaves the journal.  This causes a progression blocker if
+	// the player leaves the journal without clicking Continue.
+	bool ignoreMismatchedProjectNameInObjectLookups;
+};
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 3fd733fc82c..ea305ee2f7b 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -1826,7 +1826,7 @@ VThreadState MiniscriptThread::resume(const ResumeTaskData &taskData) {
 	if (instrsArray.size() == 0)
 		return kVThreadReturn;
 
-	if (_modifier->getStaticGUID() == 0x985d1) {
+	if (_modifier->getStaticGUID() == 0x48890) {
 		int n = 0;
 	}
 
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 918c1def102..8b19d8bc34d 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -716,9 +716,14 @@ VThreadState TimerMessengerModifier::consumeMessage(Runtime *runtime, const Comm
 		if (_scheduledEvent)
 			_scheduledEvent->cancel();
 	} else if (_executeWhen.respondsTo(msg->getEvent())) {
-		debug(3, "Timer %x '%s' scheduled to execute in %i milliseconds", getStaticGUID(), getName().c_str(), _milliseconds);
+		// 0-time events are not allowed
+		uint32 realMilliseconds = _milliseconds;
+		if (realMilliseconds == 0)
+			realMilliseconds = 1;
+
+		debug(3, "Timer %x '%s' scheduled to execute in %i milliseconds", getStaticGUID(), getName().c_str(), realMilliseconds);
 		if (!_scheduledEvent) {
-			_scheduledEvent = runtime->getScheduler().scheduleMethod<TimerMessengerModifier, &TimerMessengerModifier::activate>(runtime->getPlayTime() + _milliseconds, this);
+			_scheduledEvent = runtime->getScheduler().scheduleMethod<TimerMessengerModifier, &TimerMessengerModifier::trigger>(runtime->getPlayTime() + realMilliseconds, this);
 		}
 	}
 
@@ -740,11 +745,14 @@ Common::SharedPtr<Modifier> TimerMessengerModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(clone);
 }
 
-void TimerMessengerModifier::activate(Runtime *runtime) {
+void TimerMessengerModifier::trigger(Runtime *runtime) {
 	debug(3, "Timer %x '%s' triggered", getStaticGUID(), getName().c_str());
-	if (_looping)
-		_scheduledEvent = runtime->getScheduler().scheduleMethod<TimerMessengerModifier, &TimerMessengerModifier::activate>(runtime->getPlayTime() + _milliseconds, this);
-	else
+	if (_looping) {
+		uint32 realMilliseconds = _milliseconds;
+		if (realMilliseconds == 0)
+			realMilliseconds = 1;
+		_scheduledEvent = runtime->getScheduler().scheduleMethod<TimerMessengerModifier, &TimerMessengerModifier::trigger>(runtime->getPlayTime() + realMilliseconds, this);
+	} else
 		_scheduledEvent.reset();
 
 	_sendSpec.sendFromMessenger(runtime, this);
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 2d24fc49b04..d3263123fc9 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -384,7 +384,7 @@ public:
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
-	void activate(Runtime *runtime);
+	void trigger(Runtime *runtime);
 
 	Event _executeWhen;
 	Event _terminateWhen;
diff --git a/engines/mtropolis/module.mk b/engines/mtropolis/module.mk
index 96df62aa7f6..2a5892e4bbc 100644
--- a/engines/mtropolis/module.mk
+++ b/engines/mtropolis/module.mk
@@ -9,6 +9,7 @@ MODULE_OBJS = \
 	detection.o \
 	element_factory.o \
 	elements.o \
+	hacks.o \
 	metaengine.o \
 	miniscript.o \
 	modifiers.o \
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index 2ab4a8c77c1..15809c820e1 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -277,6 +277,8 @@ Common::Error MTropolisEngine::run() {
 		preferredHeight = 480;
 		preferredColorDepthMode = kColorDepthMode16Bit;
 
+		_runtime->getHacks().ignoreMismatchedProjectNameInObjectLookups = true;
+
 		_runtime->addVolume(0, "Installed", true);
 		_runtime->addVolume(1, "OBSIDIAN1", true);
 		_runtime->addVolume(2, "OBSIDIAN2", true);
@@ -314,11 +316,14 @@ Common::Error MTropolisEngine::run() {
 		desc->addPlugIn(PlugIns::createObsidian());
 
 		_runtime->queueProject(desc);
+
 	} else if (_gameDescription->gameID == GID_OBSIDIAN && _gameDescription->desc.platform == Common::kPlatformMacintosh) {
 		preferredWidth = 640;
 		preferredHeight = 480;
 		preferredColorDepthMode = kColorDepthMode16Bit;
 
+		_runtime->getHacks().ignoreMismatchedProjectNameInObjectLookups = true;
+
 		MacObsidianResources *resources = new MacObsidianResources();
 		Common::SharedPtr<ProjectResources> resPtr(resources);
 
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 7d72c6f2893..97fc7643565 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -884,7 +884,7 @@ MiniscriptInstructionOutcome MidiModifier::scriptSetNoteVelocity(MiniscriptThrea
 	return kMiniscriptInstructionOutcomeContinue;
 }
 
-ListVariableModifier::ListVariableModifier() : _list(new DynamicList()) {
+ListVariableModifier::ListVariableModifier() : _list(new DynamicList()), _preferredContentType(DynamicValueTypes::kInteger) {
 }
 
 bool ListVariableModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::ListVariableModifier &data) {
@@ -944,6 +944,8 @@ bool ListVariableModifier::load(const PlugInModifierLoaderContext &context, cons
 		}
 	}
 
+	_preferredContentType = expectedType;
+
 	return true;
 }
 
@@ -981,6 +983,15 @@ bool ListVariableModifier::readAttributeIndexed(MiniscriptThread *thread, Dynami
 	return Modifier::readAttributeIndexed(thread, result, attrib, index);
 }
 
+MiniscriptInstructionOutcome ListVariableModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
+	if (attrib == "count") {
+		DynamicValueWriteFuncHelper<ListVariableModifier, &ListVariableModifier::scriptSetCount>::create(this, writeProxy);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	return VariableModifier::writeRefAttribute(thread, writeProxy, attrib);
+}
+
 MiniscriptInstructionOutcome ListVariableModifier::writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib, const DynamicValue &index) {
 	if (attrib == "value") {
 		size_t realIndex = 0;
@@ -1055,6 +1066,33 @@ ListVariableModifier::ListVariableModifier(const ListVariableModifier &other) {
 		_list = other._list->clone();
 }
 
+MiniscriptInstructionOutcome ListVariableModifier::scriptSetCount(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger)) {
+		thread->error("Tried to set a list variable count to something other than an integer");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	if (asInteger < 0) {
+		thread->error("Tried to set a list variable count to a negative value");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	size_t newSize = asInteger;
+	if (newSize > _list->getSize()) {
+		if (_list->getSize() == 0) {
+			thread->error("Restoring an empty list by setting its count isn't implemented");
+			return kMiniscriptInstructionOutcomeFailed;
+		}
+
+		_list->expandToMinimumSize(newSize);
+	} else if (newSize < _list->getSize()) {
+		_list->truncateToSize(newSize);
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 Common::SharedPtr<Modifier> ListVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new ListVariableModifier(*this));
 }
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index d5a11dd0462..cc376fcf556 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -246,9 +246,9 @@ public:
 
 	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
 	bool readAttributeIndexed(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib, const DynamicValue &index) override;
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) override;
 	MiniscriptInstructionOutcome writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib, const DynamicValue &index) override;
 
-
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "List Variable Modifier"; }
 	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
@@ -272,9 +272,12 @@ private:
 	ListVariableModifier(const ListVariableModifier &other);
 	ListVariableModifier &operator=(const ListVariableModifier &other);
 
+	MiniscriptInstructionOutcome scriptSetCount(MiniscriptThread *thread, const DynamicValue &value);
+
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
 	Common::SharedPtr<DynamicList> _list;
+	DynamicValueTypes::DynamicValueType _preferredContentType;
 };
 
 class SysInfoModifier : public Modifier {
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index e692af2199d..a538652929b 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -527,6 +527,9 @@ bool DynamicListContainer<void>::setAtIndex(size_t index, const DynamicValue &dy
 	return true;
 }
 
+void DynamicListContainer<void>::truncateToSize(size_t sz) {
+}
+
 bool DynamicListContainer<void>::expandToMinimumSize(size_t sz) {
 	return false;
 }
@@ -589,6 +592,11 @@ bool DynamicListContainer<VarReference>::setAtIndex(size_t index, const DynamicV
 	return true;
 }
 
+void DynamicListContainer<VarReference>::truncateToSize(size_t sz) {
+	if (_array.size() > sz)
+		_array.resize(sz);
+}
+
 bool DynamicListContainer<VarReference>::expandToMinimumSize(size_t sz) {
 	if (_array.size() < sz) {
 		size_t prevSize = _array.size();
@@ -800,6 +808,13 @@ bool DynamicList::setAtIndex(size_t index, const DynamicValue &value) {
 	}
 }
 
+void DynamicList::truncateToSize(size_t sz) {
+	if (sz == 0)
+		clear();
+	else if (_container)
+		_container->truncateToSize(sz);
+}
+
 void DynamicList::expandToMinimumSize(size_t sz) {
 	if (_container)
 		_container->expandToMinimumSize(sz);
@@ -1669,12 +1684,40 @@ void MessengerSendSpec::resolveDestination(Runtime *runtime, Modifier *sender, C
 			if (!outStructuralDest.expired())
 				outStructuralDest = outStructuralDest.lock()->getParent()->getSelfReference().staticCast<Structural>();
 			break;
+		case kMessageDestNextElement:
+		case kMessageDestPrevElement: {
+				Common::WeakPtr<Structural> elementWeak;
+				Common::WeakPtr<Modifier> modifier;
+				resolveHierarchyStructuralDestination(runtime, sender, elementWeak, modifier, isElementFilter);
+
+				Common::SharedPtr<Structural> sibling;
+				Common::SharedPtr<Structural> element = elementWeak.lock();
+				if (element) {
+					Structural *parent = element->getParent();
+					if (parent) {
+						const Common::Array<Common::SharedPtr<Structural> > &siblings = parent->getChildren();
+						for (size_t i = 0; i < siblings.size(); i++) {
+							if (siblings[i] == element) {
+								if (destination == kMessageDestPrevElement) {
+									if (i != 0)
+										sibling = siblings[i - 1];
+								} else if (destination == kMessageDestNextElement) {
+									if (i != siblings.size() - 1)
+										sibling = siblings[i + 1];
+								}
+								break;
+							}
+						}
+					}
+				}
+
+				if (sibling)
+					outStructuralDest = sibling;
+			} break;
 		case kMessageDestChildren:
 		case kMessageDestSubsection:
 		case kMessageDestSourcesParent:
 		case kMessageDestBehavior:
-		case kMessageDestNextElement:
-		case kMessageDestPrevElement:
 		case kMessageDestBehaviorsParent:
 			warning("Not-yet-implemented message destination type");
 			break;
@@ -4558,6 +4601,15 @@ Audio::Mixer *Runtime::getAudioMixer() const {
 	return _mixer;
 }
 
+Hacks &Runtime::getHacks() {
+	return _hacks;
+}
+
+const Hacks &Runtime::getHacks() const {
+	return _hacks;
+}
+
+
 void Runtime::ensureMainWindowExists() {
 	// Maybe there's a better spot for this
 	if (_mainWindow.expired() && _project) {
@@ -5809,7 +5861,7 @@ MiniscriptInstructionOutcome VisualElement::writeRefAttribute(MiniscriptThread *
 		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetDirect>::create(this, writeProxy);
 		return kMiniscriptInstructionOutcomeContinue;
 	} else if (attrib == "position") {
-		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetPosition>::create(this, writeProxy);
+		DynamicValueWriteOrRefAttribFuncHelper<VisualElement, &VisualElement::scriptSetPosition, &VisualElement::scriptWriteRefPositionAttribute>::create(this, writeProxy);
 		return kMiniscriptInstructionOutcomeContinue;
 	} else if (attrib == "centerposition") {
 		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetCenterPosition>::create(this, writeProxy);
@@ -5908,6 +5960,32 @@ MiniscriptInstructionOutcome VisualElement::scriptSetPosition(MiniscriptThread *
 	return kMiniscriptInstructionOutcomeFailed;
 }
 
+MiniscriptInstructionOutcome VisualElement::scriptSetPositionX(MiniscriptThread *thread, const DynamicValue &dest) {
+	int32 asInteger = 0;
+	if (!dest.roundToInt(asInteger))
+		return kMiniscriptInstructionOutcomeFailed;
+
+	int32 xDelta = asInteger - _rect.left;
+
+	if (xDelta != 0)
+		offsetTranslate(xDelta, 0, false);
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome VisualElement::scriptSetPositionY(MiniscriptThread *thread, const DynamicValue &dest) {
+	int32 asInteger = 0;
+	if (!dest.roundToInt(asInteger))
+		return kMiniscriptInstructionOutcomeFailed;
+
+	int32 yDelta = asInteger - _rect.top;
+
+	if (yDelta != 0)
+		offsetTranslate(0, yDelta, false);
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 MiniscriptInstructionOutcome VisualElement::scriptSetCenterPosition(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kPoint) {
 		const Point16 &destPoint = value.getPoint();
@@ -5963,6 +6041,18 @@ MiniscriptInstructionOutcome VisualElement::scriptSetLayer(MiniscriptThread *thr
 	return kMiniscriptInstructionOutcomeContinue;
 }
 
+MiniscriptInstructionOutcome VisualElement::scriptWriteRefPositionAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
+	if (attrib == "x") {
+		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetPositionX>::create(this, writeProxy);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "y") {
+		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetPositionY>::create(this, writeProxy);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	return kMiniscriptInstructionOutcomeFailed;
+}
+
 void VisualElement::offsetTranslate(int32 xDelta, int32 yDelta, bool cachedOriginOnly) {
 	if (!cachedOriginOnly) {
 		_rect.left += xDelta;
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 5a62dbcdcbc..ab181738fb7 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -36,6 +36,7 @@
 #include "mtropolis/actions.h"
 #include "mtropolis/data.h"
 #include "mtropolis/debug.h"
+#include "mtropolis/hacks.h"
 #include "mtropolis/vthread.h"
 
 class OSystem;
@@ -511,6 +512,7 @@ public:
 	virtual ~DynamicListContainerBase();
 	virtual bool setAtIndex(size_t index, const DynamicValue &dynValue) = 0;
 	virtual bool getAtIndex(size_t index, DynamicValue &dynValue) const = 0;
+	virtual void truncateToSize(size_t sz) = 0;
 	virtual bool expandToMinimumSize(size_t sz) = 0;
 	virtual void setFrom(const DynamicListContainerBase &other) = 0; // Only supports setting same type!
 	virtual const void *getConstArrayPtr() const = 0;
@@ -567,6 +569,7 @@ class DynamicListContainer : public DynamicListContainerBase {
 public:
 	bool setAtIndex(size_t index, const DynamicValue &dynValue) override;
 	bool getAtIndex(size_t index, DynamicValue &dynValue) const override;
+	void truncateToSize(size_t sz) override;
 	bool expandToMinimumSize(size_t sz) override;
 	void setFrom(const DynamicListContainerBase &other) override;
 	const void *getConstArrayPtr() const override;
@@ -586,6 +589,7 @@ public:
 
 	bool setAtIndex(size_t index, const DynamicValue &dynValue) override;
 	bool getAtIndex(size_t index, DynamicValue &dynValue) const override;
+	void truncateToSize(size_t sz) override;
 	bool expandToMinimumSize(size_t sz) override;
 	void setFrom(const DynamicListContainerBase &other) override;
 	const void *getConstArrayPtr() const override;
@@ -603,6 +607,7 @@ class DynamicListContainer<VarReference> : public DynamicListContainerBase {
 public:
 	bool setAtIndex(size_t index, const DynamicValue &dynValue) override;
 	bool getAtIndex(size_t index, DynamicValue &dynValue) const override;
+	void truncateToSize(size_t sz) override;
 	bool expandToMinimumSize(size_t sz) override;
 	void setFrom(const DynamicListContainerBase &other) override;
 	const void *getConstArrayPtr() const override;
@@ -641,6 +646,12 @@ bool DynamicListContainer<T>::setAtIndex(size_t index, const DynamicValue &dynVa
 	return true;
 }
 
+template<class T>
+void DynamicListContainer<T>::truncateToSize(size_t sz) {
+	if (_array.size() > sz)
+		_array.resize(sz);
+}
+
 template<class T>
 bool DynamicListContainer<T>::expandToMinimumSize(size_t sz) {
 	_array.reserve(sz);
@@ -730,6 +741,7 @@ struct DynamicList {
 
 	bool getAtIndex(size_t index, DynamicValue &value) const;
 	bool setAtIndex(size_t index, const DynamicValue &value);
+	void truncateToSize(size_t sz);
 	void expandToMinimumSize(size_t sz);
 	size_t getSize() const;
 
@@ -937,6 +949,32 @@ private:
 	static DynamicValueWriteStringHelper _instance;
 };
 
+template<class TClass, MiniscriptInstructionOutcome (TClass::*TWriteMethod)(MiniscriptThread *thread, const DynamicValue &dest), MiniscriptInstructionOutcome (TClass::*TRefAttribMethod)(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib)>
+struct DynamicValueWriteOrRefAttribFuncHelper : public IDynamicValueWriteInterface {
+	MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr ptrOrOffset) const override {
+		return (static_cast<TClass *>(objectRef)->*TWriteMethod)(thread, dest);
+	}
+	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const override {
+		return (static_cast<TClass *>(objectRef)->*TRefAttribMethod)(thread, proxy, attrib);
+	}
+	MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override {
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	static void create(TClass *obj, DynamicValueWriteProxy &proxy) {
+		proxy.pod.ptrOrOffset = 0;
+		proxy.pod.objectRef = obj;
+		proxy.pod.ifc = &_instance;
+	}
+
+private:
+	static DynamicValueWriteOrRefAttribFuncHelper _instance;
+};
+
+template<class TClass, MiniscriptInstructionOutcome (TClass::*TWriteMethod)(MiniscriptThread *thread, const DynamicValue &dest), MiniscriptInstructionOutcome (TClass::*TRefAttribMethod)(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib)>
+DynamicValueWriteOrRefAttribFuncHelper<TClass, TWriteMethod, TRefAttribMethod> DynamicValueWriteOrRefAttribFuncHelper<TClass, TWriteMethod, TRefAttribMethod>::_instance;
+
+
 template<class TClass, MiniscriptInstructionOutcome (TClass::*TWriteMethod)(MiniscriptThread *thread, const DynamicValue &dest)>
 struct DynamicValueWriteFuncHelper : public IDynamicValueWriteInterface {
 	MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr ptrOrOffset) const override {
@@ -1462,6 +1500,9 @@ public:
 
 	Audio::Mixer *getAudioMixer() const;
 
+	Hacks &getHacks();
+	const Hacks &getHacks() const;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	void debugSetEnabled(bool enabled);
 	void debugBreak();
@@ -1633,6 +1674,8 @@ private:
 	uint32 _modifierOverrideCursorID;
 	bool _haveModifierOverrideCursor;
 
+	Hacks _hacks;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	Common::SharedPtr<Debugger> _debugger;
 #endif
@@ -2166,12 +2209,16 @@ protected:
 
 	MiniscriptInstructionOutcome scriptSetDirect(MiniscriptThread *thread, const DynamicValue &dest);
 	MiniscriptInstructionOutcome scriptSetPosition(MiniscriptThread *thread, const DynamicValue &dest);
+	MiniscriptInstructionOutcome scriptSetPositionX(MiniscriptThread *thread, const DynamicValue &dest);
+	MiniscriptInstructionOutcome scriptSetPositionY(MiniscriptThread *thread, const DynamicValue &dest);
 	MiniscriptInstructionOutcome scriptSetCenterPosition(MiniscriptThread *thread, const DynamicValue &dest);
 	MiniscriptInstructionOutcome scriptSetVisibility(MiniscriptThread *thread, const DynamicValue &result);
 	MiniscriptInstructionOutcome scriptSetWidth(MiniscriptThread *thread, const DynamicValue &dest);
 	MiniscriptInstructionOutcome scriptSetHeight(MiniscriptThread *thread, const DynamicValue &dest);
 	MiniscriptInstructionOutcome scriptSetLayer(MiniscriptThread *thread, const DynamicValue &dest);
 
+	MiniscriptInstructionOutcome scriptWriteRefPositionAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
+
 	void offsetTranslate(int32 xDelta, int32 yDelta, bool cachedOriginOnly);
 
 	Point16 getCenterPosition() const;


Commit: 9e010ccfd9b613a803f943b2bbe81fd1d97a0ffb
    https://github.com/scummvm/scummvm/commit/9e010ccfd9b613a803f943b2bbe81fd1d97a0ffb
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix buggy mToon decompression

Changed paths:
    engines/mtropolis/assets.cpp


diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index 2fdfb5c104a..5d44487f913 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -149,7 +149,7 @@ void CachedMToon::decompressNonTemporalFrames(const Common::Array<uint8> &data)
 }
 
 template<class TFrame, class TNumber, uint32 TLiteralMask, uint32 TTransparentRowSkipMask>
-static bool decompressMToonRLE(const TFrame &frame, Graphics::Surface &surface) {
+static bool decompressMToonRLE(const TFrame &frame, Graphics::Surface &surface, bool isBottomUp) {
 	assert(sizeof(TNumber) == surface.format.bytesPerPixel);
 
 	const Common::Array<TNumber> &coefsArray = frame.data;
@@ -168,7 +168,7 @@ static bool decompressMToonRLE(const TFrame &frame, Graphics::Surface &surface)
 	if (w != frame.width || h != frame.height)
 		return false;
 
-	TNumber *rowData = static_cast<TNumber *>(surface.getBasePtr(0, 0));
+	TNumber *rowData = static_cast<TNumber *>(surface.getBasePtr(0, isBottomUp ? (h - 1) : 0));
 
 	for (;;) {
 		if (size == 0)
@@ -191,7 +191,7 @@ static bool decompressMToonRLE(const TFrame &frame, Graphics::Surface &surface)
 				y += (transparentCountCode - TTransparentRowSkipMask);
 				x = 0;
 				if (y < h) {
-					rowData = static_cast<TNumber *>(surface.getBasePtr(0, y));
+					rowData = static_cast<TNumber *>(surface.getBasePtr(0, isBottomUp ? (h - 1 - y) : y));
 					continue;
 				} else {
 					break;
@@ -227,8 +227,9 @@ static bool decompressMToonRLE(const TFrame &frame, Graphics::Surface &surface)
 
 		if (x == w) {
 			y++;
+			x = 0;
 			if (y < h) {
-				rowData = static_cast<TNumber *>(surface.getBasePtr(0, y));
+				rowData = static_cast<TNumber *>(surface.getBasePtr(0, isBottomUp ? (h - 1 - y) : y));
 				continue;
 			} else {
 				break;
@@ -247,13 +248,15 @@ void CachedMToon::decompressRLEFrameToImage(size_t frameIndex, Graphics::Surface
 	int32 originX = frameDef.rect.left;
 	int32 originY = frameDef.rect.top;
 
+	bool isBottomUp = (_metadata->imageFormat == MToonMetadata::kImageFormatWindows);
+
 	bool decompressedOK = false;
 	if (_rleOptimizedFormat.bytesPerPixel == 4) {
-		decompressedOK = decompressMToonRLE<Rle32Frame, uint32, 0x80000000u, 0x80000000u>(_dataRLE32[frameIndex], surface);
+		decompressedOK = decompressMToonRLE<Rle32Frame, uint32, 0x80000000u, 0x80000000u>(_dataRLE32[frameIndex], surface, isBottomUp);
 	} else if (_rleOptimizedFormat.bytesPerPixel == 2) {
-		decompressedOK = decompressMToonRLE<Rle16Frame, uint16, 0x8000u, 0x8000u>(_dataRLE16[frameIndex], surface);
+		decompressedOK = decompressMToonRLE<Rle16Frame, uint16, 0x8000u, 0x8000u>(_dataRLE16[frameIndex], surface, isBottomUp);
 	} else if (_rleOptimizedFormat.bytesPerPixel == 1) {
-		decompressedOK = decompressMToonRLE<Rle8Frame, uint8, 0x80u, 0u>(_dataRLE8[frameIndex], surface);
+		decompressedOK = decompressMToonRLE<Rle8Frame, uint8, 0x80u, 0u>(_dataRLE8[frameIndex], surface, isBottomUp);
 	} else
 		error("Unknown mToon encoding");
 
@@ -317,8 +320,13 @@ void CachedMToon::loadRLEFrames(const Common::Array<uint8> &data) {
 				memcpy(&_dataRLE16[i].data[0], &data[baseOffset + 20], numDWords * 2);
 
 				uint16 *i16 = &_dataRLE16[i].data[0];
-				for (size_t swapIndex = 0; swapIndex < numDWords; swapIndex++)
-					i16[swapIndex] = FROM_BE_16(i16[swapIndex]);
+				if (_metadata->imageFormat == MToonMetadata::kImageFormatWindows) {
+					for (size_t swapIndex = 0; swapIndex < numDWords; swapIndex++)
+						i16[swapIndex] = FROM_LE_16(i16[swapIndex]);
+				} else if (_metadata->imageFormat == MToonMetadata::kImageFormatMac) {
+					for (size_t swapIndex = 0; swapIndex < numDWords; swapIndex++)
+						i16[swapIndex] = FROM_BE_16(i16[swapIndex]);
+				}
 			} else
 				error("Unknown mToon encoding");
 		}
@@ -558,13 +566,15 @@ void CachedMToon::getOrRenderFrame(uint32 prevFrame, uint32 targetFrame, Common:
 			surface->create(_metadata->rect.getWidth(), _metadata->rect.getHeight(), _rleOptimizedFormat);
 		}
 
+		bool isBottomUp = (_metadata->imageFormat == MToonMetadata::kImageFormatWindows);
+
 		for (size_t i = firstFrameToRender; i <= targetFrame; i++) {
 			if (_rleOptimizedFormat.bytesPerPixel == 1)
-				decompressMToonRLE<Rle8Frame, uint8, 0x80u, 0>(_dataRLE8[i], *surface);
+				decompressMToonRLE<Rle8Frame, uint8, 0x80u, 0>(_dataRLE8[i], *surface, isBottomUp);
 			else if (_rleOptimizedFormat.bytesPerPixel == 2)
-				decompressMToonRLE<Rle16Frame, uint16, 0x8000u, 0x8000u>(_dataRLE16[i], *surface);
+				decompressMToonRLE<Rle16Frame, uint16, 0x8000u, 0x8000u>(_dataRLE16[i], *surface, isBottomUp);
 			else if (_rleOptimizedFormat.bytesPerPixel == 4)
-				decompressMToonRLE<Rle32Frame, uint32, 0x80000000u, 0x80000000u>(_dataRLE32[i], *surface);
+				decompressMToonRLE<Rle32Frame, uint32, 0x80000000u, 0x80000000u>(_dataRLE32[i], *surface, isBottomUp);
 		}
 	}
 }


Commit: 087d97f0e8b4d10c9617643a81bae74bf81c1508
    https://github.com/scummvm/scummvm/commit/087d97f0e8b4d10c9617643a81bae74bf81c1508
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Flag ImageElement as Done

Changed paths:
    engines/mtropolis/elements.h


diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index 5293e879df7..c1ce2043a0f 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -134,7 +134,7 @@ public:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Image Element"; }
-	SupportStatus debugGetSupportStatus() const override { return kSupportStatusPartial; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:


Commit: c18b00f5be13586eeab5bb63741725afed1fa16f
    https://github.com/scummvm/scummvm/commit/c18b00f5be13586eeab5bb63741725afed1fa16f
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix mToon decode bug

Changed paths:
    engines/mtropolis/assets.cpp


diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index 5d44487f913..5a7e4d1aebd 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -280,9 +280,9 @@ void CachedMToon::loadRLEFrames(const Common::Array<uint8> &data) {
 
 		RleFrame *rleFrame = nullptr;
 		if (bpp == 8)
-			rleFrame = &_dataRLE8[0];
+			rleFrame = &_dataRLE8[i];
 		else if (bpp == 16)
-			rleFrame = &_dataRLE16[0];
+			rleFrame = &_dataRLE16[i];
 
 		size_t baseOffset = frameDef.dataOffset;
 


Commit: 790ed24635212164efd64489dc089d732e8a1200
    https://github.com/scummvm/scummvm/commit/790ed24635212164efd64489dc089d732e8a1200
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix save corruption again

Changed paths:
    engines/mtropolis/debug.cpp
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h


diff --git a/engines/mtropolis/debug.cpp b/engines/mtropolis/debug.cpp
index 61b90054d34..ea7a2359af4 100644
--- a/engines/mtropolis/debug.cpp
+++ b/engines/mtropolis/debug.cpp
@@ -1031,15 +1031,16 @@ void DebugInspectorWindow::declareDynamic(const char *name, const Common::String
 		_labeledRow.push_back(row);
 	}
 	_labeledRow[_declLabeledRow].text = data;
+	_declLabeledRow++;
 }
 
 void DebugInspectorWindow::declareLoose(const Common::String &data) {
-	if (_declLabeledRow == _labeledRow.size()) {
+	if (_declUnlabeledRow == _unlabeledRow.size()) {
 		InspectorUnlabeledRow row;
 		row.str = data;
 		_unlabeledRow.push_back(row);
 	} else
-		_unlabeledRow[_declLabeledRow].str = data;
+		_unlabeledRow[_declUnlabeledRow].str = data;
 
 	_declUnlabeledRow++;
 }
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index ddb142a3784..b6f630f8c98 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -747,6 +747,24 @@ void SoundElement::playMedia(Runtime *runtime, Project *project) {
 	}
 }
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+void SoundElement::debugInspect(IDebugInspectionReport *report) const {
+	NonVisualElement::debugInspect(report);
+
+	report->declareDynamic("leftVol", Common::String::format("%i", _leftVolume));
+	report->declareDynamic("rightVol", Common::String::format("%i", _rightVolume));
+	report->declareDynamic("balance", Common::String::format("%i", _balance));
+	report->declareDynamic("asset", Common::String::format("%i", _assetID));
+
+	AudioMetadata *metadata = _metadata.get();
+	report->declareDynamic("duration", metadata ? Common::String::format("%i", metadata->durationMSec) : Common::String("Unknown"));
+	report->declareDynamic("finishTime", Common::String::format("%i", static_cast<int>(_finishTime)));
+	report->declareDynamic("shouldPlayIfNotPaused", _shouldPlayIfNotPaused ? "true" : "false");
+	report->declareDynamic("paused", _paused ? "true" : "false");
+	report->declareDynamic("needsReset", _needsReset ? "true" : "false");
+}
+#endif
+
 MiniscriptInstructionOutcome SoundElement::scriptSetLoop(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() != DynamicValueTypes::kBoolean)
 		return kMiniscriptInstructionOutcomeFailed;
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index c1ce2043a0f..d188ae406fb 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -241,6 +241,7 @@ public:
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Sound Element"; }
 	SupportStatus debugGetSupportStatus() const override { return kSupportStatusPartial; }
+	void debugInspect(IDebugInspectionReport *report) const override;
 #endif
 
 private:
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index ea305ee2f7b..cd1f53d7848 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -743,6 +743,9 @@ MiniscriptInstructionOutcome UnorderedCompareInstruction::execute(MiniscriptThre
 			case DynamicValueTypes::kBoolean:
 				isEqual = (rs.getBool() == lsDest.getBool());
 				break;
+			default:
+				isEqual = (lsDest.getBool() == false);
+				break;
 			}
 		} break;
 	case DynamicValueTypes::kFloat: {
@@ -781,6 +784,20 @@ MiniscriptInstructionOutcome UnorderedCompareInstruction::execute(MiniscriptThre
 				break;
 			}
 		} break;
+	case DynamicValueTypes::kLabel: {
+			// Really not sure how this works but there are buggy scripts in Obsidian which
+			// were probably written as "if loop = false then ..." except Miniscript resolved
+			// "loop" as a sound marker (!) because a sound marker with that name exists instead
+			// of resolving it as equivalent to element.loop as it usually would.
+			// Strict equality checks prevent the "GEN_Streaming_Update on ALC" script from
+			// working, which prevents the bqtstreaming and bstreaming flags from being cleared,
+			// causing, among other things, the player to get stuck after the Forest->Bureau
+			// transition because the stuck streaming flags are blocking the VO.
+			if (rs.getType() == DynamicValueTypes::kBoolean)
+				isEqual = !rs.getBool();
+			else
+				isEqual = (lsDest == rs);
+		} break;
 	default:
 		isEqual = (lsDest == rs);
 		break;
@@ -1826,7 +1843,7 @@ VThreadState MiniscriptThread::resume(const ResumeTaskData &taskData) {
 	if (instrsArray.size() == 0)
 		return kVThreadReturn;
 
-	if (_modifier->getStaticGUID() == 0x48890) {
+	if (_modifier->getStaticGUID() == 0x31ae0e) {
 		int n = 0;
 	}
 
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 8b19d8bc34d..b5dc7246186 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -746,6 +746,9 @@ Common::SharedPtr<Modifier> TimerMessengerModifier::shallowClone() const {
 }
 
 void TimerMessengerModifier::trigger(Runtime *runtime) {
+	if (getStaticGUID() == 0xd9550) {
+		int n = 0;
+	}
 	debug(3, "Timer %x '%s' triggered", getStaticGUID(), getName().c_str());
 	if (_looping) {
 		uint32 realMilliseconds = _milliseconds;
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 97fc7643565..473676958aa 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -1105,41 +1105,58 @@ void ListVariableModifier::SaveLoad::commitLoad() const {
 }
 
 void ListVariableModifier::SaveLoad::saveInternal(Common::WriteStream *stream) const {
-	stream->writeUint32BE(_list->getType());
-	stream->writeUint32BE(_list->getSize());
+	recursiveWriteList(_list.get(), stream);
+}
 
-	size_t listSize = _list->getSize();
+bool ListVariableModifier::SaveLoad::loadInternal(Common::ReadStream *stream) {
+	Common::SharedPtr<DynamicList> list = recursiveReadList(stream);
+	if (list) {
+		_list = list;
+		return true;
+	} else {
+		return false;
+	}
+}
+
+void ListVariableModifier::SaveLoad::recursiveWriteList(DynamicList *list, Common::WriteStream *stream) {
+	stream->writeUint32BE(list->getType());
+	stream->writeUint32BE(list->getSize());
+
+	size_t listSize = list->getSize();
 	for (size_t i = 0; i < listSize; i++) {
-		switch (_list->getType()) {
+		switch (list->getType()) {
 		case DynamicValueTypes::kInteger:
-			stream->writeSint32BE(_list->getInt()[i]);
+			stream->writeSint32BE(list->getInt()[i]);
 			break;
 		case DynamicValueTypes::kPoint: {
-				const Point16 &pt = _list->getPoint()[i];
+				const Point16 &pt = list->getPoint()[i];
 				stream->writeSint16BE(pt.x);
 				stream->writeSint16BE(pt.y);
 			}
 			break;
 		case DynamicValueTypes::kIntegerRange: {
-				const IntRange &range = _list->getIntRange()[i];
+				const IntRange &range = list->getIntRange()[i];
 				stream->writeSint32BE(range.min);
 				stream->writeSint32BE(range.max);
 			} break;
 		case DynamicValueTypes::kFloat:
-			stream->writeDoubleBE(_list->getFloat()[i]);
+			stream->writeDoubleBE(list->getFloat()[i]);
 			break;
 		case DynamicValueTypes::kString: {
-				const Common::String &str = _list->getString()[i];
+				const Common::String &str = list->getString()[i];
 				stream->writeUint32BE(str.size());
 				stream->writeString(str);
 			} break;
 		case DynamicValueTypes::kVector: {
-				const AngleMagVector &vec = _list->getVector()[i];
+				const AngleMagVector &vec = list->getVector()[i];
 				stream->writeDoubleBE(vec.angleDegrees);
 				stream->writeDoubleBE(vec.magnitude);
 			} break;
 		case DynamicValueTypes::kBoolean:
-			stream->writeByte(_list->getBool()[i] ? 1 : 0);
+			stream->writeByte(list->getBool()[i] ? 1 : 0);
+			break;
+		case DynamicValueTypes::kList:
+			recursiveWriteList(list->getList()[i].get(), stream);
 			break;
 		default:
 			error("Can't figure out how to write a saved variable");
@@ -1148,14 +1165,15 @@ void ListVariableModifier::SaveLoad::saveInternal(Common::WriteStream *stream) c
 	}
 }
 
-bool ListVariableModifier::SaveLoad::loadInternal(Common::ReadStream *stream) {
-	_list.reset(new DynamicList());
+Common::SharedPtr<DynamicList> ListVariableModifier::SaveLoad::recursiveReadList(Common::ReadStream *stream) {
+	Common::SharedPtr<DynamicList> list;
+	list.reset(new DynamicList());
 
 	uint32 typeCode = stream->readUint32BE();
 	uint32 size = stream->readUint32BE();
 
 	if (stream->err())
-		return false;
+		return nullptr;
 
 	for (size_t i = 0; i < size; i++) {
 		DynamicValue val;
@@ -1183,9 +1201,9 @@ bool ListVariableModifier::SaveLoad::loadInternal(Common::ReadStream *stream) {
 				val.setFloat(f);
 			} break;
 		case DynamicValueTypes::kString: {
-			uint32 strLen = stream->readUint32BE();
-			if (stream->err())
-				return false;
+				uint32 strLen = stream->readUint32BE();
+				if (stream->err())
+					return nullptr;
 
 				Common::String str;
 				if (strLen > 0) {
@@ -1206,18 +1224,24 @@ bool ListVariableModifier::SaveLoad::loadInternal(Common::ReadStream *stream) {
 				byte b = stream->readByte();
 				val.setBool(b != 0);
 			} break;
+		case DynamicValueTypes::kList: {
+				Common::SharedPtr<DynamicList> childList = recursiveReadList(stream);
+				if (!childList)
+					return nullptr;
+				val.setList(childList);
+			} break;
 		default:
 			error("Can't figure out how to write a saved variable");
 			break;
 		}
 
 		if (stream->err())
-			return false;
+			return nullptr;
 
-		_list->setAtIndex(i, val);
+		list->setAtIndex(i, val);
 	}
 
-	return true;
+	return list;
 }
 
 bool SysInfoModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::SysInfoModifier &data) {
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index cc376fcf556..57889f91fc5 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -265,6 +265,9 @@ private:
 		void saveInternal(Common::WriteStream *stream) const override;
 		bool loadInternal(Common::ReadStream *stream) override;
 
+		static void recursiveWriteList(DynamicList *list, Common::WriteStream *stream);
+		static Common::SharedPtr<DynamicList> recursiveReadList(Common::ReadStream *stream);
+
 		ListVariableModifier *_modifier;
 		Common::SharedPtr<DynamicList> _list;
 	};


Commit: 62accd5196108a55b986f3ba7828560975a24e5b
    https://github.com/scummvm/scummvm/commit/62accd5196108a55b986f3ba7828560975a24e5b
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Load data for Obsidian word games

Changed paths:
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/miniscript.h
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/plugin/obsidian.cpp
    engines/mtropolis/plugin/obsidian.h
    engines/mtropolis/plugin/obsidian_data.cpp
    engines/mtropolis/plugin/obsidian_data.h
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/plugins.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index cd1f53d7848..201c6c869b5 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -856,6 +856,36 @@ MiniscriptInstructionOutcome Or::execute(MiniscriptThread *thread) const {
 	return kMiniscriptInstructionOutcomeContinue;
 }
 
+MiniscriptInstructionOutcome Neg::execute(MiniscriptThread *thread) const {
+	if (thread->getStackSize() < 1) {
+		thread->error("Stack underflow");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0, false);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	DynamicValue &value = thread->getStackValueFromTop(0).value;
+	switch (value.getType()) {
+	case DynamicValueTypes::kFloat:
+		value.setFloat(-value.getFloat());
+		break;
+	case DynamicValueTypes::kInteger: {
+		int32 i = value.getInt();
+		if (i == (0 - 1 -0x7fffffff))
+			value.setFloat(-static_cast<double>(i));
+		else
+			value.setInt(-i);
+		} break;
+	default:
+		thread->error("Couldn't negate a value of a non-numeric type");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 MiniscriptInstructionOutcome Not::execute(MiniscriptThread *thread) const {
 	if (thread->getStackSize() < 1) {
 		thread->error("Stack underflow");
diff --git a/engines/mtropolis/miniscript.h b/engines/mtropolis/miniscript.h
index dd51a7bc730..159b2a97cdb 100644
--- a/engines/mtropolis/miniscript.h
+++ b/engines/mtropolis/miniscript.h
@@ -159,7 +159,9 @@ namespace MiniscriptInstructions {
 		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
 	};
 
-	class Neg : public UnimplementedInstruction {
+	class Neg : public MiniscriptInstruction {
+	private:
+		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
 	};
 
 	class Not : public MiniscriptInstruction {
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index b5dc7246186..5ad82cfb556 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -1315,7 +1315,13 @@ bool IntegerVariableModifier::varSetValue(MiniscriptThread *thread, const Dynami
 		_value = static_cast<int32>(floor(value.getFloat() + 0.5));
 	else if (value.getType() == DynamicValueTypes::kInteger)
 		_value = value.getInt();
-	else
+	else if (value.getType() == DynamicValueTypes::kString) {
+		// Should this scan %lf to a double and round it instead?
+		int i;
+		if (!sscanf(value.getString().c_str(), "%i", &i))
+			return false;
+		_value = i;
+	} else
 		return false;
 
 	return true;
@@ -1385,6 +1391,30 @@ void IntegerRangeVariableModifier::varGetValue(MiniscriptThread *thread, Dynamic
 	dest.setIntRange(_range);
 }
 
+bool IntegerRangeVariableModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "start") {
+		result.setInt(_range.min);
+		return true;
+	}
+	if (attrib == "end") {
+		result.setInt(_range.max);
+		return true;
+	}
+	return Modifier::readAttribute(thread, result, attrib);
+}
+
+MiniscriptInstructionOutcome IntegerRangeVariableModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
+	if (attrib == "start") {
+		DynamicValueWriteIntegerHelper<int32>::create(&_range.min, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+	if (attrib == "end") {
+		DynamicValueWriteIntegerHelper<int32>::create(&_range.max, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+	return Modifier::writeRefAttribute(thread, result, attrib);
+}
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 void IntegerRangeVariableModifier::debugInspect(IDebugInspectionReport *report) const {
 	VariableModifier::debugInspect(report);
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index d3263123fc9..dd00a49fe7c 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -473,6 +473,7 @@ public:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Keyboard Messenger Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
@@ -738,6 +739,9 @@ public:
 	bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override;
 	void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override;
 
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib);
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Integer Range Variable Modifier"; }
 	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index 15809c820e1..2d2903bd1e4 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -28,6 +28,7 @@
 
 #include "mtropolis/plugins.h"
 #include "mtropolis/plugin/standard.h"
+#include "mtropolis/plugin/obsidian.h"
 
 #include "common/config-manager.h"
 #include "common/debug.h"
@@ -49,6 +50,49 @@
 
 namespace MTropolis {
 
+static Common::SharedPtr<Obsidian::WordGameData> loadWinObsidianWordGameData() {
+	Common::File f;
+	if (!f.open("RSGKit.r95")) {
+		error("Couldn't open word game data file");
+		return nullptr;
+	}
+
+	Common::SharedPtr<Obsidian::WordGameData> wgData(new Obsidian::WordGameData());
+
+	Obsidian::WordGameLoadBucket buckets[] = {
+		{0, 0},             // 0 letters
+		{0x63D54, 0x63D5C}, // 1 letter
+		{0x63BF8, 0x63CA4}, // 2 letter
+		{0x627D8, 0x631E8}, // 3 letter
+		{0x5C2C8, 0x60628}, // 4 letter
+		{0x52F4C, 0x5919C}, // 5 letter
+		{0x47A64, 0x4F2FC}, // 6 letter
+		{0x3BC98, 0x43B20}, // 7 letter
+		{0x2DA78, 0x38410}, // 8 letter
+		{0x218F8, 0x2AA18}, // 9 letter
+		{0x19D78, 0x1FA18}, // 10 letter
+		{0x15738, 0x18BE8}, // 11 letter
+		{0x128A8, 0x14DE8}, // 12 letter
+		{0x1129C, 0x1243C}, // 13 letter
+		{0x10974, 0x110C4}, // 14 letter
+		{0x105EC, 0x108BC}, // 15 letter
+		{0x10454, 0x105A8}, // 16 letter
+		{0x103A8, 0x10434}, // 17 letter
+		{0x10348, 0x10398}, // 18 letter
+		{0, 0},				// 19 letter
+		{0x10328, 0x10340}, // 20 letter
+		{0x102EC, 0x1031C}, // 21 letter
+		{0x102D0, 0x102E8},	// 22 letter
+	};
+
+	if (!wgData->load(&f, buckets, 23, 4, true)) {
+		error("Failed to load word game data file");
+		return nullptr;
+	}
+
+	return wgData;
+}
+
 static bool loadCursorsFromPE(CursorGraphicCollection &cursorGraphics, Common::SeekableReadStream *stream) {
 	Common::SharedPtr<Common::WinResources> winRes(Common::WinResources::createFromEXE(stream));
 	if (!winRes) {
@@ -131,6 +175,7 @@ struct MacObsidianResources : public ProjectResources {
 	void setup();
 	Common::SeekableReadStream *getSegmentStream(int index) const;
 	const Common::SharedPtr<CursorGraphicCollection> &getCursorGraphics() const;
+	const Common::SharedPtr<Obsidian::WordGameData> &getWordGameData() const;
 
 private:
 	Common::MacResManager _installerResMan;
@@ -141,6 +186,7 @@ private:
 	Common::SeekableReadStream *_segmentStreams[6];
 
 	Common::SharedPtr<CursorGraphicCollection> _cursorGraphics;
+	Common::SharedPtr<Obsidian::WordGameData> _wordGameData;
 };
 
 MacObsidianResources::MacObsidianResources() : _installerArchive(nullptr), _installerDataForkStream(nullptr) {
@@ -197,6 +243,49 @@ void MacObsidianResources::setup() {
 			error("Failed to read cursor resources from file '%s'", fileName);
 	}
 
+	debug(1, "Loading word games...");
+
+	{
+		Common::ArchiveMemberPtr rsgKit = _installerArchive->getMember(Common::Path("RSGKit.rPP"));
+		if (!rsgKit)
+			error("Couldn't find word game file in installer archive");
+
+		_wordGameData.reset(new Obsidian::WordGameData());
+
+		Common::SharedPtr<Common::SeekableReadStream> stream(rsgKit->createReadStream());
+		if (!stream)
+			error("Failed to open word game file");
+
+		Obsidian::WordGameLoadBucket buckets[] = {
+			{0, 0},				// 0 letters
+			{0xD7C8, 0xD7CC},	// 1 letter
+			{0xD7CC, 0xD84D},	// 2 letter
+			{0xD84D, 0xE25D},	// 3 letter
+			{0x1008C, 0x12AA8},	// 4 letter
+			{0x14C58, 0x19614},	// 5 letter
+			{0x1C73C, 0x230C1},	// 6 letter
+			{0x26D10, 0x2EB98},	// 7 letter
+			{0x32ADC, 0x3AA0E},	// 8 letter
+			{0x3E298, 0x45B88},	// 9 letter
+			{0x48BE8, 0x4E0D0},	// 10 letter
+			{0x4FFB0, 0x53460},	// 11 letter
+			{0x545F0, 0x56434},	// 12 letter
+			{0x56D84, 0x57CF0}, // 13 letter
+			{0x58158, 0x58833}, // 14 letter
+			{0x58A08, 0x58CD8}, // 15 letter
+			{0x58D8C, 0x58EAD}, // 16 letter
+			{0x58EF4, 0x58F72}, // 17 letter
+			{0x58F90, 0x58FDC},	// 18 letter
+			{0, 0},				// 19 letter
+			{0x58FEC, 0x59001},	// 20 letter
+			{0x59008, 0x59034},	// 21 letter
+			{0x5903C, 0x59053},	// 22 letter
+		};
+
+		if (!_wordGameData->load(stream.get(), buckets, 23, 1, false))
+			error("Failed to load word game data");
+	}
+
 	debug(1, "Finished unpacking installer resources");
 }
 
@@ -204,10 +293,14 @@ Common::SeekableReadStream *MacObsidianResources::getSegmentStream(int index) co
 	return _segmentStreams[index];
 }
 
-const Common::SharedPtr<CursorGraphicCollection>& MacObsidianResources::getCursorGraphics() const {
+const Common::SharedPtr<CursorGraphicCollection> &MacObsidianResources::getCursorGraphics() const {
 	return _cursorGraphics;
 }
 
+const Common::SharedPtr<Obsidian::WordGameData>& MacObsidianResources::getWordGameData() const {
+	return _wordGameData;
+}
+
 MacObsidianResources::~MacObsidianResources() {
 	for (int i = 0; i < 6; i++)
 		delete _segmentStreams[i];
@@ -307,13 +400,15 @@ Common::Error MTropolisEngine::run() {
 			}
 		}
 
+		Common::SharedPtr<Obsidian::WordGameData> wgData = loadWinObsidianWordGameData();
+
 		desc->setCursorGraphics(cursors);
 
 		Common::SharedPtr<MTropolis::PlugIn> standardPlugIn = PlugIns::createStandard();
 		static_cast<Standard::StandardPlugIn *>(standardPlugIn.get())->getHacks().allowGarbledListModData = true;
 		desc->addPlugIn(standardPlugIn);
 
-		desc->addPlugIn(PlugIns::createObsidian());
+		desc->addPlugIn(PlugIns::createObsidian(wgData));
 
 		_runtime->queueProject(desc);
 
@@ -345,7 +440,7 @@ Common::Error MTropolisEngine::run() {
 		static_cast<Standard::StandardPlugIn *>(standardPlugIn.get())->getHacks().allowGarbledListModData = true;
 		desc->addPlugIn(standardPlugIn);
 
-		desc->addPlugIn(PlugIns::createObsidian());
+		desc->addPlugIn(PlugIns::createObsidian(resources->getWordGameData()));
 
 		desc->setResources(resPtr);
 		desc->setCursorGraphics(resources->getCursorGraphics());
diff --git a/engines/mtropolis/plugin/obsidian.cpp b/engines/mtropolis/plugin/obsidian.cpp
index b157ee2f1df..dd911fe56be 100644
--- a/engines/mtropolis/plugin/obsidian.cpp
+++ b/engines/mtropolis/plugin/obsidian.cpp
@@ -116,26 +116,281 @@ MiniscriptInstructionOutcome TextWorkModifier::writeRefAttribute(MiniscriptThrea
 	return Modifier::writeRefAttribute(thread, result, attrib);
 }
 
-
 Common::SharedPtr<Modifier> TextWorkModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new TextWorkModifier(*this));
 }
 
-ObsidianPlugIn::ObsidianPlugIn() : _movementModifierFactory(this), _rectShiftModifierFactory(this), _textWorkModifierFactory(this) {
+DictionaryModifier::DictionaryModifier() : _plugIn(nullptr) {
+}
+
+bool DictionaryModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::DictionaryModifier &data) {
+	if (data.str.type != Data::PlugInTypeTaggedValue::kString)
+		return false;
+
+	_str = data.str.str;
+
+	if (data.index.type != Data::PlugInTypeTaggedValue::kInteger)
+		return false;
+
+	_index = data.index.value.asInt;
+	_isIndexResolved = true;
+	_plugIn = static_cast<ObsidianPlugIn *>(context.plugIn);
+
+	return true;
+}
+
+
+bool DictionaryModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "index") {
+		resolveStringIndex();
+		result.setInt(_index);
+		return true;
+	}
+	if (attrib == "string") {
+		result.setString(_str);
+		return true;
+	}
+
+	return Modifier::readAttribute(thread, result, attrib);
+}
+
+MiniscriptInstructionOutcome DictionaryModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
+	if (attrib == "index") {
+		DynamicValueWriteFuncHelper<DictionaryModifier, &DictionaryModifier::scriptSetIndex>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+	if (attrib == "string") {
+		DynamicValueWriteFuncHelper<DictionaryModifier, &DictionaryModifier::scriptSetString>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	return Modifier::writeRefAttribute(thread, result, attrib);
+}
+
+void DictionaryModifier::resolveStringIndex() {
+	if (_isIndexResolved)
+		return;
+
+	_index = 0;
+	_isIndexResolved = true;
+
+	const Common::Array<WordGameData::WordBucket> &wordBuckets = _plugIn->getWordGameData()->getWordBuckets();
+
+	size_t strLength = _str.size();
+	if (strLength >= wordBuckets.size())
+		return;
+
+	const WordGameData::WordBucket &bucket = wordBuckets[strLength];
+
+	size_t lowOffsetInclusive = 0;
+	size_t highOffsetExclusive = bucket.wordIndexes.size();
+
+	const char *strChars = _str.c_str();
+
+	// Binary search
+	while (lowOffsetInclusive != highOffsetExclusive) {
+		const size_t midOffset = (lowOffsetInclusive + highOffsetExclusive) / 2;
+		const char *chars = &bucket.chars[bucket.spacing * midOffset];
+
+		bool isMidGreater = false;
+		bool isMidLess = false;
+		for (size_t i = 0; i < strLength; i++) {
+			if (chars[i] > strChars[i]) {
+				isMidGreater = true;
+				break;
+			} else if (chars[i] < strChars[i]) {
+				isMidLess = true;
+				break;
+			}
+		}
+
+		if (isMidLess)
+			lowOffsetInclusive = midOffset + 1;
+		else if (isMidGreater)
+			highOffsetExclusive = midOffset;
+		else {
+			_index = bucket.wordIndexes[midOffset] + 1;
+			break;
+		}
+	}
+}
+
+MiniscriptInstructionOutcome DictionaryModifier::scriptSetString(MiniscriptThread *thread, const DynamicValue &value) {
+	if (value.getType() != DynamicValueTypes::kString) {
+		thread->error("Tried to set dictionary string to something that wasn't a string");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	if (_str != value.getString()) {
+		_str = value.getString();
+		_isIndexResolved = false;
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome DictionaryModifier::scriptSetIndex(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger)) {
+		thread->error("Tried to set dictionary index to something that wasn't a number");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	_index = asInteger;
+	if (_index < 1)
+		_str.clear();
+	else {
+		const size_t indexAdjusted = _index - 1;
+		const Common::Array<WordGameData::SortedWord> &sortedWords = _plugIn->getWordGameData()->getSortedWords();
+		if (indexAdjusted >= sortedWords.size())
+			_str.clear();
+		else
+			_str = Common::String(sortedWords[indexAdjusted].chars, sortedWords[indexAdjusted].length);
+	}
+
+	_isIndexResolved = true;
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+Common::SharedPtr<Modifier> DictionaryModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new DictionaryModifier(*this));
+}
+
+WordMixerModifier::WordMixerModifier() {
+}
+
+bool WordMixerModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::WordMixerModifier &data) {
+	return true;
+}
+
+Common::SharedPtr<Modifier> WordMixerModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new WordMixerModifier(*this));
+}
+
+ObsidianPlugIn::ObsidianPlugIn(const Common::SharedPtr<WordGameData> &wgData)
+	: _movementModifierFactory(this), _rectShiftModifierFactory(this), _textWorkModifierFactory(this),
+	  _dictionaryModifierFactory(this), _wordMixerModifierFactory(this), _wgData(wgData) {
 }
 
 void ObsidianPlugIn::registerModifiers(IPlugInModifierRegistrar *registrar) const {
 	registrar->registerPlugInModifier("Movement", &_movementModifierFactory);
 	registrar->registerPlugInModifier("rectshift", &_rectShiftModifierFactory);
 	registrar->registerPlugInModifier("TextWork", &_textWorkModifierFactory);
+	registrar->registerPlugInModifier("Dictionary", &_dictionaryModifierFactory);
+	registrar->registerPlugInModifier("WordMixer", &_wordMixerModifierFactory);
+}
+
+const Common::SharedPtr<WordGameData>& ObsidianPlugIn::getWordGameData() const {
+	return _wgData;
+}
+
+bool WordGameData::load(Common::SeekableReadStream *stream, const WordGameLoadBucket *buckets, uint numBuckets, uint alignment, bool backwards) {
+	_buckets.resize(numBuckets);
+
+	size_t totalWords = 0;
+	for (size_t i = 0; i < numBuckets; i++) {
+		const WordGameLoadBucket &inBucket = buckets[i];
+		WordBucket &outBucket = _buckets[i];
+
+		uint32 sizeBytes = inBucket.endAddress - inBucket.startAddress;
+		uint wordLength = i;
+		uint spacing = (wordLength + alignment) - (wordLength % alignment);
+
+		outBucket.spacing = spacing;
+		outBucket.chars.resize(sizeBytes);
+
+		assert(sizeBytes % alignment == 0);
+
+		if (sizeBytes > 0) {
+			if (!stream->seek(inBucket.startAddress, SEEK_SET))
+				return false;
+			stream->read(&outBucket.chars[0], sizeBytes);
+		}
+
+		uint numWords = sizeBytes / spacing;
+		outBucket.wordIndexes.resize(numWords);
+
+		if (backwards) {
+			for (size_t wordIndex = 0; wordIndex < numWords / 2; wordIndex++) {
+				char *swapA = &outBucket.chars[wordIndex * spacing];
+				char *swapB = &outBucket.chars[(numWords - 1 - wordIndex) * spacing];
+
+				for (size_t chIndex = 0; chIndex < wordLength; chIndex++) {
+					char temp = swapA[chIndex];
+					swapA[chIndex] = swapB[chIndex];
+					swapB[chIndex] = temp;
+				}
+			}
+		}
+
+		totalWords += numWords;
+	}
+
+	_sortedWords.resize(totalWords);
+
+	Common::Array<size_t> currentWordIndexes;
+	currentWordIndexes.resize(numBuckets);
+
+	for (size_t i = 0; i < numBuckets; i++)
+		currentWordIndexes[i] = 0;
+
+	for (size_t wordIndex = 0; wordIndex < totalWords; wordIndex++) {
+		size_t bestBucket = numBuckets;
+		const char *bestChars = nullptr;
+		for (size_t bucketIndex = 0; bucketIndex < numBuckets; bucketIndex++) {
+			size_t wordOffset = currentWordIndexes[bucketIndex] * _buckets[bucketIndex].spacing;
+			if (wordOffset < _buckets[bucketIndex].chars.size()) {
+				const char *candidate = &_buckets[bucketIndex].chars[wordOffset];
+				bool isWorse = true;
+				if (bestChars == nullptr)
+					isWorse = false;
+				else  {
+					// The best bucket will always be shorter if it's set, so this is only better if it precedes it alphabetically
+					for (size_t i = 0; i < bestBucket; i++) {
+						if (candidate[i] > bestChars[i]) {
+							break;
+						} else if (candidate[i] < bestChars[i]) {
+							isWorse = false;
+							break;
+						}
+					}
+				}
+
+				if (!isWorse) {
+					bestBucket = bucketIndex;
+					bestChars = candidate;
+				}
+			}
+		}
+
+		assert(bestChars != nullptr);
+
+		const size_t bucketWordIndex = currentWordIndexes[bestBucket];
+		_buckets[bestBucket].wordIndexes[bucketWordIndex] = wordIndex;
+		currentWordIndexes[bestBucket]++;
+
+		_sortedWords[wordIndex].chars = bestChars;
+		_sortedWords[wordIndex].length = bestBucket;
+	}
+
+	return !stream->err();
+}
+
+const Common::Array<WordGameData::WordBucket> &WordGameData::getWordBuckets() const {
+	return _buckets;
+}
+
+const Common::Array<WordGameData::SortedWord>& WordGameData::getSortedWords() const {
+	return _sortedWords;
 }
 
 } // End of namespace ObsidianPlugIn
 
 namespace PlugIns {
 
-Common::SharedPtr<PlugIn> createObsidian() {
-	return Common::SharedPtr<PlugIn>(new Obsidian::ObsidianPlugIn());
+Common::SharedPtr<PlugIn> createObsidian(const Common::SharedPtr<Obsidian::WordGameData> &wgData) {
+	return Common::SharedPtr<PlugIn>(new Obsidian::ObsidianPlugIn(wgData));
 }
 
 } // End of namespace PlugIns
diff --git a/engines/mtropolis/plugin/obsidian.h b/engines/mtropolis/plugin/obsidian.h
index f7ce43d3768..f1397961eab 100644
--- a/engines/mtropolis/plugin/obsidian.h
+++ b/engines/mtropolis/plugin/obsidian.h
@@ -31,6 +31,9 @@ namespace MTropolis {
 
 namespace Obsidian {
 
+class ObsidianPlugIn;
+class WordGameData;
+
 class MovementModifier : public Modifier {
 public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Obsidian::MovementModifier &data);
@@ -79,16 +82,91 @@ private:
 	int32 _lastChar;
 };
 
+class DictionaryModifier : public Modifier {
+public:
+	DictionaryModifier();
+
+	bool load(const PlugInModifierLoaderContext &context, const Data::Obsidian::DictionaryModifier &data);
+
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib);
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Dictionary Modifier"; }
+#endif
+
+private:
+	void resolveStringIndex();
+	MiniscriptInstructionOutcome scriptSetString(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetIndex(MiniscriptThread *thread, const DynamicValue &value);
+
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
+	Common::String _str;
+
+	const ObsidianPlugIn *_plugIn;
+	int32 _index;
+	bool _isIndexResolved;
+};
+
+class WordMixerModifier : public Modifier {
+public:
+	WordMixerModifier();
+
+	bool load(const PlugInModifierLoaderContext &context, const Data::Obsidian::WordMixerModifier &data);
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "WordMixer Modifier"; }
+#endif
+
+private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+};
+
 class ObsidianPlugIn : public MTropolis::PlugIn {
 public:
-	ObsidianPlugIn();
+	ObsidianPlugIn(const Common::SharedPtr<WordGameData> &wgData);
 
 	void registerModifiers(IPlugInModifierRegistrar *registrar) const override;
 
+	const Common::SharedPtr<WordGameData> &getWordGameData() const;
+
 private:
 	PlugInModifierFactory<MovementModifier, Data::Obsidian::MovementModifier> _movementModifierFactory;
 	PlugInModifierFactory<RectShiftModifier, Data::Obsidian::RectShiftModifier> _rectShiftModifierFactory;
 	PlugInModifierFactory<TextWorkModifier, Data::Obsidian::TextWorkModifier> _textWorkModifierFactory;
+	PlugInModifierFactory<WordMixerModifier, Data::Obsidian::WordMixerModifier> _wordMixerModifierFactory;
+	PlugInModifierFactory<DictionaryModifier, Data::Obsidian::DictionaryModifier> _dictionaryModifierFactory;
+
+	Common::SharedPtr<WordGameData> _wgData;
+};
+
+struct WordGameLoadBucket {
+	uint32 startAddress;
+	uint32 endAddress;
+};
+
+class WordGameData {
+public:
+	struct WordBucket {
+		Common::Array<char> chars;
+		Common::Array<uint16> wordIndexes;
+		uint32 spacing;
+	};
+
+	struct SortedWord {
+		const char *chars;
+		uint length;
+	};
+
+	bool load(Common::SeekableReadStream *stream, const WordGameLoadBucket *buckets, uint numBuckets, uint alignment, bool backwards);
+
+	const Common::Array<WordBucket> &getWordBuckets() const;
+	const Common::Array<SortedWord> &getSortedWords() const;
+
+private:
+	Common::Array<WordBucket> _buckets;
+	Common::Array<SortedWord> _sortedWords;
 };
 
 } // End of namespace Obsidian
diff --git a/engines/mtropolis/plugin/obsidian_data.cpp b/engines/mtropolis/plugin/obsidian_data.cpp
index 7aec2579487..3608c44ae57 100644
--- a/engines/mtropolis/plugin/obsidian_data.cpp
+++ b/engines/mtropolis/plugin/obsidian_data.cpp
@@ -65,6 +65,23 @@ DataReadErrorCode TextWorkModifier::load(PlugIn& plugIn, const PlugInModifier& p
 	return kDataReadErrorNone;
 }
 
+DataReadErrorCode WordMixerModifier::load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) {
+	if (prefix.plugInRevision != 1)
+		return kDataReadErrorUnsupportedRevision;
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode DictionaryModifier::load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) {
+	if (prefix.plugInRevision != 1)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!str.load(reader) || !index.load(reader))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 } // End of namespace Obsidian
 
 } // End of namespace Data
diff --git a/engines/mtropolis/plugin/obsidian_data.h b/engines/mtropolis/plugin/obsidian_data.h
index 2fedd316d23..230fb8254af 100644
--- a/engines/mtropolis/plugin/obsidian_data.h
+++ b/engines/mtropolis/plugin/obsidian_data.h
@@ -71,6 +71,19 @@ protected:
 	DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override;
 };
 
+struct DictionaryModifier : public PlugInModifierData {
+	PlugInTypeTaggedValue str;
+	PlugInTypeTaggedValue index;
+
+protected:
+	DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override;
+};
+
+struct WordMixerModifier : public PlugInModifierData {
+protected:
+	DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override;
+};
+
 } // End of namespace Obsidian
 
 } // End of namespace Data
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 473676958aa..2e947a39a5f 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -812,6 +812,12 @@ VThreadState MidiModifier::consumeMessage(Runtime *runtime, const Common::Shared
 			}
 		}
 	}
+	if (_terminateWhen.respondsTo(msg->getEvent())) {
+#ifdef MTROPOLIS_DEBUG_ENABLE
+		if (Debugger *debugger = runtime->debugGetDebugger())
+			debugger->notify(kDebugSeverityWarning, "MIDI player ordered to terminate, which isn't supported yet");
+#endif
+	}
 
 	return kVThreadReturn;
 }
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index 57889f91fc5..8f86b3ce5c9 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -50,6 +50,7 @@ public:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Cursor Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
@@ -183,7 +184,7 @@ public:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "MIDI Modifier"; }
-	SupportStatus debugGetSupportStatus() const override { return kSupportStatusPartial; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
diff --git a/engines/mtropolis/plugins.h b/engines/mtropolis/plugins.h
index 3c8ab00f903..107d05e74c7 100644
--- a/engines/mtropolis/plugins.h
+++ b/engines/mtropolis/plugins.h
@@ -28,12 +28,18 @@ class MidiDriver;
 
 namespace MTropolis {
 
+namespace Obsidian {
+
+class WordGameData;
+
+} // End of namespace Obsidian
+
 class PlugIn;
 
 namespace PlugIns {
 
 Common::SharedPtr<PlugIn> createStandard();
-Common::SharedPtr<PlugIn> createObsidian();
+Common::SharedPtr<PlugIn> createObsidian(const Common::SharedPtr<Obsidian::WordGameData> &wgData);
 
 } // End of namespace PlugIns
 
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index a538652929b..5a7fdd67d9e 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -2136,7 +2136,10 @@ MiniscriptInstructionOutcome WorldManagerInterface::writeRefAttribute(Miniscript
 		DynamicValueWriteFuncHelper<WorldManagerInterface, &WorldManagerInterface::setCurrentScene>::create(this, result);
 		return kMiniscriptInstructionOutcomeContinue;
 	}
-
+	if (attrib == "refreshcursor") {
+		DynamicValueWriteFuncHelper<WorldManagerInterface, &WorldManagerInterface::setRefreshCursor>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
 	return RuntimeObject::writeRefAttribute(thread, result, attrib);
 }
 
@@ -2167,6 +2170,16 @@ MiniscriptInstructionOutcome WorldManagerInterface::setCurrentScene(MiniscriptTh
 	return kMiniscriptInstructionOutcomeContinue;
 }
 
+MiniscriptInstructionOutcome WorldManagerInterface::setRefreshCursor(MiniscriptThread *thread, const DynamicValue &value) {
+	if (value.getType() != DynamicValueTypes::kBoolean)
+		return kMiniscriptInstructionOutcomeFailed;
+
+	if (value.getBool())
+		thread->getRuntime()->forceCursorRefreshOnce();
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 SystemInterface::SystemInterface() : _masterVolume(kFullVolume) {
 }
 
@@ -3206,7 +3219,8 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, ISaveUIProvider *saveProv
 	_nextRuntimeGUID(1), _realDisplayMode(kColorDepthModeInvalid), _fakeDisplayMode(kColorDepthModeInvalid),
 	_displayWidth(1024), _displayHeight(768), _realTimeBase(0), _playTimeBase(0), _sceneTransitionState(kSceneTransitionStateNotTransitioning),
 	_lastFrameCursor(nullptr), _defaultCursor(new DefaultCursorGraphic()), _platform(kProjectPlatformUnknown),
-	_cachedMousePosition(Point16::create(0, 0)), _realMousePosition(Point16::create(0, 0)), _trackedMouseOutside(false), _haveModifierOverrideCursor(false) {
+	_cachedMousePosition(Point16::create(0, 0)), _realMousePosition(Point16::create(0, 0)), _trackedMouseOutside(false),
+	_forceCursorRefreshOnce(true), _haveModifierOverrideCursor(false) {
 	_random.reset(new Common::RandomSource("mtropolis"));
 
 	_vthread.reset(new VThread());
@@ -3301,6 +3315,13 @@ bool Runtime::runFrame() {
 			continue;
 		}
 
+		if (_forceCursorRefreshOnce) {
+			_forceCursorRefreshOnce = false;
+			UpdateMousePositionTaskData *taskData = _vthread->pushTask("Runtime::updateMousePositionTask", this, &Runtime::updateMousePositionTask);
+			taskData->x = _cachedMousePosition.x;
+			taskData->y = _cachedMousePosition.y;
+		}
+
 		if (_queuedProjectDesc) {
 			Common::SharedPtr<ProjectDescription> desc = _queuedProjectDesc;
 			_queuedProjectDesc.reset();
@@ -4573,6 +4594,10 @@ void Runtime::clearModifierCursorOverride() {
 	}
 }
 
+void Runtime::forceCursorRefreshOnce() {
+	_forceCursorRefreshOnce = true;
+}
+
 Common::RandomSource *Runtime::getRandom() const {
 	return _random.get();
 }
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index ab181738fb7..c8bc1a405f1 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -1489,6 +1489,7 @@ public:
 	const Point16 &getCachedMousePosition() const;
 	void setModifierCursorOverride(uint32 cursorID);
 	void clearModifierCursorOverride();
+	void forceCursorRefreshOnce();
 
 	Common::RandomSource *getRandom() const;
 	WorldManagerInterface *getWorldManagerInterface() const;
@@ -1670,6 +1671,7 @@ private:
 	Common::WeakPtr<Structural> _mouseOverObject;
 	Common::WeakPtr<Structural> _mouseTrackingObject;
 	bool _trackedMouseOutside;
+	bool _forceCursorRefreshOnce;
 
 	uint32 _modifierOverrideCursorID;
 	bool _haveModifierOverrideCursor;
@@ -1768,6 +1770,7 @@ public:
 
 private:
 	MiniscriptInstructionOutcome setCurrentScene(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome setRefreshCursor(MiniscriptThread *thread, const DynamicValue &value);
 };
 
 class AssetManagerInterface : public RuntimeObject {


Commit: 4d8f8bc43bd0ac53a802e3806ad21b938fad6af7
    https://github.com/scummvm/scummvm/commit/4d8f8bc43bd0ac53a802e3806ad21b938fad6af7
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix decompression of temporally-compressed mToons that don't have the flag set

Changed paths:
    engines/mtropolis/assets.cpp
    engines/mtropolis/assets.h
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index 5a7e4d1aebd..47363f6a036 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -106,7 +106,7 @@ size_t CachedAudio::getNumSamples(const AudioMetadata &metadata) const {
 	}
 }
 
-CachedMToon::CachedMToon() {
+CachedMToon::CachedMToon() : _isRLETemporalCompressed(false) {
 }
 
 bool CachedMToon::loadFromStream(const Common::SharedPtr<MToonMetadata> &metadata, Common::ReadStream *stream, size_t size) {
@@ -120,20 +120,32 @@ bool CachedMToon::loadFromStream(const Common::SharedPtr<MToonMetadata> &metadat
 			return false;
 	}
 
-	if (metadata->codecID == kMToonRLECodecID)
+	if (metadata->codecID == kMToonRLECodecID) {
 		loadRLEFrames(data);
+		uint16 firstFrameWidth = metadata->frames[0].rect.getWidth();
+		uint16 firstFrameHeight = metadata->frames[0].rect.getHeight();
+
+		_isRLETemporalCompressed = true;
+		for (size_t i = 0; i < metadata->frames.size(); i++) {
+			const MToonMetadata::FrameDef &frame = metadata->frames[i];
+			if (frame.rect.getWidth() != firstFrameWidth || frame.rect.getHeight() != firstFrameHeight) {
+				_isRLETemporalCompressed = false;
+				break;
+			}
+		}
+	}
 
-	if (!metadata->temporalCompression)
-		decompressNonTemporalFrames(data);
+	if (!_isRLETemporalCompressed)
+		decompressFrames(data);
 
 	return true;
 }
 
-void CachedMToon::decompressNonTemporalFrames(const Common::Array<uint8> &data) {
+void CachedMToon::decompressFrames(const Common::Array<uint8> &data) {
 	size_t numFrames = _metadata->frames.size();
 
-	_ntSurfaces.resize(numFrames);
-	_optimizedNTSurfaces.resize(numFrames);
+	_decompressedFrames.resize(numFrames);
+	_optimizedFrames.resize(numFrames);
 
 	for (size_t i = 0; i < numFrames; i++) {
 		if (_metadata->codecID == kMToonRLECodecID) {
@@ -357,7 +369,7 @@ void CachedMToon::decompressRLEFrame(size_t frameIndex) {
 
 	decompressRLEFrameToImage(frameIndex, *surface);
 
-	this->_ntSurfaces[frameIndex] = surface;
+	this->_decompressedFrames[frameIndex] = surface;
 }
 
 void CachedMToon::loadUncompressedFrame(const Common::Array<uint8> &data, size_t frameIndex) {
@@ -420,7 +432,7 @@ void CachedMToon::loadUncompressedFrame(const Common::Array<uint8> &data, size_t
 		}
 	}
 
-	_ntSurfaces[frameIndex] = surface;
+	_decompressedFrames[frameIndex] = surface;
 }
 
 template<class TSrcFrame, class TSrcNumber, uint32 TSrcLiteralMask, uint32 TSrcTransparentSkipMask, class TDestFrame, class TDestNumber, uint32 TDestLiteralMask, uint32 TDestTransparentSkipMask>
@@ -474,21 +486,20 @@ void CachedMToon::rleReformat(const TSrcFrame &srcFrame, const Graphics::PixelFo
 
 void CachedMToon::optimize(Runtime *runtime) {
 	Graphics::PixelFormat renderFmt = runtime->getRenderPixelFormat();
-	if (!_metadata->temporalCompression) {
-		optimizeNonTemporal(renderFmt);
-	} else if (_metadata->codecID == kMToonRLECodecID) {
+	if (_isRLETemporalCompressed)
 		optimizeRLE(renderFmt);
-	}
+	else
+		optimizeNonTemporal(renderFmt);
 }
 
 void CachedMToon::optimizeNonTemporal(const Graphics::PixelFormat &targetFormatRef) {
 	const Graphics::PixelFormat targetFormat = targetFormatRef;
 
-	_optimizedNTSurfaces.resize(_ntSurfaces.size());
+	_optimizedFrames.resize(_decompressedFrames.size());
 
-	for (size_t i = 0; i < _ntSurfaces.size(); i++) {
-		Common::SharedPtr<Graphics::Surface> srcSurface = _ntSurfaces[i];
-		Common::SharedPtr<Graphics::Surface> &optimizedSurfRef = _optimizedNTSurfaces[i];
+	for (size_t i = 0; i < _decompressedFrames.size(); i++) {
+		Common::SharedPtr<Graphics::Surface> srcSurface = _decompressedFrames[i];
+		Common::SharedPtr<Graphics::Surface> &optimizedSurfRef = _optimizedFrames[i];
 
 		// FIXME: Aggregate these checks and merge into a single format field
 		if (optimizedSurfRef == nullptr || optimizedSurfRef->format != targetFormat) {
@@ -538,8 +549,8 @@ void CachedMToon::optimizeRLE(const Graphics::PixelFormat &targetFormatRef) {
 }
 
 void CachedMToon::getOrRenderFrame(uint32 prevFrame, uint32 targetFrame, Common::SharedPtr<Graphics::Surface>& surface) const {
-	if (!_metadata->temporalCompression) {
-		surface = _optimizedNTSurfaces[targetFrame];
+	if (!_isRLETemporalCompressed) {
+		surface = _optimizedFrames[targetFrame];
 	} else if (_metadata->codecID == kMToonRLECodecID) {
 		uint32 firstFrameToRender = 0;
 		uint32 backStopFrame = 0;
@@ -999,7 +1010,6 @@ bool MToonAsset::load(AssetLoaderContext &context, const Data::MToonAsset &data)
 	}
 
 	_metadata->codecData = data.codecData;
-	_metadata->temporalCompression = ((data.encodingFlags & Data::MToonAsset::kEncodingFlag_TemporalCompression) != 0);
 
 	return true;
 }
diff --git a/engines/mtropolis/assets.h b/engines/mtropolis/assets.h
index cd130ef6629..df3fc7c1c41 100644
--- a/engines/mtropolis/assets.h
+++ b/engines/mtropolis/assets.h
@@ -91,7 +91,6 @@ struct MToonMetadata {
 	Rect16 rect;
 	uint16 bitsPerPixel;
 	uint32 codecID;
-	bool temporalCompression;
 
 	Common::Array<FrameDef> frames;
 	Common::Array<FrameRangeDef> frameRanges;
@@ -136,7 +135,7 @@ private:
 	static const uint32 kMToonRLEKeyframePrefix = 0x524c4520;
 	static const uint32 kMToonRLETemporalFramePrefix = 1;
 
-	void decompressNonTemporalFrames(const Common::Array<uint8> &data);
+	void decompressFrames(const Common::Array<uint8> &data);
 	void decompressRLEFrameToImage(size_t frameIndex, Graphics::Surface &surface);
 	void loadRLEFrames(const Common::Array<uint8> &data);
 	void decompressRLEFrame(size_t frameIndex);
@@ -148,9 +147,10 @@ private:
 	Common::Array<Rle8Frame> _dataRLE8;
 	Common::Array<Rle16Frame> _dataRLE16;
 	Common::Array<Rle32Frame> _dataRLE32;
+	bool _isRLETemporalCompressed;
 
-	Common::Array<Common::SharedPtr<Graphics::Surface> > _ntSurfaces;
-	Common::Array<Common::SharedPtr<Graphics::Surface> > _optimizedNTSurfaces;
+	Common::Array<Common::SharedPtr<Graphics::Surface> > _decompressedFrames;
+	Common::Array<Common::SharedPtr<Graphics::Surface> > _optimizedFrames;
 
 	Graphics::PixelFormat _rleInternalFormat;
 	Graphics::PixelFormat _rleOptimizedFormat;
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index b6f630f8c98..d9566e169aa 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -454,6 +454,8 @@ MToonElement::MToonElement() : _cel1Based(1), _renderedFrame(0), _flushPriority(
 }
 
 MToonElement::~MToonElement() {
+	if (_playMediaSignaller)
+		_playMediaSignaller->removeReceiver(this);
 }
 
 bool MToonElement::load(ElementLoaderContext &context, const Data::MToonElement &data) {
@@ -491,7 +493,9 @@ MiniscriptInstructionOutcome MToonElement::writeRefAttribute(MiniscriptThread *t
 	} else if (attrib == "flushpriority") {
 		DynamicValueWriteIntegerHelper<int32>::create(&_flushPriority, result);
 		return kMiniscriptInstructionOutcomeContinue;
-
+	} else if (attrib == "maintainrate") {
+		DynamicValueWriteBoolHelper::create(&_maintainRate, result);
+		return kMiniscriptInstructionOutcomeContinue;
 	}
 
 	return VisualElement::writeRefAttribute(thread, result, attrib);
@@ -513,9 +517,16 @@ void MToonElement::activate() {
 
 	_cachedMToon = static_cast<MToonAsset *>(asset.get())->loadAndCacheMToon(_runtime);
 	_metadata = _cachedMToon->getMetadata();
+
+	_playMediaSignaller = project->notifyOnPlayMedia(this);
 }
 
 void MToonElement::deactivate() {
+	if (_playMediaSignaller) {
+		_playMediaSignaller->removeReceiver(this);
+		_playMediaSignaller.reset();
+	}
+
 	_renderSurface.reset();
 }
 
@@ -542,6 +553,8 @@ void MToonElement::render(Window *window) {
 	}
 }
 
+void MToonElement::playMedia(Runtime *runtime, Project *project) {
+}
 
 TextLabelElement::TextLabelElement() : _needsRender(false), _isBitmap(false) {
 }
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index d188ae406fb..716d6008d1a 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -148,7 +148,7 @@ private:
 	Runtime *_runtime;
 };
 
-class MToonElement : public VisualElement {
+class MToonElement : public VisualElement, public IPlayMediaSignalReceiver {
 public:
 	MToonElement();
 	~MToonElement();
@@ -169,6 +169,8 @@ public:
 #endif
 
 private:
+	void playMedia(Runtime *runtime, Project *project) override;
+
 	bool _cacheBitmap;
 
 	// If set, then carry over residual frame time and display at the desired rate.  If not set, reset residual each frame for smoother animation.
@@ -185,6 +187,7 @@ private:
 
 	Common::SharedPtr<MToonMetadata> _metadata;
 	Common::SharedPtr<CachedMToon> _cachedMToon;
+	Common::SharedPtr<PlayMediaSignaller> _playMediaSignaller;
 };
 
 class TextLabelElement : public VisualElement {
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 5a7fdd67d9e..e08d78d5627 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -1546,6 +1546,33 @@ void DynamicValueWriteStringHelper::create(Common::String *strValue, DynamicValu
 
 DynamicValueWriteStringHelper DynamicValueWriteStringHelper::_instance;
 
+MiniscriptInstructionOutcome DynamicValueWriteBoolHelper::write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr ptrOrOffset) const {
+	bool &dest = *static_cast<bool *>(objectRef);
+	switch (value.getType()) {
+	case DynamicValueTypes::kBoolean:
+		dest = value.getBool();
+		return kMiniscriptInstructionOutcomeContinue;
+	default:
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+}
+
+MiniscriptInstructionOutcome DynamicValueWriteBoolHelper::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const {
+	return kMiniscriptInstructionOutcomeFailed;
+}
+
+MiniscriptInstructionOutcome DynamicValueWriteBoolHelper::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const {
+	return kMiniscriptInstructionOutcomeFailed;
+}
+
+void DynamicValueWriteBoolHelper::create(bool *boolValue, DynamicValueWriteProxy &proxy) {
+	proxy.pod.ptrOrOffset = 0;
+	proxy.pod.objectRef = boolValue;
+	proxy.pod.ifc = &_instance;
+}
+
+DynamicValueWriteBoolHelper DynamicValueWriteBoolHelper::_instance;
+
 MiniscriptInstructionOutcome DynamicValueWriteObjectHelper::write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr ptrOrOffset) const {
 	thread->error("Can't write to read-only object value");
 	return kMiniscriptInstructionOutcomeFailed;
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index c8bc1a405f1..942606c8fb5 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -949,6 +949,17 @@ private:
 	static DynamicValueWriteStringHelper _instance;
 };
 
+struct DynamicValueWriteBoolHelper : public IDynamicValueWriteInterface {
+	MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr ptrOrOffset) const override;
+	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const override;
+	MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
+
+	static void create(bool *boolValue, DynamicValueWriteProxy &proxy);
+
+private:
+	static DynamicValueWriteBoolHelper _instance;
+};
+
 template<class TClass, MiniscriptInstructionOutcome (TClass::*TWriteMethod)(MiniscriptThread *thread, const DynamicValue &dest), MiniscriptInstructionOutcome (TClass::*TRefAttribMethod)(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib)>
 struct DynamicValueWriteOrRefAttribFuncHelper : public IDynamicValueWriteInterface {
 	MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr ptrOrOffset) const override {


Commit: ab07cd3e5661b3fa9c10c414b9129edfaac27832
    https://github.com/scummvm/scummvm/commit/ab07cd3e5661b3fa9c10c414b9129edfaac27832
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: mToon fixes (get Obsidian file cabinets working)

Changed paths:
    engines/mtropolis/assets.cpp
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/miniscript.h
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index 47363f6a036..7a4e87209d1 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -327,6 +327,9 @@ void CachedMToon::loadRLEFrames(const Common::Array<uint8> &data) {
 				_dataRLE8[i].data.resize(frameDataSize);
 				memcpy(&_dataRLE8[i].data[0], &data[baseOffset + 20], frameDataSize);
 			} else if (bpp == 16) {
+				// RLE16 data size excludes the header
+				frameDataSize -= 20;
+
 				uint32 numDWords = frameDataSize / 2;
 				_dataRLE16[i].data.resize(numDWords);
 				memcpy(&_dataRLE16[i].data[0], &data[baseOffset + 20], numDWords * 2);
@@ -448,7 +451,8 @@ void CachedMToon::rleReformat(const TSrcFrame &srcFrame, const Graphics::PixelFo
 	destFrame.version = srcFrame.version;
 
 	while (offset < srcFrame.data.size()) {
-		const uint32 rleCode = srcFrame.data[offset];
+		uint32 rleCodeOffset = offset;
+		const uint32 rleCode = srcFrame.data[rleCodeOffset];
 		if (rleCode == 0) {
 			destFrame.data[offset] = 0;
 			offset++;
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index d9566e169aa..4f65a24942e 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -22,6 +22,7 @@
 #include "mtropolis/elements.h"
 #include "mtropolis/assets.h"
 #include "mtropolis/element_factory.h"
+#include "mtropolis/miniscript.h"
 #include "mtropolis/render.h"
 
 #include "video/video_decoder.h"
@@ -450,7 +451,7 @@ void ImageElement::render(Window *window) {
 	}
 }
 
-MToonElement::MToonElement() : _cel1Based(1), _renderedFrame(0), _flushPriority(0) {
+MToonElement::MToonElement() : _frame(0), _renderedFrame(0), _flushPriority(0), _celStartTimeMSec(0), _isPlaying(false), _playRange(IntRange::create(1, 1)) {
 }
 
 MToonElement::~MToonElement() {
@@ -475,11 +476,17 @@ bool MToonElement::load(ElementLoaderContext &context, const Data::MToonElement
 
 bool MToonElement::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
 	if (attrib == "cel") {
-		result.setInt(_cel1Based);
+		result.setInt(_frame + 1);
 		return true;
 	} else if (attrib == "flushpriority") {
 		result.setInt(_flushPriority);
 		return true;
+	} else if (attrib == "rate") {
+		result.setFloat(_rateTimes10000 / 10000.0);
+		return true;
+	} else if (attrib == "range") {
+		result.setIntRange(_playRange);
+		return true;
 	}
 
 	return VisualElement::readAttribute(thread, result, attrib);
@@ -488,7 +495,7 @@ bool MToonElement::readAttribute(MiniscriptThread *thread, DynamicValue &result,
 MiniscriptInstructionOutcome MToonElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
 	if (attrib == "cel") {
 		// TODO proper support
-		DynamicValueWriteIntegerHelper<uint32>::create(&_cel1Based, result);
+		DynamicValueWriteFuncHelper<MToonElement, &MToonElement::scriptSetCel>::create(this, result);
 		return kMiniscriptInstructionOutcomeContinue;
 	} else if (attrib == "flushpriority") {
 		DynamicValueWriteIntegerHelper<int32>::create(&_flushPriority, result);
@@ -496,11 +503,37 @@ MiniscriptInstructionOutcome MToonElement::writeRefAttribute(MiniscriptThread *t
 	} else if (attrib == "maintainrate") {
 		DynamicValueWriteBoolHelper::create(&_maintainRate, result);
 		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "rate") {
+		DynamicValueWriteFuncHelper<MToonElement, &MToonElement::scriptSetRate>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "range") {
+		DynamicValueWriteFuncHelper<MToonElement, &MToonElement::scriptSetRange>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
 	}
 
 	return VisualElement::writeRefAttribute(thread, result, attrib);
 }
 
+VThreadState MToonElement::consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties>& msg) {
+	if (Event::create(EventIDs::kPlay, 0).respondsTo(msg->getEvent())) {
+		StartPlayingTaskData *startPlayingTaskData = runtime->getVThread().pushTask("MToonElement::startPlayingTask", this, &MToonElement::startPlayingTask);
+		startPlayingTaskData->runtime = runtime;
+
+		ChangeFlagTaskData *becomeVisibleTaskData = runtime->getVThread().pushTask("MToonElement::changeVisibilityTask", static_cast<VisualElement *>(this), &MToonElement::changeVisibilityTask);
+		becomeVisibleTaskData->desiredFlag = true;
+		becomeVisibleTaskData->runtime = runtime;
+
+		return kVThreadReturn;
+	}
+	if (Event::create(EventIDs::kStop, 0).respondsTo(msg->getEvent())) {
+		// Works differently from movies: Needs to hide the element and pause
+		warning("mToon element stops are not implemented");
+		return kVThreadReturn;
+	}
+
+	return kVThreadReturn;
+}
+
 void MToonElement::activate() {
 	Project *project = _runtime->getProject();
 	Common::SharedPtr<Asset> asset = project->getAssetByID(_assetID).lock();
@@ -519,6 +552,7 @@ void MToonElement::activate() {
 	_metadata = _cachedMToon->getMetadata();
 
 	_playMediaSignaller = project->notifyOnPlayMedia(this);
+	_playRange = IntRange::create(1, _metadata->frames.size());
 }
 
 void MToonElement::deactivate() {
@@ -533,17 +567,9 @@ void MToonElement::deactivate() {
 void MToonElement::render(Window *window) {
 	if (_cachedMToon) {
 		_cachedMToon->optimize(_runtime);
-		uint32 frame = 0;
-
-		if (_cel1Based < 1)
-			frame = 0;
-		else if (_cel1Based > _metadata->frames.size())
-			frame = _metadata->frames.size() - 1;
-		else
-			frame = _cel1Based - 1;
 
-		_cachedMToon->getOrRenderFrame(_renderedFrame, frame, _renderSurface);
-		_renderedFrame = frame;
+		_cachedMToon->getOrRenderFrame(_renderedFrame, _frame, _renderSurface);
+		_renderedFrame = _frame;
 	}
 
 	if (_renderSurface) {
@@ -553,9 +579,202 @@ void MToonElement::render(Window *window) {
 	}
 }
 
+VThreadState MToonElement::startPlayingTask(const StartPlayingTaskData &taskData) {
+	_frame = _playRange.min;
+	_paused = false;
+	_isPlaying = false;	// Reset play state, it starts for real in playMedia
+
+	Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kPlay, 0), DynamicValue(), getSelfReference()));
+	Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+	taskData.runtime->sendMessageOnVThread(dispatch);
+
+	return kVThreadReturn;
+}
+
+VThreadState MToonElement::changeFrameTask(const ChangeFrameTaskData &taskData) {
+	if (taskData.frame == _frame)
+		return kVThreadReturn;
+
+	uint32 minFrame = _playRange.min;
+	uint32 maxFrame = _playRange.max;
+	_frame = taskData.frame;
+
+	if (_frame == minFrame) {
+		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kAtFirstCel, 0), DynamicValue(), getSelfReference()));
+		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+		taskData.runtime->sendMessageOnVThread(dispatch);
+	}
+
+	if (_frame == maxFrame) {
+		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kAtLastCel, 0), DynamicValue(), getSelfReference()));
+		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+		taskData.runtime->sendMessageOnVThread(dispatch);
+	}
+
+	return kVThreadReturn;
+}
+
 void MToonElement::playMedia(Runtime *runtime, Project *project) {
+	uint32 targetFrame = _frame;
+
+	if (_paused)
+		return;
+
+	uint32 minFrame = _playRange.min - 1;
+	uint32 maxFrame = _playRange.max - 1;
+
+	uint64 playTime = runtime->getPlayTime();
+	if (!_isPlaying) {
+		_isPlaying = true;
+		_celStartTimeMSec = runtime->getPlayTime();
+	}
+
+	if (_rateTimes10000 < 0) {
+		warning("Playing mToons backwards is not implemented yet");
+		_rateTimes10000 = 0; 
+		return;
+	}
+
+	// Might be possible due to drift?
+	if (playTime < _celStartTimeMSec)
+		return;
+
+	uint64 timeSinceCelStart = playTime - _celStartTimeMSec;
+	uint64 framesAdvanced = timeSinceCelStart * static_cast<uint64>(_rateTimes10000) / static_cast<uint64>(10000000);
+
+	if (framesAdvanced > 0) {
+		// This needs to be handled correctly: Reaching the last frame triggers At Last Cel,
+		// but going PAST the last frame triggers automatic stop and pause.
+		// The Obsidian bureau filing cabinets depend on this, since they reset the cel when
+		// reaching the last cel but do not unpause.
+		bool ranPastEnd = false;
+
+		size_t framesRemainingToOnePastEnd = (maxFrame + 1) -_frame;
+		if (framesRemainingToOnePastEnd <= framesAdvanced) {
+			ranPastEnd = true;
+			if (_loop)
+				targetFrame = minFrame;
+			else
+				targetFrame = maxFrame;
+		} else
+			targetFrame = _frame + framesAdvanced;
+
+		if (_frame != targetFrame) {
+			_frame = targetFrame;
+
+			if (_frame == maxFrame) {
+				Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kAtLastCel, 0), DynamicValue(), getSelfReference()));
+				Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+				runtime->queueMessage(dispatch);
+			}
+
+			if (_frame == minFrame) {
+				Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kAtFirstCel, 0), DynamicValue(), getSelfReference()));
+				Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+				runtime->queueMessage(dispatch);
+			}
+		}
+
+		if (ranPastEnd && !_loop) {
+			_paused = true;
+
+			// Unlike movies, reaching the end of an mToon fires "Paused" not "Stopped"
+			Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kPause, 0), DynamicValue(), getSelfReference()));
+			Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+			runtime->queueMessage(dispatch);
+		}
+
+		if (_maintainRate)
+			_celStartTimeMSec = playTime;
+		else
+			_celStartTimeMSec += (static_cast<uint64>(10000000) * framesAdvanced) / _rateTimes10000;
+	}
 }
 
+MiniscriptInstructionOutcome MToonElement::scriptSetCel(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger)) {
+		thread->error("Attempted to set mToon cel to an invalid value");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	if (asInteger < _playRange.min)
+		asInteger = _playRange.min;
+	else if (asInteger > _playRange.max)
+		asInteger = _playRange.max;
+
+	uint32 frame = asInteger - 1;
+	_celStartTimeMSec = thread->getRuntime()->getPlayTime();
+
+	if (frame != _frame) {
+		ChangeFrameTaskData *taskData = thread->getRuntime()->getVThread().pushTask("MToonElement::changeFrameTask", this, &MToonElement::changeFrameTask);
+		taskData->runtime = _runtime;
+		taskData->frame = frame;
+
+		return kMiniscriptInstructionOutcomeYieldToVThreadNoRetry;
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome MToonElement::scriptSetRange(MiniscriptThread *thread, const DynamicValue &value) {
+	if (value.getType() != DynamicValueTypes::kIntegerRange) {
+		thread->error("Invalid type for mToon range");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	IntRange intRange = value.getIntRange();
+	size_t numFrames = _metadata->frames.size();
+	if (intRange.min < 1)
+		intRange.min = 1;
+	if (intRange.max > numFrames)
+		intRange.max = numFrames;
+
+	if (intRange.max < intRange.min)
+		intRange.min = intRange.max;
+
+	_playRange = intRange;
+
+	uint32 targetFrame = _frame;
+	uint32 minFrame = intRange.min - 1;
+	uint32 maxFrame = intRange.max - 1;
+	if (targetFrame < minFrame)
+		targetFrame = minFrame;
+	else if (targetFrame > maxFrame)
+		targetFrame = maxFrame;
+
+	if (targetFrame != _frame) {
+		ChangeFrameTaskData *taskData = thread->getRuntime()->getVThread().pushTask("MToonElement::changeFrameTask", this, &MToonElement::changeFrameTask);
+		taskData->frame = targetFrame;
+		taskData->runtime = _runtime;
+		return kMiniscriptInstructionOutcomeYieldToVThreadNoRetry;
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+void MToonElement::onPauseStateChanged() {
+	_celStartTimeMSec = _runtime->getPlayTime();
+}
+
+MiniscriptInstructionOutcome MToonElement::scriptSetRate(MiniscriptThread *thread, const DynamicValue &value) {
+	switch (value.getType()) {
+	case DynamicValueTypes::kFloat:
+		_rateTimes10000 = static_cast<int32>(round(value.getFloat()) * 10000.0);
+		break;
+	case DynamicValueTypes::kInteger:
+		_rateTimes10000 = value.getInt() * 10000;
+		break;
+	default:
+		thread->error("Invalid type for Miniscript rate");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+	_celStartTimeMSec = thread->getRuntime()->getPlayTime();
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+
 TextLabelElement::TextLabelElement() : _needsRender(false), _isBitmap(false) {
 }
 
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index 716d6008d1a..0eaf90e23bc 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -158,6 +158,8 @@ public:
 	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
 	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) override;
 
+	VThreadState consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
+
 	void activate() override;
 	void deactivate() override;
 
@@ -169,7 +171,24 @@ public:
 #endif
 
 private:
+	struct StartPlayingTaskData {
+		Runtime *runtime;
+	};
+
+	struct ChangeFrameTaskData {
+		Runtime *runtime;
+		uint32 frame;
+	};
+
+	VThreadState startPlayingTask(const StartPlayingTaskData &taskData);
+	VThreadState changeFrameTask(const ChangeFrameTaskData &taskData);
+
 	void playMedia(Runtime *runtime, Project *project) override;
+	MiniscriptInstructionOutcome scriptSetRate(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetCel(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetRange(MiniscriptThread *thread, const DynamicValue &value);
+
+	void onPauseStateChanged();
 
 	bool _cacheBitmap;
 
@@ -177,9 +196,11 @@ private:
 	bool _maintainRate;
 
 	uint32 _assetID;
-	uint32 _rateTimes10000;
-	uint32 _cel1Based;
+	int32 _rateTimes10000;
+	uint32 _frame;
 	int32 _flushPriority;
+	uint32 _celStartTimeMSec;
+	bool _isPlaying;	// Is actually rolling media, this is only set by playMedia because it needs to start after scene transition
 
 	Runtime *_runtime;
 	Common::SharedPtr<Graphics::Surface> _renderSurface;
@@ -188,6 +209,8 @@ private:
 	Common::SharedPtr<MToonMetadata> _metadata;
 	Common::SharedPtr<CachedMToon> _cachedMToon;
 	Common::SharedPtr<PlayMediaSignaller> _playMediaSignaller;
+
+	IntRange _playRange;
 };
 
 class TextLabelElement : public VisualElement {
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 201c6c869b5..2bb51ace257 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -1280,6 +1280,64 @@ MiniscriptInstructionOutcome PointCreate::execute(MiniscriptThread *thread) cons
 	return kMiniscriptInstructionOutcomeContinue;
 }
 
+MiniscriptInstructionOutcome RangeCreate::execute(MiniscriptThread *thread) const {
+	if (thread->getStackSize() < 2) {
+		thread->error("Stack underflow");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0, false);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	outcome = thread->dereferenceRValue(1, false);
+	if (outcome != kMiniscriptInstructionOutcomeContinue)
+		return outcome;
+
+	DynamicValue &yVal = thread->getStackValueFromTop(0).value;
+	DynamicValue &xValDest = thread->getStackValueFromTop(1).value;
+
+	int32 coords[2];
+	DynamicValue *coordInputs[2] = {&xValDest, &yVal};
+
+	for (int i = 0; i < 2; i++) {
+		DynamicValue *v = coordInputs[i];
+		DynamicValue listContents;
+
+		if (v->getType() == DynamicValueTypes::kList) {
+			// Yes this is actually allowed
+			const Common::SharedPtr<DynamicList> &list = v->getList();
+			if (list->getSize() != 1 || !list->getAtIndex(0, listContents)) {
+				thread->error("Can't convert list to integer");
+				return kMiniscriptInstructionOutcomeFailed;
+			}
+
+			v = &listContents;
+		}
+
+		switch (v->getType()) {
+		case DynamicValueTypes::kFloat:
+			coords[i] = static_cast<int32>(floor(v->getFloat() + 0.5)) & 0xffff;
+			break;
+		case DynamicValueTypes::kInteger:
+			coords[i] = v->getInt();
+			break;
+		case DynamicValueTypes::kBoolean:
+			coords[i] = (v->getBool()) ? 1 : 0;
+			break;
+		default:
+			thread->error("Invalid input for point creation");
+			return kMiniscriptInstructionOutcomeFailed;
+		}
+	}
+
+	xValDest.setIntRange(IntRange::create(coords[0], coords[1]));
+
+	thread->popValues(1);
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 GetChild::GetChild(uint32 attribute, bool isLValue, bool isIndexed)
 	: _attribute(attribute), _isLValue(isLValue), _isIndexed(isIndexed) {
 }
@@ -1873,10 +1931,6 @@ VThreadState MiniscriptThread::resume(const ResumeTaskData &taskData) {
 	if (instrsArray.size() == 0)
 		return kVThreadReturn;
 
-	if (_modifier->getStaticGUID() == 0x31ae0e) {
-		int n = 0;
-	}
-
 	MiniscriptInstruction *const *instrs = &instrsArray[0];
 	size_t numInstrs = instrsArray.size();
 
diff --git a/engines/mtropolis/miniscript.h b/engines/mtropolis/miniscript.h
index 159b2a97cdb..6d7ada6ccfc 100644
--- a/engines/mtropolis/miniscript.h
+++ b/engines/mtropolis/miniscript.h
@@ -265,7 +265,9 @@ namespace MiniscriptInstructions {
 		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
 	};
 
-	class RangeCreate : public UnimplementedInstruction {
+	class RangeCreate : public MiniscriptInstruction {
+	private:
+		MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override;
 	};
 
 	class VectorCreate : public UnimplementedInstruction {
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 5ad82cfb556..a60fffec686 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -746,9 +746,6 @@ Common::SharedPtr<Modifier> TimerMessengerModifier::shallowClone() const {
 }
 
 void TimerMessengerModifier::trigger(Runtime *runtime) {
-	if (getStaticGUID() == 0xd9550) {
-		int n = 0;
-	}
 	debug(3, "Timer %x '%s' triggered", getStaticGUID(), getName().c_str());
 	if (_looping) {
 		uint32 realMilliseconds = _milliseconds;
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index e08d78d5627..40c094cbe3b 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -2411,45 +2411,17 @@ bool Structural::readAttribute(MiniscriptThread *thread, DynamicValue &result, c
 			result.clear();
 		return true;
 	} else if (attrib == "previous") {
-		Structural *parent = getParent();
-		if (parent) {
-			const Common::Array<Common::SharedPtr<Structural> > &neighborhood = parent->getChildren();
-			bool found = false;
-			size_t foundIndex = 0;
-			for (size_t i = 0; i < neighborhood.size(); i++) {
-				if (neighborhood[i].get() == this) {
-					foundIndex = i;
-					found = true;
-					break;
-				}
-			}
-
-			if (found && foundIndex > 0)
-				result.setObject(neighborhood[foundIndex - 1]->getSelfReference());
-			else
-				result.clear();
-		} else
+		Structural *sibling = findPrevSibling();
+		if (sibling)
+			result.setObject(sibling->getSelfReference());
+		else
 			result.clear();
 		return true;
 	} else if (attrib == "next") {
-		Structural *parent = getParent();
-		if (parent) {
-			const Common::Array<Common::SharedPtr<Structural> > &neighborhood = parent->getChildren();
-			bool found = false;
-			size_t foundIndex = 0;
-			for (size_t i = 0; i < neighborhood.size(); i++) {
-				if (neighborhood[i].get() == this) {
-					foundIndex = i;
-					found = true;
-					break;
-				}
-			}
-
-			if (found && foundIndex < neighborhood.size() - 1)
-				result.setObject(neighborhood[foundIndex + 1]->getSelfReference());
-			else
-				result.clear();
-		} else
+		Structural *sibling = findNextSibling();
+		if (sibling)
+			result.setObject(sibling->getSelfReference());
+		else
 			result.clear();
 		return true;
 	} else if (attrib == "scene") {
@@ -2511,6 +2483,22 @@ MiniscriptInstructionOutcome Structural::writeRefAttribute(MiniscriptThread *thr
 		} else {
 			return kMiniscriptInstructionOutcomeFailed;
 		}
+	} else if (attrib == "next") {
+		Structural *sibling = findNextSibling();
+		if (sibling) {
+			DynamicValueWriteObjectHelper::create(sibling, result);
+			return kMiniscriptInstructionOutcomeContinue;
+		} else {
+			return kMiniscriptInstructionOutcomeFailed;
+		}
+	} else if (attrib == "previous") {
+		Structural *sibling = findPrevSibling();
+		if (sibling) {
+			DynamicValueWriteObjectHelper::create(sibling, result);
+			return kMiniscriptInstructionOutcomeContinue;
+		} else {
+			return kMiniscriptInstructionOutcomeFailed;
+		}
 	} else if (attrib == "loop") {
 		DynamicValueWriteFuncHelper<Structural, &Structural::scriptSetLoop>::create(this, result);
 		return kMiniscriptInstructionOutcomeContinue;
@@ -2557,6 +2545,48 @@ Structural *Structural::getParent() const {
 	return _parent;
 }
 
+Structural *Structural::findNextSibling() const {
+	Structural *parent = getParent();
+	if (parent) {
+		const Common::Array<Common::SharedPtr<Structural> > &neighborhood = parent->getChildren();
+		bool found = false;
+		size_t foundIndex = 0;
+		for (size_t i = 0; i < neighborhood.size(); i++) {
+			if (neighborhood[i].get() == this) {
+				foundIndex = i;
+				found = true;
+				break;
+			}
+		}
+
+		if (found && foundIndex < neighborhood.size() - 1)
+			return neighborhood[foundIndex + 1].get();
+	}
+
+	return nullptr;
+}
+
+Structural *Structural::findPrevSibling() const {
+	Structural *parent = getParent();
+	if (parent) {
+		const Common::Array<Common::SharedPtr<Structural> > &neighborhood = parent->getChildren();
+		bool found = false;
+		size_t foundIndex = 0;
+		for (size_t i = 0; i < neighborhood.size(); i++) {
+			if (neighborhood[i].get() == this) {
+				foundIndex = i;
+				found = true;
+				break;
+			}
+		}
+
+		if (found && foundIndex > 0)
+			return neighborhood[foundIndex - 1].get();
+	}
+
+	return nullptr;
+}
+
 void Structural::setParent(Structural *parent) {
 	_parent = parent;
 }
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 942606c8fb5..f1b6f91007d 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -340,6 +340,13 @@ struct IntRange {
 		return !((*this) == other);
 	}
 
+	inline static IntRange create(int32 min, int32 max) {
+		IntRange result;
+		result.min = min;
+		result.max = max;
+		return result;
+	}
+
 	bool refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib);
 	Common::String toString() const;
 };
@@ -1831,6 +1838,8 @@ public:
 	void holdAssets(const Common::Array<Common::SharedPtr<Asset> > &assets);
 
 	Structural *getParent() const;
+	Structural *findNextSibling() const;
+	Structural *findPrevSibling() const;
 	void setParent(Structural *parent);
 
 	// Helper that finds the scene containing the structural object, or itself if it is the scene


Commit: 63af8a81469b8dd7c3722a4efce28a94dd2594e6
    https://github.com/scummvm/scummvm/commit/63af8a81469b8dd7c3722a4efce28a94dd2594e6
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Handle bad mToon size fields

Changed paths:
    engines/mtropolis/assets.cpp


diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index 7a4e87209d1..f3622756e74 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -327,10 +327,9 @@ void CachedMToon::loadRLEFrames(const Common::Array<uint8> &data) {
 				_dataRLE8[i].data.resize(frameDataSize);
 				memcpy(&_dataRLE8[i].data[0], &data[baseOffset + 20], frameDataSize);
 			} else if (bpp == 16) {
-				// RLE16 data size excludes the header
-				frameDataSize -= 20;
-
-				uint32 numDWords = frameDataSize / 2;
+				// In RLE16, frameDataSize is sometimes set to frameDef.compressedSize but sometimes contains garbage,
+				// so we need to ignore it and derive size from the frameDef instead.
+				uint32 numDWords = (frameDef.compressedSize - 20) / 2;
 				_dataRLE16[i].data.resize(numDWords);
 				memcpy(&_dataRLE16[i].data[0], &data[baseOffset + 20], numDWords * 2);
 


Commit: a4c8fb1b8551428a1a3fe8ae5491a91f95ddfda9
    https://github.com/scummvm/scummvm/commit/a4c8fb1b8551428a1a3fe8ae5491a91f95ddfda9
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add support for QuickTime ranges, fix Obsidian Bureau light carousel triggering without interaction.

Changed paths:
    common/quicktime.h
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h
    video/qt_decoder.cpp
    video/qt_decoder.h


diff --git a/common/quicktime.h b/common/quicktime.h
index 707bf81a15d..f943af79aad 100644
--- a/common/quicktime.h
+++ b/common/quicktime.h
@@ -84,6 +84,11 @@ public:
 	 */
 	void setChunkBeginOffset(uint32 offset) { _beginOffset = offset; }
 
+	/**
+	 * Returns the movie time scale
+	 */
+	uint32 getTimeScale() const { return _timeScale; }
+
 	/** Find out if this parser has an open file handle */
 	bool isOpen() const { return _fd != nullptr; }
 
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 4f65a24942e..ece9e98b63b 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -205,6 +205,25 @@ bool MovieElement::load(ElementLoaderContext &context, const Data::MovieElement
 	return true;
 }
 
+bool MovieElement::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "range") {
+		result.setIntRange(_playRange);
+		return true;
+	}
+
+	return VisualElement::readAttribute(thread, result, attrib);
+}
+
+MiniscriptInstructionOutcome MovieElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
+	if (attrib == "range") {
+		DynamicValueWriteOrRefAttribFuncHelper<MovieElement, &MovieElement::scriptSetRange, &MovieElement::scriptRangeWriteRefAttribute>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	return VisualElement::writeRefAttribute(thread, result, attrib);
+}
+
+
 VThreadState MovieElement::consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
 	if (Event::create(EventIDs::kPlay, 0).respondsTo(msg->getEvent())) {
 		StartPlayingTaskData *startPlayingTaskData = runtime->getVThread().pushTask("MovieElement::startPlayingTask", this, &MovieElement::startPlayingTask);
@@ -255,9 +274,15 @@ void MovieElement::activate() {
 	if (!_videoDecoder->loadStream(movieDataStream))
 		_videoDecoder.reset();
 
+	_timeScale = qtDecoder->getTimeScale();
+
 	_unloadSignaller = project->notifyOnSegmentUnload(segmentIndex, this);
 	_playMediaSignaller = project->notifyOnPlayMedia(this);
 
+	_maxTimestamp = qtDecoder->getDuration().convertToFramerate(qtDecoder->getTimeScale()).totalNumberOfFrames();
+	_playRange = IntRange::create(0, _maxTimestamp);
+	_currentTimestamp = 0;
+
 	if (!_paused && _visible) {
 		StartPlayingTaskData *startPlayingTaskData = _runtime->getVThread().pushTask("MovieElement::startPlayingTask", this, &MovieElement::startPlayingTask);
 		startPlayingTaskData->runtime = _runtime;
@@ -278,40 +303,17 @@ void MovieElement::deactivate() {
 }
 
 void MovieElement::render(Window *window) {
-	int framesDecodedThisFrame = 0;
-
 	if (_needsReset) {
-		// TODO: Seek elsewhere
-		_videoDecoder->seekToFrame(0);
+		_videoDecoder->setReverse(_reversed);
+		_videoDecoder->seek(Audio::Timestamp(0, _timeScale).addFrames(_currentTimestamp));
+		_videoDecoder->setEndTime(Audio::Timestamp(0, _timeScale).addFrames(_reversed ? _playRange.min : _playRange.max));
 		const Graphics::Surface *decodedFrame = _videoDecoder->decodeNextFrame();
-		if (decodedFrame) {
+		if (decodedFrame)
 			_displayFrame = decodedFrame;
-			framesDecodedThisFrame++;
-		}
 
 		_needsReset = false;
 	}
 
-	while (_videoDecoder->needsUpdate()) {
-		if (_playEveryFrame && framesDecodedThisFrame > 0)
-			break;
-
-		const Graphics::Surface *decodedFrame = _videoDecoder->decodeNextFrame();
-
-		// GNARLY HACK: QuickTimeDecoder doesn't return true for endOfVideo or false for needsUpdate until it
-		// tries decoding past the end, so we're assuming that the decoded frame memory stays valid until we
-		// actually have a new frame and continuing to use it.
-		if (decodedFrame) {
-			framesDecodedThisFrame++;
-			_displayFrame = decodedFrame;
-			if (_playEveryFrame)
-				break;
-		}
-	}
-
-	if (framesDecodedThisFrame > 1)
-		debug(1, "Perf warning: %i video frames decoded in one frame", framesDecodedThisFrame);
-
 	if (_displayFrame) {
 		Graphics::ManagedSurface *target = window->getSurface().get();
 		Common::Rect srcRect(0, 0, _displayFrame->w, _displayFrame->h);
@@ -321,6 +323,10 @@ void MovieElement::render(Window *window) {
 }
 
 void MovieElement::playMedia(Runtime *runtime, Project *project) {
+	// If this isn't visible, then it wasn't rendered
+	if (!_visible)
+		return;
+
 	if (_videoDecoder) {
 		if (_shouldPlayIfNotPaused) {
 			if (_paused) {
@@ -331,10 +337,10 @@ void MovieElement::playMedia(Runtime *runtime, Project *project) {
 				}
 			} else {
 				// Goal state is playing
-				if (_videoDecoder->isPaused())
-					_videoDecoder->pauseVideo(false);
 				if (!_videoDecoder->isPlaying())
 					_videoDecoder->start();
+				if (_videoDecoder->isPaused())
+					_videoDecoder->pauseVideo(false);
 
 				_currentPlayState = kMediaStatePlaying;
 			}
@@ -346,6 +352,63 @@ void MovieElement::playMedia(Runtime *runtime, Project *project) {
 			_currentPlayState = kMediaStateStopped;
 		}
 
+		uint32 minTS = _playRange.min;
+		uint32 maxTS = _playRange.max;
+		uint32 targetTS = _currentTimestamp;
+
+		int framesDecodedThisFrame = 0;
+		while (_videoDecoder->needsUpdate()) {
+			if (_playEveryFrame && framesDecodedThisFrame > 0)
+				break;
+
+			const Graphics::Surface *decodedFrame = _videoDecoder->decodeNextFrame();
+
+			// GNARLY HACK: QuickTimeDecoder doesn't return true for endOfVideo or false for needsUpdate until it
+			// tries decoding past the end, so we're assuming that the decoded frame memory stays valid until we
+			// actually have a new frame and continuing to use it.
+			if (decodedFrame) {
+				framesDecodedThisFrame++;
+				_displayFrame = decodedFrame;
+				if (_playEveryFrame)
+					break;
+			}
+		}
+
+		if (_currentPlayState == kMediaStatePlaying) {
+			if (_videoDecoder->endOfVideo())
+				targetTS = _reversed ? _playRange.min : _playRange.max;
+			else
+				targetTS = (_videoDecoder->getTime() * _timeScale + 500) / 1000;
+		}
+
+		if (framesDecodedThisFrame > 1)
+			debug(1, "Perf warning: %i video frames decoded in one frame", framesDecodedThisFrame);
+
+		if (targetTS < _playRange.min)
+			targetTS = _playRange.min;
+		if (targetTS > _playRange.max)
+			targetTS = _playRange.max;
+
+		// Sync TS to the end of video if we hit the end
+
+		if (targetTS != _currentTimestamp) {
+			assert(!_paused);
+
+			_currentTimestamp = targetTS;
+
+			if (_currentTimestamp == maxTS) {
+				Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kAtLastCel, 0), DynamicValue(), getSelfReference()));
+				Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+				runtime->queueMessage(dispatch);
+			}
+
+			if (_currentTimestamp == minTS) {
+				Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kAtFirstCel, 0), DynamicValue(), getSelfReference()));
+				Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+				runtime->queueMessage(dispatch);
+			}
+		}
+
 		if (_currentPlayState == kMediaStatePlaying && _videoDecoder->endOfVideo()) {
 			if (_alternate) {
 				_reversed = !_reversed;
@@ -353,14 +416,14 @@ void MovieElement::playMedia(Runtime *runtime, Project *project) {
 					warning("Failed to reverse video decoder, disabling it");
 					_videoDecoder.reset();
 				}
+
+				uint32 endTS = _reversed ? _playRange.min : _playRange.max;
+				_videoDecoder->setEndTime(Audio::Timestamp(0, _timeScale).addFrames(endTS));
 			} else {
+				// It doesn't look like movies fire any events upon reaching the end, just At Last Cel and At First Cel
 				_videoDecoder->stop();
 				_currentPlayState = kMediaStateStopped;
 			}
-
-			Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(_reversed ? EventIDs::kAtFirstCel : EventIDs::kAtLastCel, 0), DynamicValue(), getSelfReference()));
-			Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
-			runtime->queueMessage(dispatch);
 		}
 	}
 }
@@ -369,10 +432,88 @@ void MovieElement::onSegmentUnloaded(int segmentIndex) {
 	_videoDecoder.reset();
 }
 
+MiniscriptInstructionOutcome MovieElement::scriptSetRange(MiniscriptThread *thread, const DynamicValue &value) {
+	if (value.getType() != DynamicValueTypes::kIntegerRange) {
+		thread->error("Wrong type for movie element range");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	return scriptSetRangeTyped(thread, value.getIntRange());
+}
+
+MiniscriptInstructionOutcome MovieElement::scriptSetRangeStart(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger)) {
+		thread->error("Couldn't set movie element range start");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	return scriptSetRangeTyped(thread, IntRange::create(asInteger, _playRange.max));
+}
+
+MiniscriptInstructionOutcome MovieElement::scriptSetRangeEnd(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger)) {
+		thread->error("Couldn't set movie element range end");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	return scriptSetRangeTyped(thread, IntRange::create(_playRange.min, asInteger));
+}
+
+MiniscriptInstructionOutcome MovieElement::scriptRangeWriteRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
+	if (attrib == "start") {
+		DynamicValueWriteFuncHelper<MovieElement, &MovieElement::scriptSetRangeStart>::create(this, result);
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+	if (attrib == "end") {
+		DynamicValueWriteFuncHelper<MovieElement, &MovieElement::scriptSetRangeStart>::create(this, result);
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	return kMiniscriptInstructionOutcomeFailed;
+}
+
+MiniscriptInstructionOutcome MovieElement::scriptSetRangeTyped(MiniscriptThread *thread, const IntRange &range) {
+	_playRange = range;
+
+	if (_playRange.min < 0)
+		_playRange.min = 0;
+	else if (_playRange.min > _maxTimestamp)
+		_playRange.min = _maxTimestamp;
+
+	if (_playRange.max > _maxTimestamp)
+		_playRange.max = _maxTimestamp;
+
+	if (_playRange.max < _playRange.min)
+		_playRange.max = _playRange.min;
+
+	uint32 minTS = _playRange.min;
+	uint32 maxTS = _playRange.max;
+	uint32 targetTS = _currentTimestamp;
+
+	if (targetTS < minTS)
+		targetTS = minTS;
+	if (targetTS > maxTS)
+		targetTS = maxTS;
+
+	if (targetTS != _currentTimestamp) {
+		SeekToTimeTaskData *taskData = thread->getRuntime()->getVThread().pushTask("MovieElement::seekToTimeTask", this, &MovieElement::seekToTimeTask);
+		taskData->runtime = _runtime;
+		taskData->timestamp = targetTS;
+
+		return kMiniscriptInstructionOutcomeYieldToVThreadNoRetry;
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 VThreadState MovieElement::startPlayingTask(const StartPlayingTaskData &taskData) {
 	if (_videoDecoder) {
-		// TODO: Frame ranges
+		_videoDecoder->stop();
+		_currentPlayState = kMediaStateStopped;
 		_needsReset = true;
+
 		_shouldPlayIfNotPaused = true;
 		_paused = false;
 
@@ -384,6 +525,42 @@ VThreadState MovieElement::startPlayingTask(const StartPlayingTaskData &taskData
 	return kVThreadReturn;
 }
 
+VThreadState MovieElement::seekToTimeTask(const SeekToTimeTaskData &taskData) {
+	uint32 minTS = _playRange.min;
+	uint32 maxTS = _playRange.max;
+
+	uint32 targetTS = taskData.timestamp;
+
+	if (targetTS < minTS)
+		targetTS = minTS;
+	if (targetTS > maxTS)
+		targetTS = maxTS;
+
+	if (targetTS == _currentTimestamp)
+		return kVThreadReturn;
+
+	_currentTimestamp = targetTS;
+	if (_videoDecoder) {
+		_videoDecoder->stop();
+		_currentPlayState = kMediaStateStopped;
+	}
+	_needsReset = true;
+
+	if (_currentTimestamp == minTS) {
+		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kAtFirstCel, 0), DynamicValue(), getSelfReference()));
+		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+		taskData.runtime->sendMessageOnVThread(dispatch);
+	}
+
+	if (_currentTimestamp == maxTS) {
+		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kAtLastCel, 0), DynamicValue(), getSelfReference()));
+		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+		taskData.runtime->sendMessageOnVThread(dispatch);
+	}
+
+	return kVThreadReturn;
+}
+
 ImageElement::ImageElement() : _cacheBitmap(false), _runtime(nullptr) {
 }
 
@@ -727,6 +904,9 @@ MiniscriptInstructionOutcome MToonElement::scriptSetRange(MiniscriptThread *thre
 	size_t numFrames = _metadata->frames.size();
 	if (intRange.min < 1)
 		intRange.min = 1;
+	else if (intRange.min > numFrames)
+		intRange.min = numFrames;
+
 	if (intRange.max > numFrames)
 		intRange.max = numFrames;
 
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index 0eaf90e23bc..61560a3b4a1 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -75,6 +75,9 @@ public:
 
 	bool load(ElementLoaderContext &context, const Data::MovieElement &data);
 
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) override;
+
 	VThreadState consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
 
 	void activate() override;
@@ -91,11 +94,24 @@ public:
 private:
 	void onSegmentUnloaded(int segmentIndex) override;
 
+	MiniscriptInstructionOutcome scriptSetRange(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetRangeStart(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetRangeEnd(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptRangeWriteRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib);
+
+	MiniscriptInstructionOutcome scriptSetRangeTyped(MiniscriptThread *thread, const IntRange &range);
+
 	struct StartPlayingTaskData {
 		Runtime *runtime;
 	};
 
+	struct SeekToTimeTaskData {
+		Runtime *runtime;
+		uint32 timestamp;
+	};
+
 	VThreadState startPlayingTask(const StartPlayingTaskData &taskData);
+	VThreadState seekToTimeTask(const SeekToTimeTaskData &taskData);
 
 	bool _cacheBitmap;
 	bool _alternate;
@@ -109,6 +125,11 @@ private:
 	uint32 _assetID;
 
 	Common::SharedPtr<Video::VideoDecoder> _videoDecoder;
+	uint32 _maxTimestamp;
+	uint32 _timeScale;
+	uint32 _currentTimestamp;
+	IntRange _playRange;
+
 	const Graphics::Surface *_displayFrame;
 
 	Common::SharedPtr<SegmentUnloadSignaller> _unloadSignaller;
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 2bb51ace257..5c916b1d52b 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -518,7 +518,7 @@ MiniscriptInstructionOutcome Set::execute(MiniscriptThread *thread) const {
 	}
 	thread->popValues(2);
 
-	return kMiniscriptInstructionOutcomeContinue;
+	return outcome;
 }
 
 Send::Send(const Event &evt, const MessageFlags &messageFlags) : _evt(evt), _messageFlags(messageFlags) {
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 40c094cbe3b..a078447ab09 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -2745,9 +2745,15 @@ MiniscriptInstructionOutcome Structural::scriptSetPaused(MiniscriptThread *threa
 	_paused = targetValue;
 	onPauseStateChanged();
 
-	Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(targetValue ? EventIDs::kPause : EventIDs::kUnpause, 0), DynamicValue(), getSelfReference()));
-	Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
-	thread->getRuntime()->sendMessageOnVThread(dispatch);
+	// Quirk: "pause" state changes during scene transitions don't fire "Paused" events.
+	// This is necessary in Obsidian to prevent the rotator lever from triggering when leaving the menu
+	// while at the Bureau light carousel, since the lever isn't flagged as paused but is set paused
+	// via an init script, and the lever trigger is detected via the pause event.
+	if (!thread->getRuntime()->isAwaitingSceneTransition()) {
+		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(targetValue ? EventIDs::kPause : EventIDs::kUnpause, 0), DynamicValue(), getSelfReference()));
+		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+		thread->getRuntime()->sendMessageOnVThread(dispatch);
+	}
 
 	return kMiniscriptInstructionOutcomeYieldToVThreadNoRetry;
 }
@@ -4655,6 +4661,10 @@ void Runtime::forceCursorRefreshOnce() {
 	_forceCursorRefreshOnce = true;
 }
 
+bool Runtime::isAwaitingSceneTransition() const {
+	return _sceneTransitionState != kSceneTransitionStateNotTransitioning;
+}
+
 Common::RandomSource *Runtime::getRandom() const {
 	return _random.get();
 }
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index f1b6f91007d..d63129084e4 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -1509,6 +1509,8 @@ public:
 	void clearModifierCursorOverride();
 	void forceCursorRefreshOnce();
 
+	bool isAwaitingSceneTransition() const;
+
 	Common::RandomSource *getRandom() const;
 	WorldManagerInterface *getWorldManagerInterface() const;
 	AssetManagerInterface *getAssetManagerInterface() const;
diff --git a/video/qt_decoder.cpp b/video/qt_decoder.cpp
index a226f9fb374..165acf0d0fa 100644
--- a/video/qt_decoder.cpp
+++ b/video/qt_decoder.cpp
@@ -558,6 +558,22 @@ const Graphics::Surface *QuickTimeDecoder::VideoTrackHandler::decodeNextFrame()
 	return frame;
 }
 
+Audio::Timestamp QuickTimeDecoder::VideoTrackHandler::getFrameTime(uint frame) const {
+	// TODO: This probably doesn't work right with edit lists
+	int cumulativeDuration = 0;
+	for (int ttsIndex = 0; ttsIndex < _parent->timeToSampleCount; ttsIndex++) {
+		const TimeToSampleEntry &tts = _parent->timeToSample[ttsIndex];
+		if (frame < tts.count)
+			return Audio::Timestamp(0, _parent->timeScale).addFrames(cumulativeDuration + frame * tts.duration);
+		else {
+			frame -= tts.count;
+			cumulativeDuration += tts.duration * tts.count;
+		}
+	}
+
+	return Audio::Timestamp().addFrames(-1);
+}
+
 const byte *QuickTimeDecoder::VideoTrackHandler::getPalette() const {
 	_dirtyPalette = false;
 	return _forcedDitherPalette ? _forcedDitherPalette : _curPalette;
diff --git a/video/qt_decoder.h b/video/qt_decoder.h
index 62496629e1f..fd7bc7a595b 100644
--- a/video/qt_decoder.h
+++ b/video/qt_decoder.h
@@ -139,6 +139,7 @@ private:
 		int getFrameCount() const;
 		uint32 getNextFrameStartTime() const; // milliseconds
 		const Graphics::Surface *decodeNextFrame();
+		Audio::Timestamp getFrameTime(uint frame) const;
 		const byte *getPalette() const;
 		bool hasDirtyPalette() const { return _curPalette; }
 		bool setReverse(bool reverse);


Commit: 5f2f692accfe37374aefcb35b7ac51b3374fb35a
    https://github.com/scummvm/scummvm/commit/5f2f692accfe37374aefcb35b7ac51b3374fb35a
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: More Obsidian Bureau fixes

Changed paths:
    engines/mtropolis/elements.cpp
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard_data.cpp


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index ece9e98b63b..c975d3616f4 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -223,9 +223,15 @@ MiniscriptInstructionOutcome MovieElement::writeRefAttribute(MiniscriptThread *t
 	return VisualElement::writeRefAttribute(thread, result, attrib);
 }
 
-
 VThreadState MovieElement::consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
 	if (Event::create(EventIDs::kPlay, 0).respondsTo(msg->getEvent())) {
+		// These reverse order
+		{
+			Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kPlay, 0), DynamicValue(), getSelfReference()));
+			Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+			runtime->sendMessageOnVThread(dispatch);
+		}
+
 		StartPlayingTaskData *startPlayingTaskData = runtime->getVThread().pushTask("MovieElement::startPlayingTask", this, &MovieElement::startPlayingTask);
 		startPlayingTaskData->runtime = runtime;
 
@@ -516,10 +522,6 @@ VThreadState MovieElement::startPlayingTask(const StartPlayingTaskData &taskData
 
 		_shouldPlayIfNotPaused = true;
 		_paused = false;
-
-		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kPlay, 0), DynamicValue(), getSelfReference()));
-		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
-		taskData.runtime->sendMessageOnVThread(dispatch);
 	}
 
 	return kVThreadReturn;
@@ -806,35 +808,35 @@ void MToonElement::playMedia(Runtime *runtime, Project *project) {
 		_celStartTimeMSec = runtime->getPlayTime();
 	}
 
-	if (_rateTimes10000 < 0) {
-		warning("Playing mToons backwards is not implemented yet");
-		_rateTimes10000 = 0; 
-		return;
-	}
+	const bool isReversed = (_rateTimes10000 < 0);
+	uint32 absRateTimes10000;
+	if (isReversed)
+		absRateTimes10000 = -_rateTimes10000;
+	else
+		absRateTimes10000 = _rateTimes10000;
 
 	// Might be possible due to drift?
 	if (playTime < _celStartTimeMSec)
 		return;
 
 	uint64 timeSinceCelStart = playTime - _celStartTimeMSec;
-	uint64 framesAdvanced = timeSinceCelStart * static_cast<uint64>(_rateTimes10000) / static_cast<uint64>(10000000);
+	uint64 framesAdvanced = timeSinceCelStart * static_cast<uint64>(absRateTimes10000) / static_cast<uint64>(10000000);
 
 	if (framesAdvanced > 0) {
-		// This needs to be handled correctly: Reaching the last frame triggers At Last Cel,
-		// but going PAST the last frame triggers automatic stop and pause.
-		// The Obsidian bureau filing cabinets depend on this, since they reset the cel when
-		// reaching the last cel but do not unpause.
+		// This needs to be handled correctly: Reaching the last frame triggers At Last Cel or At First Cel,
+		// but going PAST the end frame triggers automatic stop and pause. The Obsidian bureau filing cabinets
+		// depend on this, since they reset the cel when reaching the last cel but do not unpause.
 		bool ranPastEnd = false;
 
-		size_t framesRemainingToOnePastEnd = (maxFrame + 1) -_frame;
+		size_t framesRemainingToOnePastEnd = isReversed ? (_frame - minFrame + 1) : (maxFrame + 1 - _frame);
 		if (framesRemainingToOnePastEnd <= framesAdvanced) {
 			ranPastEnd = true;
 			if (_loop)
-				targetFrame = minFrame;
+				targetFrame = isReversed ? maxFrame : minFrame;
 			else
-				targetFrame = maxFrame;
+				targetFrame = isReversed ? minFrame : maxFrame;
 		} else
-			targetFrame = _frame + framesAdvanced;
+			targetFrame = isReversed ? (_frame - framesAdvanced) : (_frame + framesAdvanced);
 
 		if (_frame != targetFrame) {
 			_frame = targetFrame;
@@ -855,7 +857,6 @@ void MToonElement::playMedia(Runtime *runtime, Project *project) {
 		if (ranPastEnd && !_loop) {
 			_paused = true;
 
-			// Unlike movies, reaching the end of an mToon fires "Paused" not "Stopped"
 			Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kPause, 0), DynamicValue(), getSelfReference()));
 			Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
 			runtime->queueMessage(dispatch);
@@ -864,7 +865,7 @@ void MToonElement::playMedia(Runtime *runtime, Project *project) {
 		if (_maintainRate)
 			_celStartTimeMSec = playTime;
 		else
-			_celStartTimeMSec += (static_cast<uint64>(10000000) * framesAdvanced) / _rateTimes10000;
+			_celStartTimeMSec += (static_cast<uint64>(10000000) * framesAdvanced) / absRateTimes10000;
 	}
 }
 
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 2e947a39a5f..9c351bed7c8 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -425,10 +425,11 @@ bool ObjectReferenceVariableModifier::load(const PlugInModifierLoaderContext &co
 
 	_setToSourceParentWhen.load(data.setToSourceParentWhen.value.asEvent);
 
-	if (data.objectPath.type != Data::PlugInTypeTaggedValue::kString)
+	if (data.objectPath.type == Data::PlugInTypeTaggedValue::kString)
+		_objectPath = data.objectPath.str;
+	else if (data.objectPath.type != Data::PlugInTypeTaggedValue::kNull)
 		return false;
 
-	_objectPath = data.objectPath.str;
 	_object.reset();
 
 	return true;
diff --git a/engines/mtropolis/plugin/standard_data.cpp b/engines/mtropolis/plugin/standard_data.cpp
index c2b9c8ef2df..b9c5852f890 100644
--- a/engines/mtropolis/plugin/standard_data.cpp
+++ b/engines/mtropolis/plugin/standard_data.cpp
@@ -68,7 +68,13 @@ DataReadErrorCode ObjectReferenceVariableModifier::load(PlugIn &plugIn, const Pl
 	if (prefix.plugInRevision != 2)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!setToSourceParentWhen.load(reader) || !unknown1.load(reader) || !objectPath.load(reader))
+	if (!setToSourceParentWhen.load(reader) || !unknown1.load(reader))
+		return kDataReadErrorReadFailed;
+
+	bool hasNoPath = (unknown1.type == Data::PlugInTypeTaggedValue::kInteger && unknown1.value.asInt == 0);
+	if (hasNoPath)
+		objectPath.type = Data::PlugInTypeTaggedValue::kNull;
+	else if (!objectPath.load(reader))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;


Commit: 2cdd270e2c87c9a68d654db695b3df98186e892b
    https://github.com/scummvm/scummvm/commit/2cdd270e2c87c9a68d654db695b3df98186e892b
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Allow arithmetic on booleans

Changed paths:
    engines/mtropolis/miniscript.cpp


diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 5c916b1d52b..3cb3cde5e59 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -626,6 +626,9 @@ MiniscriptInstructionOutcome BinaryArithInstruction::execute(MiniscriptThread *t
 		case DynamicValueTypes::kFloat:
 			leftVal = lsDest.getFloat();
 			break;
+		case DynamicValueTypes::kBoolean:
+			leftVal = lsDest.getBool() ? 1.0 : 0.0;
+			break;
 		default:
 			thread->error("Invalid left-side type for binary arithmetic operator");
 			return kMiniscriptInstructionOutcomeFailed;
@@ -639,6 +642,9 @@ MiniscriptInstructionOutcome BinaryArithInstruction::execute(MiniscriptThread *t
 		case DynamicValueTypes::kFloat:
 			rightVal = rs.getFloat();
 			break;
+		case DynamicValueTypes::kBoolean:
+			rightVal = rs.getBool() ? 1.0 : 0.0;
+			break;
 		default:
 			thread->error("Invalid right-side type for binary arithmetic operator");
 			return kMiniscriptInstructionOutcomeFailed;


Commit: a9d94c2ed73a43a409204324d1e673050933c6c1
    https://github.com/scummvm/scummvm/commit/a9d94c2ed73a43a409204324d1e673050933c6c1
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix timers not reactivating after termination

Changed paths:
    engines/mtropolis/modifiers.cpp


diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index a60fffec686..5c7e2be2275 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -713,8 +713,10 @@ bool TimerMessengerModifier::respondsToEvent(const Event &evt) const {
 VThreadState TimerMessengerModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
 	// If this terminates AND starts then just cancel out and terminate
 	if (_terminateWhen.respondsTo(msg->getEvent())) {
-		if (_scheduledEvent)
+		if (_scheduledEvent) {
 			_scheduledEvent->cancel();
+			_scheduledEvent.reset();
+		}
 	} else if (_executeWhen.respondsTo(msg->getEvent())) {
 		// 0-time events are not allowed
 		uint32 realMilliseconds = _milliseconds;


Commit: f1a680864e2d8a0a1a154ebd1dfafdc35c9b7839
    https://github.com/scummvm/scummvm/commit/f1a680864e2d8a0a1a154ebd1dfafdc35c9b7839
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix some bugs affecting Spider fire puzzle

Changed paths:
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index a078447ab09..c6bcfaf66d9 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -313,17 +313,18 @@ bool IntRange::load(const Data::IntRange &range) {
 	return true;
 }
 
-bool IntRange::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib) {
+MiniscriptInstructionOutcome IntRange::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib) {
 	if (attrib == "start") {
 		DynamicValueWriteIntegerHelper<int32>::create(&min, proxy);
-		return true;
+		return kMiniscriptInstructionOutcomeContinue;
 	}
-	if (attrib == "y") {
+	if (attrib == "end") {
 		DynamicValueWriteIntegerHelper<int32>::create(&max, proxy);
-		return true;
+		return kMiniscriptInstructionOutcomeContinue;
 	}
 
-	return false;
+	thread->error("Couldn't reference int range attribute '" + attrib + "'");
+	return kMiniscriptInstructionOutcomeFailed;
 }
 
 Common::String IntRange::toString() const {
@@ -796,9 +797,12 @@ Common::Array<ObjectReference> &DynamicList::getObjectReference() {
 
 bool DynamicList::setAtIndex(size_t index, const DynamicValue &value) {
 	if (_type != value.getType()) {
-		if (_container != nullptr && _container->getSize() != 0)
-			return false;
-		else {
+		if (_container != nullptr && _container->getSize() != 0) {
+			DynamicValue converted;
+			if (!value.convertToType(_type, converted))
+				return false;
+			return setAtIndex(index, converted);
+		} else {
 			clear();
 			changeToType(value.getType());
 			return _container->setAtIndex(index, value);
@@ -980,36 +984,35 @@ MiniscriptInstructionOutcome DynamicList::WriteProxyInterface::write(MiniscriptT
 
 MiniscriptInstructionOutcome DynamicList::WriteProxyInterface::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const {
 	DynamicList *list = static_cast<DynamicList *>(objectRef);
-	bool succeeded = false;
+
 	switch (list->getType()) {
 	case DynamicValueTypes::kPoint:
 		list->expandToMinimumSize(ptrOrOffset + 1);
-		succeeded = list->getPoint()[ptrOrOffset].refAttrib(thread, proxy, attrib);
-		break;
+		return list->getPoint()[ptrOrOffset].refAttrib(thread, proxy, attrib);
 	case DynamicValueTypes::kIntegerRange:
 		list->expandToMinimumSize(ptrOrOffset + 1);
-		succeeded = list->getIntRange()[ptrOrOffset].refAttrib(thread, proxy, attrib);
-		break;
+		return list->getIntRange()[ptrOrOffset].refAttrib(thread, proxy, attrib);
 	case DynamicValueTypes::kVector:
 		list->expandToMinimumSize(ptrOrOffset + 1);
-		succeeded = list->getVector()[ptrOrOffset].refAttrib(thread, proxy, attrib);
+		return list->getVector()[ptrOrOffset].refAttrib(thread, proxy, attrib);
 	case DynamicValueTypes::kObject: {
-			if (list->getSize() <= ptrOrOffset)
-			return kMiniscriptInstructionOutcomeFailed;
+			if (list->getSize() <= ptrOrOffset) {
+				return kMiniscriptInstructionOutcomeFailed;
+				}
 
 			Common::SharedPtr<RuntimeObject> obj = list->getObjectReference()[ptrOrOffset].object.lock();
 			proxy.containerList.reset();
-			succeeded = (obj && obj->writeRefAttribute(thread, proxy, attrib));
+			if (!obj) {
+				thread->error("Attempted to reference an attribute of an invalid object reference");
+				return kMiniscriptInstructionOutcomeFailed;
+			}
+
+			return obj->writeRefAttribute(thread, proxy, attrib);
 		} break;
 	default:
-		succeeded = false;
-		break;
+		thread->error("Couldn't reference an attribute of a list element");
+		return kMiniscriptInstructionOutcomeFailed;
 	}
-
-	if (succeeded)
-		return kMiniscriptInstructionOutcomeContinue;
-
-	return kMiniscriptInstructionOutcomeFailed;
 }
 
 MiniscriptInstructionOutcome DynamicList::WriteProxyInterface::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const {
@@ -1383,6 +1386,25 @@ bool DynamicValue::roundToInt(int32 &outInt) const {
 	return false;
 }
 
+bool DynamicValue::convertToType(DynamicValueTypes::DynamicValueType targetType, DynamicValue &result) const {
+	if (_type == targetType) {
+		result = *this;
+		return true;
+	}
+
+	switch (_type) {
+	case DynamicValueTypes::kInteger:
+		return convertIntToType(targetType, result);
+	case DynamicValueTypes::kFloat:
+		return convertFloatToType(targetType, result);
+	case DynamicValueTypes::kBoolean:
+		return convertBoolToType(targetType, result);
+	default:
+		warning("Couldn't convert dynamic value from source type");
+		return false;
+	}
+}
+
 void DynamicValue::setObject(const ObjectReference &value) {
 	if (_type != DynamicValueTypes::kObject)
 		clear();
@@ -1466,6 +1488,63 @@ void DynamicValue::clear() {
 	_type = DynamicValueTypes::kNull;
 }
 
+bool DynamicValue::convertIntToType(DynamicValueTypes::DynamicValueType targetType, DynamicValue &result) const {
+	int32 value = this->getInt();
+
+	switch (targetType) {
+	case DynamicValueTypes::kInteger:
+		result.setInt(value);
+		return true;
+	case DynamicValueTypes::kBoolean:
+		result.setBool(value != 0);
+		return true;
+	case DynamicValueTypes::kFloat:
+		result.setFloat(value);
+		return true;
+	default:
+		warning("Unable to implicitly convert dynamic value");
+		return false;
+	}
+}
+
+bool DynamicValue::convertFloatToType(DynamicValueTypes::DynamicValueType targetType, DynamicValue &result) const {
+	double value = this->getFloat();
+
+	switch (targetType) {
+	case DynamicValueTypes::kInteger:
+		result.setInt(static_cast<int32>(round(value)));
+		return true;
+	case DynamicValueTypes::kBoolean:
+		result.setBool(value != 0.0);
+		return true;
+	case DynamicValueTypes::kFloat:
+		result.setFloat(value);
+		return true;
+	default:
+		warning("Unable to implicitly convert dynamic value");
+		return false;
+	}
+}
+
+bool DynamicValue::convertBoolToType(DynamicValueTypes::DynamicValueType targetType, DynamicValue &result) const {
+	bool value = this->getBool();
+
+	switch (targetType) {
+	case DynamicValueTypes::kInteger:
+		result.setInt(value ? 1 : 0);
+		return true;
+	case DynamicValueTypes::kBoolean:
+		result.setBool(value);
+		return true;
+	case DynamicValueTypes::kFloat:
+		result.setFloat(value ? 1.0 : 0.0);
+		return true;
+	default:
+		warning("Unable to implicitly convert dynamic value");
+		return false;
+	}
+}
+
 void DynamicValue::initFromOther(const DynamicValue &other) {
 	assert(_type == DynamicValueTypes::kNull);
 
@@ -2795,7 +2874,6 @@ Common::WeakPtr<RuntimeObject> ObjectLinkingScope::resolve(uint32 staticGUID) co
 		if (_parent)
 			return _parent->resolve(staticGUID);
 
-		warning("Couldn't resolve static GUID %x", staticGUID);
 		return Common::WeakPtr<RuntimeObject>();
 	}
 }
@@ -2816,7 +2894,6 @@ Common::WeakPtr<RuntimeObject> ObjectLinkingScope::resolve(const Common::String
 		if (_parent)
 			return _parent->resolve(*namePtr, true);
 
-		warning("Couldn't resolve object name '%s'", name.c_str());
 		return Common::WeakPtr<RuntimeObject>();
 	}
 }
@@ -2825,8 +2902,13 @@ Common::WeakPtr<RuntimeObject> ObjectLinkingScope::resolve(uint32 staticGUID, co
 	Common::WeakPtr<RuntimeObject> byGUIDResult = resolve(staticGUID);
 	if (!byGUIDResult.expired())
 		return byGUIDResult;
-	else
-		return resolve(name, isNameAlreadyInsensitive);
+	else {
+		Common::WeakPtr<RuntimeObject> fallback = resolve(name, isNameAlreadyInsensitive);
+		if (fallback.expired()) {
+			warning("Couldn't resolve static guid '%x' with name '%s'", staticGUID, name.c_str());
+		}
+		return fallback;
+	}
 }
 
 void ObjectLinkingScope::reset() {
@@ -6288,13 +6370,26 @@ bool Modifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, con
 
 		return false;
 	}
+	if (attrib == "name") {
+		result.setString(_name);
+		return true;
+	}
 
 	return false;
 
 }
 
+MiniscriptInstructionOutcome Modifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
+	if (attrib == "parent") {
+		DynamicValueWriteObjectHelper::create(_parent.lock().get(), writeProxy);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "name") {
+		DynamicValueWriteStringHelper::create(&_name, writeProxy);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
 
-
+	return RuntimeObject::writeRefAttribute(thread, writeProxy, attrib);
+}
 
 void Modifier::materialize(Runtime *runtime, ObjectLinkingScope *outerScope) {
 	ObjectLinkingScope innerScope;
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index d63129084e4..53568a9da5a 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -347,7 +347,7 @@ struct IntRange {
 		return result;
 	}
 
-	bool refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib);
+	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib);
 	Common::String toString() const;
 };
 
@@ -835,6 +835,8 @@ struct DynamicValue {
 
 	bool roundToInt(int32 &outInt) const;
 
+	bool convertToType(DynamicValueTypes::DynamicValueType targetType, DynamicValue &result) const;
+
 	DynamicValue &operator=(const DynamicValue &other);
 
 	bool operator==(const DynamicValue &other) const;
@@ -866,6 +868,10 @@ private:
 		b = temp;
 	}
 
+	bool convertIntToType(DynamicValueTypes::DynamicValueType targetType, DynamicValue &result) const;
+	bool convertFloatToType(DynamicValueTypes::DynamicValueType targetType, DynamicValue &result) const;
+	bool convertBoolToType(DynamicValueTypes::DynamicValueType targetType, DynamicValue &result) const;
+
 	void initFromOther(const DynamicValue &other);
 
 	DynamicValueTypes::DynamicValueType _type;
@@ -2302,6 +2308,7 @@ public:
 	virtual ~Modifier();
 
 	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) override;
 
 	void materialize(Runtime *runtime, ObjectLinkingScope *outerScope);
 


Commit: 9a3b35deb74db19be81d211d4534b108e4ca9302
    https://github.com/scummvm/scummvm/commit/9a3b35deb74db19be81d211d4534b108e4ca9302
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Don't reject invalid rects in drag motion modifier since they're insets and not actual rects

Changed paths:
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 5c7e2be2275..0c9e99a8092 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -533,7 +533,7 @@ bool DragMotionModifier::load(ModifierLoaderContext &context, const Data::DragMo
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
 
-	if (!_enableWhen.load(data.enableWhen) || !_disableWhen.load(data.disableWhen) || !_constraintMargin.load(data.constraintMargin))
+	if (!_enableWhen.load(data.enableWhen) || !_disableWhen.load(data.disableWhen) || !_constraintMargin.loadUnchecked(data.constraintMargin))
 		return false;
 
 	bool constrainVertical = false;
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index c6bcfaf66d9..2e467ce122c 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -306,6 +306,15 @@ bool Rect16::load(const Data::Rect &rect) {
 	return true;
 }
 
+bool Rect16::loadUnchecked(const Data::Rect &rect) {
+	top = rect.top;
+	left = rect.left;
+	bottom = rect.bottom;
+	right = rect.right;
+
+	return true;
+}
+
 bool IntRange::load(const Data::IntRange &range) {
 	max = range.max;
 	min = range.min;
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 53568a9da5a..3662d528f3d 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -304,6 +304,7 @@ struct Rect16 {
 	int16 right;
 
 	bool load(const Data::Rect &rect);
+	bool loadUnchecked(const Data::Rect &rect);
 
 	inline bool operator==(const Rect16 &other) const {
 		return top == other.top && left == other.left && bottom == other.bottom && right == other.right;


Commit: b3678090aef325804f6481c1f6660697388440cd
    https://github.com/scummvm/scummvm/commit/b3678090aef325804f6481c1f6660697388440cd
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Silently ignore attempts to set position to invalid value

Changed paths:
    engines/mtropolis/runtime.cpp


diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 2e467ce122c..96a3d632e69 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -6140,7 +6140,10 @@ MiniscriptInstructionOutcome VisualElement::scriptSetPosition(MiniscriptThread *
 
 		return kMiniscriptInstructionOutcomeContinue;
 	}
-	return kMiniscriptInstructionOutcomeFailed;
+
+	// Assigning non-point values to position silently fails
+	// Obsidian relies on this behavior due to a bug in the air puzzle completion script
+	return kMiniscriptInstructionOutcomeContinue;
 }
 
 MiniscriptInstructionOutcome VisualElement::scriptSetPositionX(MiniscriptThread *thread, const DynamicValue &dest) {


Commit: f9c294fd83b50a13ffa72408d47c3b8664f79ae9
    https://github.com/scummvm/scummvm/commit/f9c294fd83b50a13ffa72408d47c3b8664f79ae9
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix mToon rate being off by a factor of 10, fix temporal compression detection (kind of)

Changed paths:
    engines/mtropolis/assets.cpp
    engines/mtropolis/assets.h
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h


diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index f3622756e74..59847f35018 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -122,17 +122,27 @@ bool CachedMToon::loadFromStream(const Common::SharedPtr<MToonMetadata> &metadat
 
 	if (metadata->codecID == kMToonRLECodecID) {
 		loadRLEFrames(data);
-		uint16 firstFrameWidth = metadata->frames[0].rect.getWidth();
-		uint16 firstFrameHeight = metadata->frames[0].rect.getHeight();
+		uint16 firstFrameWidth = 0;
+		uint16 firstFrameHeight = 0;
 
+		bool haveAnyTemporalFrames = false;
+		bool haveDifferentDimensions = false;
 		_isRLETemporalCompressed = true;
 		for (size_t i = 0; i < metadata->frames.size(); i++) {
 			const MToonMetadata::FrameDef &frame = metadata->frames[i];
-			if (frame.rect.getWidth() != firstFrameWidth || frame.rect.getHeight() != firstFrameHeight) {
-				_isRLETemporalCompressed = false;
-				break;
+			if (_rleData[i].isKeyframe)
+				haveAnyTemporalFrames = true;
+
+			if (i > 0) {
+				if (_rleData[i].width != _rleData[0].width || _rleData[i].height != _rleData[0].height) {
+					haveDifferentDimensions = true;
+					break;
+				}
 			}
 		}
+
+		if (haveAnyTemporalFrames && !haveDifferentDimensions)
+			_isRLETemporalCompressed = true;
 	}
 
 	if (!_isRLETemporalCompressed)
@@ -155,17 +165,13 @@ void CachedMToon::decompressFrames(const Common::Array<uint8> &data) {
 		}
 	}
 
-	_dataRLE8.clear();
-	_dataRLE16.clear();
-	_dataRLE32.clear();
+	_rleData.clear();
 }
 
-template<class TFrame, class TNumber, uint32 TLiteralMask, uint32 TTransparentRowSkipMask>
-static bool decompressMToonRLE(const TFrame &frame, Graphics::Surface &surface, bool isBottomUp) {
+template<class TNumber, uint32 TLiteralMask, uint32 TTransparentRowSkipMask>
+static bool decompressMToonRLE(const CachedMToon::RleFrame &frame, const Common::Array<TNumber> &coefsArray, Graphics::Surface &surface, bool isBottomUp) {
 	assert(sizeof(TNumber) == surface.format.bytesPerPixel);
 
-	const Common::Array<TNumber> &coefsArray = frame.data;
-
 	size_t size = coefsArray.size();
 	if (size == 0)
 		return false;
@@ -264,11 +270,11 @@ void CachedMToon::decompressRLEFrameToImage(size_t frameIndex, Graphics::Surface
 
 	bool decompressedOK = false;
 	if (_rleOptimizedFormat.bytesPerPixel == 4) {
-		decompressedOK = decompressMToonRLE<Rle32Frame, uint32, 0x80000000u, 0x80000000u>(_dataRLE32[frameIndex], surface, isBottomUp);
+		decompressedOK = decompressMToonRLE<uint32, 0x80000000u, 0x80000000u>(_rleData[frameIndex], _rleData[frameIndex].data32, surface, isBottomUp);
 	} else if (_rleOptimizedFormat.bytesPerPixel == 2) {
-		decompressedOK = decompressMToonRLE<Rle16Frame, uint16, 0x8000u, 0x8000u>(_dataRLE16[frameIndex], surface, isBottomUp);
+		decompressedOK = decompressMToonRLE<uint16, 0x8000u, 0x8000u>(_rleData[frameIndex], _rleData[frameIndex].data16, surface, isBottomUp);
 	} else if (_rleOptimizedFormat.bytesPerPixel == 1) {
-		decompressedOK = decompressMToonRLE<Rle8Frame, uint8, 0x80u, 0u>(_dataRLE8[frameIndex], surface, isBottomUp);
+		decompressedOK = decompressMToonRLE<uint8, 0x80u, 0u>(_rleData[frameIndex], _rleData[frameIndex].data8, surface, isBottomUp);
 	} else
 		error("Unknown mToon encoding");
 
@@ -280,21 +286,12 @@ void CachedMToon::loadRLEFrames(const Common::Array<uint8> &data) {
 	size_t numFrames = _metadata->frames.size();
 	uint16 bpp = _metadata->bitsPerPixel;
 
-	if (bpp == 8)
-		_dataRLE8.resize(numFrames);
-	else if (bpp == 16)
-		_dataRLE16.resize(numFrames);
-	else
-		error("Unsupported RLE encoding");
+	_rleData.resize(numFrames);
 
 	for (size_t i = 0; i < numFrames; i++) {
 		const MToonMetadata::FrameDef &frameDef = _metadata->frames[i];
 
-		RleFrame *rleFrame = nullptr;
-		if (bpp == 8)
-			rleFrame = &_dataRLE8[i];
-		else if (bpp == 16)
-			rleFrame = &_dataRLE16[i];
+		RleFrame &rleFrame = _rleData[i];
 
 		size_t baseOffset = frameDef.dataOffset;
 
@@ -306,7 +303,7 @@ void CachedMToon::loadRLEFrames(const Common::Array<uint8> &data) {
 			headerInts[hi] = unpacked;
 		}
 
-		rleFrame->isKeyframe = (headerInts[0] == kMToonRLEKeyframePrefix);
+		rleFrame.isKeyframe = (headerInts[0] == kMToonRLEKeyframePrefix);
 		if (headerInts[1] == 0x01000001) {
 			if (bpp != 8)
 				error("Unknown mToon encoding");
@@ -316,24 +313,24 @@ void CachedMToon::loadRLEFrames(const Common::Array<uint8> &data) {
 		} else
 			error("Unknown mToon encoding");
 
-		rleFrame->version = headerInts[1];
-		rleFrame->width = headerInts[2];
-		rleFrame->height = headerInts[3];
+		rleFrame.version = headerInts[1];
+		rleFrame.width = headerInts[2];
+		rleFrame.height = headerInts[3];
 
 		uint32 frameDataSize = headerInts[4];
 
 		if (frameDataSize > 0) {
 			if (bpp == 8) {
-				_dataRLE8[i].data.resize(frameDataSize);
-				memcpy(&_dataRLE8[i].data[0], &data[baseOffset + 20], frameDataSize);
+				rleFrame.data8.resize(frameDataSize);
+				memcpy(&rleFrame.data8[0], &data[baseOffset + 20], frameDataSize);
 			} else if (bpp == 16) {
 				// In RLE16, frameDataSize is sometimes set to frameDef.compressedSize but sometimes contains garbage,
 				// so we need to ignore it and derive size from the frameDef instead.
 				uint32 numDWords = (frameDef.compressedSize - 20) / 2;
-				_dataRLE16[i].data.resize(numDWords);
-				memcpy(&_dataRLE16[i].data[0], &data[baseOffset + 20], numDWords * 2);
+				rleFrame.data16.resize(numDWords);
+				memcpy(&rleFrame.data16[0], &data[baseOffset + 20], numDWords * 2);
 
-				uint16 *i16 = &_dataRLE16[i].data[0];
+				uint16 *i16 = &rleFrame.data16[0];
 				if (_metadata->imageFormat == MToonMetadata::kImageFormatWindows) {
 					for (size_t swapIndex = 0; swapIndex < numDWords; swapIndex++)
 						i16[swapIndex] = FROM_LE_16(i16[swapIndex]);
@@ -359,15 +356,9 @@ void CachedMToon::loadRLEFrames(const Common::Array<uint8> &data) {
 void CachedMToon::decompressRLEFrame(size_t frameIndex) {
 	Common::SharedPtr<Graphics::Surface> surface(new Graphics::Surface());
 
-	RleFrame *frame = nullptr;
-	if (_rleInternalFormat.bytesPerPixel == 1)
-		frame = &_dataRLE8[frameIndex];
-	else if (_rleInternalFormat.bytesPerPixel == 2)
-		frame = &_dataRLE16[frameIndex];
-	else
-		error("Unknown mToon encoding");
+	RleFrame &frame = _rleData[frameIndex];
 
-	surface->create(frame->width, frame->height, _rleInternalFormat);
+	surface->create(frame.width, frame.height, _rleInternalFormat);
 
 	decompressRLEFrameToImage(frameIndex, *surface);
 
@@ -437,51 +428,47 @@ void CachedMToon::loadUncompressedFrame(const Common::Array<uint8> &data, size_t
 	_decompressedFrames[frameIndex] = surface;
 }
 
-template<class TSrcFrame, class TSrcNumber, uint32 TSrcLiteralMask, uint32 TSrcTransparentSkipMask, class TDestFrame, class TDestNumber, uint32 TDestLiteralMask, uint32 TDestTransparentSkipMask>
-void CachedMToon::rleReformat(const TSrcFrame &srcFrame, const Graphics::PixelFormat &srcFormatRef, TDestFrame &destFrame, const Graphics::PixelFormat &destFormatRef) {
+template<class TSrcNumber, uint32 TSrcLiteralMask, uint32 TSrcTransparentSkipMask, class TDestNumber, uint32 TDestLiteralMask, uint32 TDestTransparentSkipMask>
+void CachedMToon::rleReformat(RleFrame &frame, const Common::Array<TSrcNumber> &srcData, const Graphics::PixelFormat &srcFormatRef, Common::Array<TDestNumber> &destData, const Graphics::PixelFormat &destFormatRef) {
 	const Graphics::PixelFormat srcFormat = srcFormatRef;
 	const Graphics::PixelFormat destFormat = destFormatRef;
 
 	size_t offset = 0;
-	destFrame.data.resize(srcFrame.data.size());
-	destFrame.width = srcFrame.width;
-	destFrame.height = srcFrame.height;
-	destFrame.isKeyframe = srcFrame.isKeyframe;
-	destFrame.version = srcFrame.version;
+	destData.resize(srcData.size());
 
-	while (offset < srcFrame.data.size()) {
+	while (offset < srcData.size()) {
 		uint32 rleCodeOffset = offset;
-		const uint32 rleCode = srcFrame.data[rleCodeOffset];
+		const uint32 rleCode = srcData[rleCodeOffset];
 		if (rleCode == 0) {
-			destFrame.data[offset] = 0;
+			destData[offset] = 0;
 			offset++;
 
-			uint32 numTransparentCode = srcFrame.data[offset];
+			uint32 numTransparentCode = srcData[offset];
 			if (numTransparentCode & TSrcTransparentSkipMask)
-				destFrame.data[offset] = (numTransparentCode - TSrcTransparentSkipMask) + TDestTransparentSkipMask;
+				destData[offset] = (numTransparentCode - TSrcTransparentSkipMask) + TDestTransparentSkipMask;
 			else
-				destFrame.data[offset] = numTransparentCode;
+				destData[offset] = numTransparentCode;
 			offset++;
 		} else if (rleCode & TSrcLiteralMask) {
 			uint32 numLiterals = rleCode - TSrcLiteralMask;
 
-			destFrame.data[offset] = numLiterals + TDestLiteralMask;
+			destData[offset] = numLiterals + TDestLiteralMask;
 			offset++;
 
 			while (numLiterals) {
 				uint8 a, r, g, b;
-				srcFormat.colorToARGB(srcFrame.data[offset], a, r, g, b);
-				destFrame.data[offset] = destFormat.ARGBToColor(a, r, g, b);
+				srcFormat.colorToARGB(srcData[offset], a, r, g, b);
+				destData[offset] = destFormat.ARGBToColor(a, r, g, b);
 				offset++;
 				numLiterals--;
 			}
 		} else {
-			destFrame.data[offset] = rleCode;
+			destData[offset] = rleCode;
 			offset++;
 
 			uint8 a, r, g, b;
-			srcFormat.colorToARGB(srcFrame.data[offset], a, r, g, b);
-			destFrame.data[offset] = destFormat.ARGBToColor(a, r, g, b);
+			srcFormat.colorToARGB(srcData[offset], a, r, g, b);
+			destData[offset] = destFormat.ARGBToColor(a, r, g, b);
 			offset++;
 		}
 	}
@@ -534,14 +521,14 @@ void CachedMToon::optimizeRLE(const Graphics::PixelFormat &targetFormatRef) {
 	for (size_t i = 0; i < numFrames; i++) {
 		if (_rleInternalFormat.bytesPerPixel == 2) {
 			if (targetFormat.bytesPerPixel == 4)
-				rleReformat<Rle16Frame, uint16, 0x8000u, 0x8000u, Rle32Frame, uint32, 0x80000000u, 0x80000000u>(_dataRLE16[i], _rleInternalFormat, _dataRLE32[i], targetFormat);
+				rleReformat<uint16, 0x8000u, 0x8000u, uint32, 0x80000000u, 0x80000000u>(_rleData[i], _rleData[i].data16, _rleInternalFormat, _rleData[i].data32, targetFormat);
 			else if (targetFormat.bytesPerPixel == 2)
-				rleReformat<Rle16Frame, uint16, 0x8000u, 0x8000u, Rle16Frame, uint16, 0x8000u, 0x8000u>(_dataRLE16[i], _rleInternalFormat, _dataRLE16[i], targetFormat);
+				rleReformat<uint16, 0x8000u, 0x8000u, uint16, 0x8000u, 0x8000u>(_rleData[i], _rleData[i].data16, _rleInternalFormat, _rleData[i].data16, targetFormat);
 		} else if (_rleInternalFormat.bytesPerPixel == 4) {
 			if (targetFormat.bytesPerPixel == 4)
-				rleReformat<Rle32Frame, uint32, 0x80000000u, 0x80000000u, Rle32Frame, uint32, 0x80000000u, 0x80000000u>(_dataRLE32[i], _rleInternalFormat, _dataRLE32[i], targetFormat);
+				rleReformat<uint32, 0x80000000u, 0x80000000u, uint32, 0x80000000u, 0x80000000u>(_rleData[i], _rleData[i].data32, _rleInternalFormat, _rleData[i].data32, targetFormat);
 			else if (targetFormat.bytesPerPixel == 2)
-				rleReformat<Rle32Frame, uint32, 0x80000000u, 0x80000000u, Rle16Frame, uint16, 0x8000u, 0x8000u>(_dataRLE32[i], _rleInternalFormat, _dataRLE16[i], targetFormat);
+				rleReformat<uint32, 0x80000000u, 0x80000000u, uint16, 0x8000u, 0x8000u>(_rleData[i], _rleData[i].data32, _rleInternalFormat, _rleData[i].data16, targetFormat);
 		}
 	}
 
@@ -584,11 +571,11 @@ void CachedMToon::getOrRenderFrame(uint32 prevFrame, uint32 targetFrame, Common:
 
 		for (size_t i = firstFrameToRender; i <= targetFrame; i++) {
 			if (_rleOptimizedFormat.bytesPerPixel == 1)
-				decompressMToonRLE<Rle8Frame, uint8, 0x80u, 0>(_dataRLE8[i], *surface, isBottomUp);
+				decompressMToonRLE<uint8, 0x80u, 0>(_rleData[i], _rleData[i].data8, *surface, isBottomUp);
 			else if (_rleOptimizedFormat.bytesPerPixel == 2)
-				decompressMToonRLE<Rle16Frame, uint16, 0x8000u, 0x8000u>(_dataRLE16[i], *surface, isBottomUp);
+				decompressMToonRLE<uint16, 0x8000u, 0x8000u>(_rleData[i], _rleData[i].data16, *surface, isBottomUp);
 			else if (_rleOptimizedFormat.bytesPerPixel == 4)
-				decompressMToonRLE<Rle32Frame, uint32, 0x80000000u, 0x80000000u>(_dataRLE32[i], *surface, isBottomUp);
+				decompressMToonRLE<uint32, 0x80000000u, 0x80000000u>(_rleData[i], _rleData[i].data32, *surface, isBottomUp);
 		}
 	}
 }
@@ -999,6 +986,7 @@ bool MToonAsset::load(AssetLoaderContext &context, const Data::MToonAsset &data)
 
 	_metadata->bitsPerPixel = data.bitsPerPixel;
 	_metadata->codecID = data.codecID;
+	_metadata->encodingFlags = data.encodingFlags;
 
 	_metadata->frames.resize(data.frames.size());
 	for (size_t i = 0; i < data.frames.size(); i++) {
diff --git a/engines/mtropolis/assets.h b/engines/mtropolis/assets.h
index df3fc7c1c41..df0471d1f3d 100644
--- a/engines/mtropolis/assets.h
+++ b/engines/mtropolis/assets.h
@@ -91,6 +91,7 @@ struct MToonMetadata {
 	Rect16 rect;
 	uint16 bitsPerPixel;
 	uint32 codecID;
+	uint32 encodingFlags;
 
 	Common::Array<FrameDef> frames;
 	Common::Array<FrameRangeDef> frameRanges;
@@ -117,18 +118,9 @@ private:
 		uint32 width;
 		uint32 height;
 		bool isKeyframe;
-	};
-
-	struct Rle32Frame : public RleFrame {
-		Common::Array<uint32> data;
-	};
-
-	struct Rle16Frame : public RleFrame {
-		Common::Array<uint16> data;
-	};
-
-	struct Rle8Frame : public RleFrame {
-		Common::Array<uint8> data;
+		Common::Array<uint8> data8;
+		Common::Array<uint16> data16;
+		Common::Array<uint32> data32;
 	};
 
 	static const uint32 kMToonRLECodecID = 0x2e524c45;
@@ -141,12 +133,10 @@ private:
 	void decompressRLEFrame(size_t frameIndex);
 	void loadUncompressedFrame(const Common::Array<uint8> &data, size_t frameIndex);
 
-	template<class TSrcFrame, class TSrcNumber, uint32 TSrcLiteralMask, uint32 TSrcTransparentSkipMask, class TDestFrame, class TDestNumber, uint32 TDestLiteralMask, uint32 TDestTransparentSkipMask>
-	void rleReformat(const TSrcFrame &srcFrame, const Graphics::PixelFormat &srcFormatRef, TDestFrame &destFrame, const Graphics::PixelFormat &destFormatRef);
+	template<class TSrcNumber, uint32 TSrcLiteralMask, uint32 TSrcTransparentSkipMask, class TDestNumber, uint32 TDestLiteralMask, uint32 TDestTransparentSkipMask>
+	void rleReformat(RleFrame &frame, const Common::Array<TSrcNumber> &srcData, const Graphics::PixelFormat &srcFormatRef, Common::Array<TDestNumber> &destData, const Graphics::PixelFormat &destFormatRef);
 
-	Common::Array<Rle8Frame> _dataRLE8;
-	Common::Array<Rle16Frame> _dataRLE16;
-	Common::Array<Rle32Frame> _dataRLE32;
+	Common::Array<RleFrame> _rleData;
 	bool _isRLETemporalCompressed;
 
 	Common::Array<Common::SharedPtr<Graphics::Surface> > _decompressedFrames;
diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index a6c22b54d3d..f03e3cbf919 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -847,7 +847,7 @@ DataReadErrorCode MToonElement::load(DataReader &reader) {
 			|| !reader.readU16(lengthOfName) || !reader.readU32(elementFlags) || !reader.readU16(layer)
 			|| !reader.readU32(animationFlags) || !reader.readBytes(unknown4) || !reader.readU16(sectionID)
 			|| !rect1.load(reader) || !rect2.load(reader) || !reader.readU32(assetID)
-			|| !reader.readU32(rateTimes10000) || !reader.readU32(streamLocator) || !reader.readU32(unknown6)
+			|| !reader.readU32(rateTimes100000) || !reader.readU32(streamLocator) || !reader.readU32(unknown6)
 			|| !reader.readTerminatedStr(name, lengthOfName))
 		return kDataReadErrorReadFailed;
 
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index afcc49275dd..7f46fd6b82e 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -679,7 +679,7 @@ struct MToonElement : public StructuralDef {
 	Rect rect1;
 	Rect rect2;
 	uint32 assetID;
-	uint32 rateTimes10000;
+	uint32 rateTimes100000;
 	uint32 streamLocator;
 	uint32 unknown6;
 
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index c975d3616f4..944025b621a 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -648,7 +648,7 @@ bool MToonElement::load(ElementLoaderContext &context, const Data::MToonElement
 	_maintainRate = ((data.elementFlags & Data::AnimationFlags::kPlayEveryFrame) == 0);	// NOTE: Inverted intentionally
 	_assetID = data.assetID;
 	_runtime = context.runtime;
-	_rateTimes10000 = data.rateTimes10000;
+	_rateTimes100000 = data.rateTimes100000;
 
 	return true;
 }
@@ -661,7 +661,7 @@ bool MToonElement::readAttribute(MiniscriptThread *thread, DynamicValue &result,
 		result.setInt(_flushPriority);
 		return true;
 	} else if (attrib == "rate") {
-		result.setFloat(_rateTimes10000 / 10000.0);
+		result.setFloat(_rateTimes100000 / 100000.0);
 		return true;
 	} else if (attrib == "range") {
 		result.setIntRange(_playRange);
@@ -808,19 +808,21 @@ void MToonElement::playMedia(Runtime *runtime, Project *project) {
 		_celStartTimeMSec = runtime->getPlayTime();
 	}
 
-	const bool isReversed = (_rateTimes10000 < 0);
-	uint32 absRateTimes10000;
+	const bool isReversed = (_rateTimes100000 < 0);
+	uint32 absRateTimes100000;
 	if (isReversed)
-		absRateTimes10000 = -_rateTimes10000;
+		absRateTimes100000 = -_rateTimes100000;
 	else
-		absRateTimes10000 = _rateTimes10000;
+		absRateTimes100000 = _rateTimes100000;
 
 	// Might be possible due to drift?
 	if (playTime < _celStartTimeMSec)
 		return;
 
 	uint64 timeSinceCelStart = playTime - _celStartTimeMSec;
-	uint64 framesAdvanced = timeSinceCelStart * static_cast<uint64>(absRateTimes10000) / static_cast<uint64>(10000000);
+	uint64 framesAdvanced = timeSinceCelStart * static_cast<uint64>(absRateTimes100000) / static_cast<uint64>(100000000);
+
+	debug(3, "mToon frames advanced in %i msec since %i at rate %i: %i", static_cast<int>(timeSinceCelStart), static_cast<int>(_celStartTimeMSec), static_cast<int>(absRateTimes100000), static_cast<int>(framesAdvanced));
 
 	if (framesAdvanced > 0) {
 		// This needs to be handled correctly: Reaching the last frame triggers At Last Cel or At First Cel,
@@ -865,7 +867,7 @@ void MToonElement::playMedia(Runtime *runtime, Project *project) {
 		if (_maintainRate)
 			_celStartTimeMSec = playTime;
 		else
-			_celStartTimeMSec += (static_cast<uint64>(10000000) * framesAdvanced) / absRateTimes10000;
+			_celStartTimeMSec += (static_cast<uint64>(100000000) * framesAdvanced) / absRateTimes100000;
 	}
 }
 
@@ -941,10 +943,10 @@ void MToonElement::onPauseStateChanged() {
 MiniscriptInstructionOutcome MToonElement::scriptSetRate(MiniscriptThread *thread, const DynamicValue &value) {
 	switch (value.getType()) {
 	case DynamicValueTypes::kFloat:
-		_rateTimes10000 = static_cast<int32>(round(value.getFloat()) * 10000.0);
+		_rateTimes100000 = static_cast<int32>(round(value.getFloat()) * 100000.0);
 		break;
 	case DynamicValueTypes::kInteger:
-		_rateTimes10000 = value.getInt() * 10000;
+		_rateTimes100000 = value.getInt() * 100000;
 		break;
 	default:
 		thread->error("Invalid type for Miniscript rate");
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index 61560a3b4a1..663f75f3d96 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -217,7 +217,7 @@ private:
 	bool _maintainRate;
 
 	uint32 _assetID;
-	int32 _rateTimes10000;
+	int32 _rateTimes100000;
 	uint32 _frame;
 	int32 _flushPriority;
 	uint32 _celStartTimeMSec;


Commit: 1050da2a12852799a47715e1e94ecb21183108ee
    https://github.com/scummvm/scummvm/commit/1050da2a12852799a47715e1e94ecb21183108ee
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Text rendering and Obsidian WordMixer

Changed paths:
    engines/mtropolis/assets.cpp
    engines/mtropolis/assets.h
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/miniscript.h
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/plugin/obsidian.cpp
    engines/mtropolis/plugin/obsidian.h
    engines/mtropolis/plugin/obsidian_data.cpp
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/render.cpp
    engines/mtropolis/render.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index 59847f35018..7723430607e 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -22,6 +22,7 @@
 #include "mtropolis/assets.h"
 #include "mtropolis/asset_factory.h"
 
+#include "graphics/managed_surface.h"
 #include "graphics/surface.h"
 
 #include "audio/audiostream.h"
@@ -1078,7 +1079,7 @@ bool TextAsset::load(AssetLoaderContext &context, const Data::TextAsset &data) {
 		if (!_bitmapRect.load(data.bitmapRect))
 			return false;
 
-		_bitmapData.reset(new Graphics::Surface());
+		_bitmapData.reset(new Graphics::ManagedSurface());
 
 		uint16 width = _bitmapRect.getWidth();
 		uint16 height = _bitmapRect.getHeight();
@@ -1132,7 +1133,7 @@ bool TextAsset::isBitmap() const {
 	return _isBitmap;
 }
 
-const Common::SharedPtr<Graphics::Surface>& TextAsset::getBitmapSurface() const {
+const Common::SharedPtr<Graphics::ManagedSurface>& TextAsset::getBitmapSurface() const {
 	return _bitmapData;
 }
 
diff --git a/engines/mtropolis/assets.h b/engines/mtropolis/assets.h
index df0471d1f3d..c1b11a51958 100644
--- a/engines/mtropolis/assets.h
+++ b/engines/mtropolis/assets.h
@@ -283,7 +283,7 @@ public:
 	AssetType getAssetType() const override;
 
 	bool isBitmap() const;
-	const Common::SharedPtr<Graphics::Surface> &getBitmapSurface() const;
+	const Common::SharedPtr<Graphics::ManagedSurface> &getBitmapSurface() const;
 	const Common::String &getString() const;
 	const Common::Array<MacFormattingSpan> &getMacFormattingSpans() const;
 
@@ -292,7 +292,7 @@ private:
 	TextAlignment _alignment;
 	bool _isBitmap;
 
-	Common::SharedPtr<Graphics::Surface> _bitmapData;
+	Common::SharedPtr<Graphics::ManagedSurface> _bitmapData;
 	Common::String _stringData;
 
 	Common::Array<MacFormattingSpan> _macFormattingSpans;
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 944025b621a..f46d1736126 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -29,6 +29,10 @@
 #include "video/qt_decoder.h"
 
 #include "common/substream.h"
+
+#include "graphics/macgui/macfontmanager.h"
+#include "graphics/fontman.h"
+#include "graphics/font.h"
 #include "graphics/managed_surface.h"
 
 namespace MTropolis {
@@ -958,12 +962,16 @@ MiniscriptInstructionOutcome MToonElement::scriptSetRate(MiniscriptThread *threa
 }
 
 
-TextLabelElement::TextLabelElement() : _needsRender(false), _isBitmap(false) {
+TextLabelElement::TextLabelElement() : _needsRender(false), _isBitmap(false), _macFontID(0), _size(12), _alignment(kTextAlignmentLeft) {
 }
 
 TextLabelElement::~TextLabelElement() {
 }
 
+bool TextLabelElement::isTextLabel() const {
+	return true;
+}
+
 bool TextLabelElement::load(ElementLoaderContext &context, const Data::TextLabelElement &data) {
 	if (!loadCommon(data.name, data.guid, data.rect1, data.elementFlags, data.layer, 0, data.sectionID))
 		return false;
@@ -976,13 +984,62 @@ bool TextLabelElement::load(ElementLoaderContext &context, const Data::TextLabel
 }
 
 bool TextLabelElement::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "text") {
+		result.setString(_text);
+		return true;
+	}
+
 	return VisualElement::readAttribute(thread, result, attrib);
 }
 
+bool TextLabelElement::readAttributeIndexed(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib, const DynamicValue &index) {
+	if (attrib == "line") {
+		int32 asInteger = 0;
+		if (!index.roundToInt(asInteger) || asInteger < 1) {
+			thread->error("Invalid text label line index");
+			return false;
+		}
+
+		size_t lineIndex = asInteger - 1;
+		uint32 startPos;
+		uint32 endPos;
+		if (findLineRange(lineIndex, startPos, endPos))
+			result.setString(_text.substr(startPos, endPos - startPos));
+		else
+			result.setString("");
+
+		return true;
+	}
+
+	return VisualElement::readAttributeIndexed(thread, result, attrib, index);
+}
+
 MiniscriptInstructionOutcome TextLabelElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
+	if (attrib == "text") {
+		DynamicValueWriteFuncHelper<TextLabelElement, &TextLabelElement::scriptSetText>::create(this, writeProxy);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
 	return VisualElement::writeRefAttribute(thread, writeProxy, attrib);
 }
 
+MiniscriptInstructionOutcome TextLabelElement::writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib, const DynamicValue &index) {
+	if (attrib == "line") {
+		int32 asInteger = 0;
+		if (!index.roundToInt(asInteger) || asInteger < 1) {
+			thread->error("Invalid text label line set index");
+			return kMiniscriptInstructionOutcomeFailed;
+		}
+
+		writeProxy.pod.ifc = &TextLabelLineWriteInterface::_instance;
+		writeProxy.pod.objectRef = this;
+		writeProxy.pod.ptrOrOffset = asInteger - 1;
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	return VisualElement::writeRefAttributeIndexed(thread, writeProxy, attrib, index);
+}
+
 void TextLabelElement::activate() {
 	Project *project = _runtime->getProject();
 	Common::SharedPtr<Asset> asset = project->getAssetByID(_assetID).lock();
@@ -1012,10 +1069,251 @@ void TextLabelElement::activate() {
 void TextLabelElement::deactivate() {
 }
 
-
 void TextLabelElement::render(Window *window) {
+	if (!_visible)
+		return;
+
+	int renderWidth = _rect.getWidth();
+	int renderHeight = _rect.getHeight();
+	if (_renderedText) {
+		if (renderWidth != _renderedText->w || renderHeight != _renderedText->h)
+			_needsRender = true;
+	}
+
+	if (_needsRender) {
+		_needsRender = false;
+
+		_renderedText.reset();
+		_renderedText.reset(new Graphics::ManagedSurface());
+
+		_renderedText->create(renderWidth, renderHeight, Graphics::PixelFormat::createFormatCLUT8());
+		_renderedText->fillRect(Common::Rect(0, 0, renderWidth, renderHeight), 0);
+
+		const Graphics::Font *font = nullptr;
+		if (_fontFamilyName.size() > 0) {
+			font = FontMan.getFontByName(_fontFamilyName.c_str());
+		} else if (_macFontID != 0) {
+			// TODO: Formatting spans
+			int slant = 0;
+			// FIXME/HACK: These aren't public...
+			if (_styleFlags.bold)
+				slant |= 1;
+			if (_styleFlags.italic)
+				slant |= 2;
+			if (_styleFlags.underline)
+				slant |= 4;
+			if (_styleFlags.outline)
+				slant |= 8;
+			if (_styleFlags.shadow)
+				slant |= 16;
+			if (_styleFlags.condensed)
+				slant |= 32;
+			if (_styleFlags.expanded)
+				slant |= 64;
+
+			// FIXME/HACK: This is a stupid way to make getFont return null on failure
+			font = _runtime->getMacFontManager()->getFont(Graphics::MacFont(_macFontID, _size, slant, static_cast<Graphics::FontManager::FontUsage>(-1)));
+		}
+
+		if (!font)
+			font = FontMan.getFontByUsage(Graphics::FontManager::kConsoleFont);
+
+		int height = font->getFontHeight();
+		int ascent = font->getFontAscent();
+
+		Graphics::TextAlign textAlign = Graphics::kTextAlignLeft;
+		switch (_alignment) {
+		case kTextAlignmentLeft:
+			textAlign = Graphics::kTextAlignLeft;
+			break;
+		case kTextAlignmentCenter:
+			textAlign = Graphics::kTextAlignCenter;
+			break;
+		case kTextAlignmentRight:
+			textAlign = Graphics::kTextAlignRight;
+			break;
+		default:
+			break;
+		}
+
+		int line = 0;
+		uint32 lineStart = 0;
+		while (lineStart < _text.size()) {
+			bool noMoreLines = false;
+			uint32 lineEndPos = _text.find('\r', lineStart);
+			if (lineEndPos == Common::String::npos) {
+				lineEndPos = _text.size();
+				noMoreLines = true;
+			}
+
+			Common::String lineStr;
+			if (lineStart != 0 || lineEndPos != _text.size())
+				lineStr = _text.substr(lineStart, lineEndPos - lineStart);
+			else
+				lineStr = _text;
+
+			// Split the line into sublines
+			while (lineStr.size() > 0) {
+				size_t lineCommitted = 0;
+				bool prevWasWhitespace = true;
+				bool hasWhitespaceAtStart = false;
+				for (size_t i = 0; i <= lineStr.size(); i++) {
+					bool isWhitespace = (i == lineStr.size() || lineStr[i] < ' ');
+
+					if (isWhitespace) {
+						if (!prevWasWhitespace) {
+							int width = font->getStringWidth(lineStr.substr(0, i));
+							if (width > renderWidth)
+								break;
+						}
+						lineCommitted = i + 1;
+					}
+
+					prevWasWhitespace = isWhitespace;
+				}
+
+				if (lineCommitted > lineStr.size())
+					lineCommitted = lineStr.size();
+
+				// Too little space for anything
+				if (lineCommitted == 0) {
+					lineCommitted = 1;
+					for (size_t i = 2; i <= lineStr.size(); i++) {
+						int width = font->getStringWidth(lineStr.substr(0, i));
+						if (width > renderWidth)
+							break;
+						lineCommitted = i;
+					}
+				}
+
+				font->drawString(_renderedText.get(), lineStr.substr(0, lineCommitted), 0, line * height + (height - ascent) / 2, renderWidth, 1, textAlign, 0, false);
+
+				if (lineCommitted == lineStr.size())
+					lineStr.clear();
+				else {
+					lineStr = lineStr.substr(lineCommitted);
+					line++;
+				}
+			}
+
+			if (noMoreLines)
+				break;
+
+			line++;
+			lineStart = lineEndPos + 1;
+		}
+	}
+
+	Graphics::ManagedSurface *target = window->getSurface().get();
+	Common::Rect srcRect(0, 0, renderWidth, renderHeight);
+	Common::Rect destRect(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y, _cachedAbsoluteOrigin.x + _rect.getWidth(), _cachedAbsoluteOrigin.y + _rect.getHeight());
+
+	const uint32 opaqueColor = 0xff000000;
+	const uint32 drawPalette[2] = {0, opaqueColor};
+
+	if (_renderedText) {
+		_renderedText->setPalette(drawPalette, 0, 2);
+		target->transBlitFrom(*_renderedText.get(), srcRect, destRect, 0);
+	}
+}
+
+void TextLabelElement::setTextStyle(uint16 macFontID, const Common::String &fontFamilyName, uint size, TextAlignment alignment, const TextStyleFlags &styleFlags) {
+	_needsRender = true;
+
+	_macFontID = macFontID;
+	_fontFamilyName = fontFamilyName;
+	_size = size;
+	_alignment = alignment;
+	_styleFlags = styleFlags;
+}
+
+MiniscriptInstructionOutcome TextLabelElement::scriptSetText(MiniscriptThread *thread, const DynamicValue &value) {
+	if (value.getType() != DynamicValueTypes::kString) {
+		thread->error("Tried to set a text label element's text to something that wasn't a string");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	_text = value.getString();
+	_needsRender = true;
+	_macFormattingSpans.clear();
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+
+MiniscriptInstructionOutcome TextLabelElement::scriptSetLine(MiniscriptThread *thread, size_t lineIndex, const DynamicValue &value) {
+	if (value.getType() != DynamicValueTypes::kString) {
+		thread->error("Tried to set a text label element's text to something that wasn't a string");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	uint32 startPos;
+	uint32 endPos;
+	if (findLineRange(lineIndex, startPos, endPos))
+		_text = _text.substr(0, startPos) + value.getString() + _text.substr(endPos, _text.size() - endPos);
+	else {
+		size_t numLines = countLines();
+		while (numLines <= lineIndex) {
+			_text += '\r';
+			numLines++;
+		}
+		_text += value.getString();
+	}
+
+	_needsRender = true;
+	_macFormattingSpans.clear();
+	
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+bool TextLabelElement::findLineRange(size_t lineIndex, uint32 &outStartPos, uint32 &outEndPos) const {
+	uint32 lineStart = 0;
+	uint32 lineEnd = _text.size();
+	size_t linesToScan = lineIndex + 1;
+
+	while (linesToScan > 0) {
+		linesToScan--;
+
+		lineEnd = _text.find('\r', lineStart);
+		if (lineEnd == Common::String::npos) {
+			lineEnd = _text.size();
+			break;
+		}
+	}
+
+	if (linesToScan > 0)
+		return false;
+
+	outStartPos = lineStart;
+	outEndPos = lineEnd;
+
+	return true;
 }
 
+size_t TextLabelElement::countLines() const {
+	size_t numLines = 1;
+	for (char c : _text)
+		if (c == '\r')
+			numLines++;
+
+	return numLines;
+}
+
+MiniscriptInstructionOutcome TextLabelElement::TextLabelLineWriteInterface::write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr ptrOrOffset) const {
+	return static_cast<TextLabelElement *>(objectRef)->scriptSetLine(thread, ptrOrOffset, dest);
+}
+
+MiniscriptInstructionOutcome TextLabelElement::TextLabelLineWriteInterface::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const {
+	return kMiniscriptInstructionOutcomeFailed;
+}
+
+MiniscriptInstructionOutcome TextLabelElement::TextLabelLineWriteInterface::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const {
+	return kMiniscriptInstructionOutcomeFailed;
+}
+
+TextLabelElement::TextLabelLineWriteInterface TextLabelElement::TextLabelLineWriteInterface::_instance;
+
+
 SoundElement::SoundElement() : _finishTime(0), _shouldPlayIfNotPaused(true), _needsReset(true) {
 }
 
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index 663f75f3d96..37f75194d7e 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -239,22 +239,42 @@ public:
 	TextLabelElement();
 	~TextLabelElement();
 
+	bool isTextLabel() const override;
+
 	bool load(ElementLoaderContext &context, const Data::TextLabelElement &data);
 
 	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
+	bool readAttributeIndexed(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib, const DynamicValue &index) override;
 	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
+	MiniscriptInstructionOutcome writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib, const DynamicValue &index) override;
 
 	void activate() override;
 	void deactivate() override;
 
 	void render(Window *window) override;
 
+	void setTextStyle(uint16 macFontID, const Common::String &fontFamilyName, uint size, TextAlignment alignment, const TextStyleFlags &styleFlags);
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Text Label Element"; }
 	SupportStatus debugGetSupportStatus() const override { return kSupportStatusPartial; }
 #endif
 
 private:
+	struct TextLabelLineWriteInterface : public IDynamicValueWriteInterface {
+		MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr ptrOrOffset) const override;
+		MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const override;
+		MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
+
+		static TextLabelLineWriteInterface _instance;
+	};
+
+	MiniscriptInstructionOutcome scriptSetText(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetLine(MiniscriptThread *thread, size_t lineIndex, const DynamicValue &value);
+
+	bool findLineRange(size_t lineNum, uint32 &outStartPos, uint32 &outEndPos) const;
+	size_t countLines() const;
+
 	bool _cacheBitmap;
 	bool _needsRender;
 
@@ -262,8 +282,18 @@ private:
 	uint32 _assetID;
 
 	Common::String _text;
+	uint16 _macFontID;
+	Common::String _fontFamilyName;
+	uint _size;
+	TextAlignment _alignment;
+	TextStyleFlags _styleFlags;
+
 	Common::Array<MacFormattingSpan> _macFormattingSpans;
-	Common::SharedPtr<Graphics::Surface> _renderedText;	// NOTE: This may be a pre-rendered instance that is read-only.  Rendering must create a new surface!
+
+	// NOTE: This may be a surface loaded from data, so it must not be altered.
+	// If you need to render again, recreate the surface.  If you want to change
+	// this behavior, please add a flag indicating that it is from the asset.
+	Common::SharedPtr<Graphics::ManagedSurface> _renderedText;
 
 	Runtime *_runtime;
 };
@@ -275,8 +305,8 @@ public:
 
 	bool load(ElementLoaderContext &context, const Data::SoundElement &data);
 
-	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
-	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) override;
 
 	VThreadState consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg);
 
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 3cb3cde5e59..4f16d94797c 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -484,7 +484,7 @@ MiniscriptInstructionOutcome Set::execute(MiniscriptThread *thread) const {
 	}
 
 	// Convert value
-	MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0, false);
+	MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0, true);
 	if (outcome != kMiniscriptInstructionOutcomeContinue)
 		return outcome;
 
@@ -1679,18 +1679,26 @@ PushGlobal::PushGlobal(uint32 globalID, bool isLValue) : _globalID(globalID), _i
 }
 
 MiniscriptInstructionOutcome PushGlobal::execute(MiniscriptThread *thread) const {
-	DynamicValue value;
+	thread->pushValue(DynamicValue());
+
+	DynamicValue &value = thread->getStackValueFromTop(0).value;
+
 	switch (_globalID) {
 	case kGlobalRefElement:
 	case kGlobalRefSection:
 	case kGlobalRefScene:
 	case kGlobalRefProject:
-		return executeFindFilteredParent(thread);
+		return executeFindFilteredParent(thread, value);
 	case kGlobalRefModifier:
 		value.setObject(thread->getModifier()->getSelfReference());
 		break;
 	case kGlobalRefIncomingData:
-		value = thread->getMessageProperties()->getValue();
+		if (_isLValue) {
+			DynamicValueWriteProxy proxy;
+			thread->createWriteIncomingDataProxy(proxy);
+			value.setWriteProxy(proxy);
+		} else
+			value = thread->getMessageProperties()->getValue();
 		break;
 	case kGlobalRefSource:
 		value.setObject(ObjectReference(thread->getMessageProperties()->getSource()));
@@ -1713,12 +1721,10 @@ MiniscriptInstructionOutcome PushGlobal::execute(MiniscriptThread *thread) const
 		return kMiniscriptInstructionOutcomeFailed;
 	}
 
-	thread->pushValue(value);
-
 	return kMiniscriptInstructionOutcomeContinue;
 }
 
-MiniscriptInstructionOutcome PushGlobal::executeFindFilteredParent(MiniscriptThread *thread) const {
+MiniscriptInstructionOutcome PushGlobal::executeFindFilteredParent(MiniscriptThread *thread, DynamicValue &result) const {
 	Common::WeakPtr<RuntimeObject> ref = thread->getModifier()->getSelfReference();
 	for (;;) {
 		Common::SharedPtr<RuntimeObject> obj = ref.lock();
@@ -1762,10 +1768,7 @@ MiniscriptInstructionOutcome PushGlobal::executeFindFilteredParent(MiniscriptThr
 		}
 	}
 
-	DynamicValue value;
-	value.setObject(ref);
-
-	thread->pushValue(value);
+	result.setObject(ref);
 
 	return kMiniscriptInstructionOutcomeContinue;
 }
@@ -1927,6 +1930,31 @@ bool MiniscriptThread::evaluateTruthOfResult(bool &isTrue) {
 	return true;
 }
 
+void MiniscriptThread::createWriteIncomingDataProxy(DynamicValueWriteProxy &proxy) {
+	proxy.pod.ifc = &IncomingDataWriteInterface::_instance;
+	proxy.pod.objectRef = this;
+	proxy.pod.ptrOrOffset = 0;
+}
+
+MiniscriptInstructionOutcome MiniscriptThread::IncomingDataWriteInterface::write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr ptrOrOffset) const {
+	thread->_msgProps->setValue(value);
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome MiniscriptThread::IncomingDataWriteInterface::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const {
+	// TODO: Generic refAttrib for dynamic values
+	return kMiniscriptInstructionOutcomeFailed;
+}
+
+MiniscriptInstructionOutcome MiniscriptThread::IncomingDataWriteInterface::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const {
+	// TODO: Generic refAttribIndexed for dynamic values
+	return kMiniscriptInstructionOutcomeFailed;
+}
+
+MiniscriptThread::IncomingDataWriteInterface MiniscriptThread::IncomingDataWriteInterface::_instance;
+
+
 VThreadState MiniscriptThread::resumeTask(const ResumeTaskData &data) {
 	return data.thread->resume(data);
 }
diff --git a/engines/mtropolis/miniscript.h b/engines/mtropolis/miniscript.h
index 6d7ada6ccfc..790ea6f500d 100644
--- a/engines/mtropolis/miniscript.h
+++ b/engines/mtropolis/miniscript.h
@@ -351,7 +351,7 @@ namespace MiniscriptInstructions {
 			kGlobalRefActiveScene = 11,
 		};
 
-		MiniscriptInstructionOutcome executeFindFilteredParent(MiniscriptThread *thread) const;
+		MiniscriptInstructionOutcome executeFindFilteredParent(MiniscriptThread *thread, DynamicValue &result) const;
 
 		uint32 _globalID;
 		bool _isLValue;
@@ -409,7 +409,17 @@ public:
 
 	bool evaluateTruthOfResult(bool &isTrue);
 
+	void createWriteIncomingDataProxy(DynamicValueWriteProxy &proxy);
+
 private:
+	struct IncomingDataWriteInterface : public IDynamicValueWriteInterface {
+		MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr ptrOrOffset) const override;
+		MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const override;
+		MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
+
+		static IncomingDataWriteInterface _instance;
+	};
+
 	struct ResumeTaskData {
 		Common::SharedPtr<MiniscriptThread> thread;
 	};
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 0c9e99a8092..dd307efff2e 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -24,6 +24,8 @@
 #include "mtropolis/modifier_factory.h"
 #include "mtropolis/saveload.h"
 
+#include "mtropolis/elements.h"
+
 #include "common/memstream.h"
 
 namespace MTropolis {
@@ -321,7 +323,7 @@ bool MessengerModifier::respondsToEvent(const Event &evt) const {
 
 VThreadState MessengerModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
 	if (_when.respondsTo(msg->getEvent())) {
-		_sendSpec.sendFromMessenger(runtime, this);
+		_sendSpec.sendFromMessenger(runtime, this, msg->getValue());
 	}
 
 	return kVThreadReturn;
@@ -645,6 +647,7 @@ VThreadState IfMessengerModifier::consumeMessage(Runtime *runtime, const Common:
 		EvaluateAndSendTaskData *evalAndSendData = runtime->getVThread().pushTask("IfMessengerModifier::evaluateAndSendTask", this, &IfMessengerModifier::evaluateAndSendTask);
 		evalAndSendData->thread = thread;
 		evalAndSendData->runtime = runtime;
+		evalAndSendData->incomingData = msg->getValue();
 
 		MiniscriptThread::runOnVThread(runtime->getVThread(), thread);
 	}
@@ -680,7 +683,7 @@ VThreadState IfMessengerModifier::evaluateAndSendTask(const EvaluateAndSendTaskD
 		return kVThreadError;
 
 	if (isTrue)
-		_sendSpec.sendFromMessenger(taskData.runtime, this);
+		_sendSpec.sendFromMessenger(taskData.runtime, this, taskData.incomingData);
 
 	return kVThreadReturn;
 }
@@ -726,6 +729,9 @@ VThreadState TimerMessengerModifier::consumeMessage(Runtime *runtime, const Comm
 		debug(3, "Timer %x '%s' scheduled to execute in %i milliseconds", getStaticGUID(), getName().c_str(), realMilliseconds);
 		if (!_scheduledEvent) {
 			_scheduledEvent = runtime->getScheduler().scheduleMethod<TimerMessengerModifier, &TimerMessengerModifier::trigger>(runtime->getPlayTime() + realMilliseconds, this);
+			_incomingData = msg->getValue();
+			if (_incomingData.getType() == DynamicValueTypes::kList)
+				_incomingData.setList(_incomingData.getList()->clone());
 		}
 	}
 
@@ -757,7 +763,7 @@ void TimerMessengerModifier::trigger(Runtime *runtime) {
 	} else
 		_scheduledEvent.reset();
 
-	_sendSpec.sendFromMessenger(runtime, this);
+	_sendSpec.sendFromMessenger(runtime, this, _incomingData);
 }
 
 bool BoundaryDetectionMessengerModifier::load(ModifierLoaderContext &context, const Data::BoundaryDetectionMessengerModifier &data) {
@@ -1014,16 +1020,13 @@ bool KeyboardMessengerModifier::checkKeyEventTrigger(Runtime *runtime, Common::E
 
 void KeyboardMessengerModifier::dispatchMessage(Runtime *runtime, const Common::String &charStr) {
 	Common::SharedPtr<MessageProperties> msgProps;
-	if (_sendSpec.with.getType() == DynamicValueTypes::kIncomingData) {
-		if (charStr.size() != 1)
-			warning("Keyboard messenger is supposed to send the character code, but they key was a special key and we haven't implemented conversion of those keycodes");
 
-		DynamicValue charStrValue;
-		charStrValue.setString(charStr);
-		_sendSpec.sendFromMessengerWithCustomData(runtime, this, charStrValue);
-	} else {
-		_sendSpec.sendFromMessenger(runtime, this);
-	}
+	if (charStr.size() != 1)
+		warning("Keyboard messenger is supposed to send the character code, but they key was a special key and we haven't implemented conversion of those keycodes");
+
+	DynamicValue charStrValue;
+	charStrValue.setString(charStr);
+	_sendSpec.sendFromMessenger(runtime, this, charStrValue);
 }
 
 void KeyboardMessengerModifier::visitInternalReferences(IStructuralReferenceVisitor *visitor) {
@@ -1034,20 +1037,6 @@ void KeyboardMessengerModifier::linkInternalReferences(ObjectLinkingScope *scope
 	_sendSpec.linkInternalReferences(scope);
 }
 
-TextStyleModifier::StyleFlags::StyleFlags() : bold(false), italic(false), underline(false), outline(false), shadow(false), condensed(false), expanded(false) {
-}
-
-bool TextStyleModifier::StyleFlags::load(uint8 dataStyleFlags) {
-	bold = ((dataStyleFlags & 0x01) != 0);
-	italic = ((dataStyleFlags & 0x02) != 0);
-	underline = ((dataStyleFlags & 0x03) != 0);
-	outline = ((dataStyleFlags & 0x04) != 0);
-	shadow = ((dataStyleFlags & 0x10) != 0);
-	condensed = ((dataStyleFlags & 0x20) != 0);
-	expanded = ((dataStyleFlags & 0x40) != 0);
-	return true;
-}
-
 bool TextStyleModifier::load(ModifierLoaderContext &context, const Data::TextStyleModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -1057,12 +1046,59 @@ bool TextStyleModifier::load(ModifierLoaderContext &context, const Data::TextSty
 
 	_macFontID = data.macFontID;
 	_size = data.size;
-	_alignment = static_cast<Alignment>(data.alignment);
 	_fontFamilyName = data.fontFamilyName;
 
+	if (!_styleFlags.load(data.flags))
+		return false;
+
+	switch (data.alignment) {
+	case 0:
+		_alignment = kTextAlignmentLeft;
+		break;
+	case 1:
+		_alignment = kTextAlignmentCenter;
+		break;
+	case 0xffff:
+		_alignment = kTextAlignmentRight;
+		break;
+	default:
+		warning("Unrecognized text alignment");
+		return false;
+	}
+
 	return true;
 }
 
+bool TextStyleModifier::respondsToEvent(const Event &evt) const {
+	if (_applyWhen.respondsTo(evt) || _removeWhen.respondsTo(evt))
+		return true;
+
+	return Modifier::respondsToEvent(evt);
+}
+
+VThreadState TextStyleModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	if (_applyWhen.respondsTo(msg->getEvent())) {
+		Structural *owner = findStructuralOwner();
+		if (owner && owner->isElement()) {
+			Element *element = static_cast<Element *>(owner);
+			if (element->isVisual()) {
+				VisualElement *visualElement = static_cast<VisualElement *>(element);
+				if (visualElement->isTextLabel()) {
+					static_cast<TextLabelElement *>(visualElement)->setTextStyle(_macFontID, _fontFamilyName, _size, _alignment, _styleFlags);
+				}
+			}
+		}
+
+		return kVThreadReturn;
+	} else if (_removeWhen.respondsTo(msg->getEvent())) {
+		// Doesn't actually do anything
+		return kVThreadReturn;
+	}
+
+	return Modifier::consumeMessage(runtime, msg);
+}
+
+
 Common::SharedPtr<Modifier> TextStyleModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new TextStyleModifier(*this));
 }
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index dd00a49fe7c..ed387fb3391 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -22,6 +22,7 @@
 #ifndef MTROPOLIS_MODIFIERS_H
 #define MTROPOLIS_MODIFIERS_H
 
+#include "mtropolis/render.h"
 #include "mtropolis/runtime.h"
 #include "mtropolis/data.h"
 
@@ -349,6 +350,7 @@ private:
 	struct EvaluateAndSendTaskData {
 		Common::SharedPtr<MiniscriptThread> thread;
 		Runtime *runtime;
+		DynamicValue incomingData;
 	};
 
 	Common::SharedPtr<Modifier> shallowClone() const override;
@@ -391,6 +393,7 @@ private:
 	MessengerSendSpec _sendSpec;
 	uint32 _milliseconds;
 	bool _looping;
+	DynamicValue _incomingData;
 
 	Common::SharedPtr<ScheduledEvent> _scheduledEvent;
 };
@@ -521,24 +524,8 @@ class TextStyleModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::TextStyleModifier &data);
 
-	enum Alignment {
-		kAlignmentLeft = 0,
-		kAlignmentCenter = 1,
-		kAlignmentRight = 0xffff,
-	};
-
-	struct StyleFlags {
-		bool bold : 1;
-		bool italic : 1;
-		bool underline : 1;
-		bool outline : 1;
-		bool shadow : 1;
-		bool condensed : 1;
-		bool expanded : 1;
-
-		StyleFlags();
-		bool load(uint8 dataStyleFlags);
-	};
+	bool respondsToEvent(const Event &evt) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Text Style Modifier"; }
@@ -551,7 +538,8 @@ private:
 	uint16 _size;
 	ColorRGB8 _textColor;
 	ColorRGB8 _backgroundColor;
-	Alignment _alignment;
+	TextAlignment _alignment;
+	TextStyleFlags _styleFlags;
 	Event _applyWhen;
 	Event _removeWhen;
 	Common::String _fontFamilyName;
diff --git a/engines/mtropolis/plugin/obsidian.cpp b/engines/mtropolis/plugin/obsidian.cpp
index dd911fe56be..92dac2eae1d 100644
--- a/engines/mtropolis/plugin/obsidian.cpp
+++ b/engines/mtropolis/plugin/obsidian.cpp
@@ -85,8 +85,18 @@ bool TextWorkModifier::readAttribute(MiniscriptThread *thread, DynamicValue &res
 		result.setInt(index);
 		return true;
 	} else if (attrib == "numword") {
-		thread->error("Not-yet-implemented TextWork attribute 'numword'");
-		return false;
+		int numWords = 0;
+		bool lastWasWhitespace = true;
+		for (size_t i = 0; i < _string.size(); i++) {
+			char c = _string[i];
+			bool isWhitespace = (c <= ' ');
+			if (lastWasWhitespace && !isWhitespace)
+				numWords++;
+			lastWasWhitespace = isWhitespace;
+		}
+
+		result.setInt(numWords);
+		return true;
 	}
 
 	return Modifier::readAttribute(thread, result, attrib);
@@ -106,11 +116,11 @@ MiniscriptInstructionOutcome TextWorkModifier::writeRefAttribute(MiniscriptThrea
 		DynamicValueWriteStringHelper::create(&_token, result);
 		return kMiniscriptInstructionOutcomeContinue;
 	} else if (attrib == "firstword") {
-		thread->error("Not-yet-implemented TextWork attrib 'firstword'");
-		return kMiniscriptInstructionOutcomeFailed;
+		DynamicValueWriteFuncHelper<TextWorkModifier, &TextWorkModifier::scriptSetFirstWord>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
 	} else if (attrib == "lastword") {
-		thread->error("Not-yet-implemented TextWork attrib 'lastword'");
-		return kMiniscriptInstructionOutcomeFailed;
+		DynamicValueWriteFuncHelper<TextWorkModifier, &TextWorkModifier::scriptSetLastWord>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
 	}
 
 	return Modifier::writeRefAttribute(thread, result, attrib);
@@ -120,6 +130,75 @@ Common::SharedPtr<Modifier> TextWorkModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new TextWorkModifier(*this));
 }
 
+MiniscriptInstructionOutcome TextWorkModifier::scriptSetFirstWord(MiniscriptThread *thread, const DynamicValue &value) {
+	// This and lastword are only used in tandem with lastword, exact functionality is unclear since it's
+	// also used in tandem with "output" which is normally used with firstchar+lastchar.
+	// We attempt to emulate it by setting firstchar+lastchar to the correct values
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger))
+		return kMiniscriptInstructionOutcomeFailed;
+
+	int numWords = 0;
+	bool lastWasWhitespace = true;
+	for (size_t i = 0; i < _string.size(); i++) {
+		char c = _string[i];
+		bool isWhitespace = (c <= ' ');
+		if (lastWasWhitespace && !isWhitespace) {
+			numWords++;
+
+			if (numWords == asInteger) {
+				_firstChar = i + 1;
+				return kMiniscriptInstructionOutcomeContinue;
+			}
+		}
+		lastWasWhitespace = isWhitespace;
+
+	}
+
+	thread->error("Invalid index for 'firstword'");
+	return kMiniscriptInstructionOutcomeFailed;
+}
+
+MiniscriptInstructionOutcome TextWorkModifier::scriptSetLastWord(MiniscriptThread* thread, const DynamicValue& value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger))
+		return kMiniscriptInstructionOutcomeFailed;
+
+	int numWordEnds = 0;
+	bool lastWasWhitespace = true;
+	for (size_t i = 0; i < _string.size(); i++) {
+		char c = _string[i];
+		bool isWhitespace = (c <= ' ');
+		if (!lastWasWhitespace && isWhitespace) {
+			numWordEnds++;
+
+			if (numWordEnds == asInteger) {
+				_firstChar = i - 1;
+				return kMiniscriptInstructionOutcomeContinue;
+			}
+		}
+		lastWasWhitespace = isWhitespace;
+
+		if (numWordEnds == asInteger) {
+			_lastChar = i;
+			return kMiniscriptInstructionOutcomeContinue;
+		}
+	}
+
+	if (!lastWasWhitespace) {
+		numWordEnds++;
+		if (numWordEnds == asInteger) {
+			_lastChar = _string.size();
+			return kMiniscriptInstructionOutcomeContinue;
+		}
+	}
+
+	thread->error("Invalid index for 'firstword'");
+	return kMiniscriptInstructionOutcomeFailed;
+}
+
+
+
 DictionaryModifier::DictionaryModifier() : _plugIn(nullptr) {
 }
 
@@ -257,13 +336,155 @@ Common::SharedPtr<Modifier> DictionaryModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new DictionaryModifier(*this));
 }
 
-WordMixerModifier::WordMixerModifier() {
+WordMixerModifier::WordMixerModifier() : _matches(0), _result(0) {
 }
 
 bool WordMixerModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::WordMixerModifier &data) {
+	_plugIn = static_cast<const ObsidianPlugIn *>(context.plugIn);
+
 	return true;
 }
 
+bool WordMixerModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "result") {
+		result.setInt(_result);
+		return true;
+	}
+	if (attrib == "matches") {
+		result.setInt(_matches);
+		return true;
+	}
+	if (attrib == "output") {
+		result.setString(_output);
+		return true;
+	}
+
+	return Modifier::readAttribute(thread, result, attrib);
+}
+
+MiniscriptInstructionOutcome WordMixerModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
+	if (attrib == "input") {
+		DynamicValueWriteFuncHelper<WordMixerModifier, &WordMixerModifier::scriptSetInput>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+	if (attrib == "search") {
+		DynamicValueWriteFuncHelper<WordMixerModifier, &WordMixerModifier::scriptSetSearch>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	return Modifier::writeRefAttribute(thread, result, attrib);
+}
+
+MiniscriptInstructionOutcome WordMixerModifier::scriptSetInput(MiniscriptThread *thread, const DynamicValue &value) {
+	if (value.getType() != DynamicValueTypes::kString) {
+		thread->error("Invalid type for WordMixer input attribute");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	_input = value.getString();
+
+	Common::Array<char> sourceChars;
+	for (char c : _input) {
+		if (c > ' ')
+			sourceChars.push_back(invariantToLower(c));
+	}
+
+	Common::Array<bool> charIsUsed;
+	charIsUsed.resize(sourceChars.size());
+
+	const Common::Array<WordGameData::WordBucket> &wordBuckets = _plugIn->getWordGameData()->getWordBuckets();
+
+	_output.clear();
+	_matches = 0;
+
+	size_t numWordBuckets = wordBuckets.size();
+	for (size_t rbucket = 0; rbucket < numWordBuckets; rbucket++) {
+		size_t wordLength = numWordBuckets - 1 - rbucket;
+
+		const WordGameData::WordBucket &bucket = wordBuckets[wordLength];
+
+		size_t numWords = bucket.wordIndexes.size();
+
+		for (size_t wi = 0; wi < numWords; wi++) {
+			const char *wordChars = &bucket.chars[bucket.spacing * wi];
+
+			for (bool &b : charIsUsed)
+				b = false;
+
+			bool isMatch = true;
+			for (size_t ci = 0; ci < wordLength; ci++) {
+				const char wordChar = wordChars[ci];
+
+				bool foundAvailableSource = false;
+				for (size_t srci = 0; srci < sourceChars.size(); srci++) {
+					if (sourceChars[srci] == wordChar && !charIsUsed[srci]) {
+						foundAvailableSource = true;
+						charIsUsed[srci] = true;
+						break;
+					}
+				}
+
+				if (!foundAvailableSource) {
+					isMatch = false;
+					break;
+				}
+			}
+
+			if (isMatch) {
+				if (_matches > 0)
+					_output += ' ';
+				_output += Common::String(wordChars, wordLength);
+				_matches++;
+			}
+		}
+
+		if (_matches > 0)
+			break;
+	}
+
+	if (_matches == 0)
+		_output = "xxx";
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome WordMixerModifier::scriptSetSearch(MiniscriptThread *thread, const DynamicValue &value) {
+	if (value.getType() != DynamicValueTypes::kBoolean) {
+		thread->error("Invalid type for WordMixer search attribute");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	if (!value.getBool())
+		return kMiniscriptInstructionOutcomeContinue;
+
+	size_t searchLength = _input.size();
+	const Common::Array<WordGameData::WordBucket> &buckets = _plugIn->getWordGameData()->getWordBuckets();
+	_result = 0;
+	if (searchLength < buckets.size()) {
+		const WordGameData::WordBucket &bucket = buckets[searchLength];
+
+		bool found = false;
+		for (size_t wi = 0; wi < bucket.wordIndexes.size(); wi++) {
+			const char *wordChars = &bucket.chars[wi * bucket.spacing];
+
+			bool isMatch = true;
+			for (size_t ci = 0; ci < searchLength; ci++) {
+				if (invariantToLower(_input[ci]) != wordChars[ci]) {
+					isMatch = false;
+					break;
+				}
+			}
+
+			if (isMatch) {
+				_result = 1;
+				break;
+			}
+		}
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 Common::SharedPtr<Modifier> WordMixerModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new WordMixerModifier(*this));
 }
diff --git a/engines/mtropolis/plugin/obsidian.h b/engines/mtropolis/plugin/obsidian.h
index f1397961eab..624d895e469 100644
--- a/engines/mtropolis/plugin/obsidian.h
+++ b/engines/mtropolis/plugin/obsidian.h
@@ -74,6 +74,9 @@ public:
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 
+	MiniscriptInstructionOutcome scriptSetFirstWord(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetLastWord(MiniscriptThread *thread, const DynamicValue &value);
+
 	Common::String _string;
 	Common::String _token;
 
@@ -115,12 +118,25 @@ public:
 
 	bool load(const PlugInModifierLoaderContext &context, const Data::Obsidian::WordMixerModifier &data);
 
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib);
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "WordMixer Modifier"; }
 #endif
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+
+	MiniscriptInstructionOutcome scriptSetInput(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetSearch(MiniscriptThread *thread, const DynamicValue &value);
+
+	Common::String _input;
+	Common::String _output;
+	int _matches;
+	int _result;
+
+	const ObsidianPlugIn *_plugIn;
 };
 
 class ObsidianPlugIn : public MTropolis::PlugIn {
diff --git a/engines/mtropolis/plugin/obsidian_data.cpp b/engines/mtropolis/plugin/obsidian_data.cpp
index 3608c44ae57..f779f3eac84 100644
--- a/engines/mtropolis/plugin/obsidian_data.cpp
+++ b/engines/mtropolis/plugin/obsidian_data.cpp
@@ -66,9 +66,13 @@ DataReadErrorCode TextWorkModifier::load(PlugIn& plugIn, const PlugInModifier& p
 }
 
 DataReadErrorCode WordMixerModifier::load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) {
-	if (prefix.plugInRevision != 1)
+	if (prefix.plugInRevision != 0)
 		return kDataReadErrorUnsupportedRevision;
 
+	// Looks like this contains matches, but don't really need them...
+	if (!reader.skip(prefix.subObjectSize))
+		return kDataReadErrorReadFailed;
+
 	return kDataReadErrorNone;
 }
 
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 9c351bed7c8..6070688ba8d 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -963,8 +963,11 @@ Common::SharedPtr<ModifierSaveLoad> ListVariableModifier::getSaveLoad() {
 bool ListVariableModifier::varSetValue(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kList)
 		_list = value.getList()->clone();
-	else
-		return false;
+	else {
+		if (!_list)
+			_list.reset(new DynamicList());
+		return _list->setAtIndex(0, value);
+	}
 
 	return true;
 }
diff --git a/engines/mtropolis/render.cpp b/engines/mtropolis/render.cpp
index e8ee471d74e..25ef49db6ec 100644
--- a/engines/mtropolis/render.cpp
+++ b/engines/mtropolis/render.cpp
@@ -78,6 +78,20 @@ inline int expand5To8(int value) {
 	return (value * 33) >> 2;
 }
 
+TextStyleFlags::TextStyleFlags() : bold(false), italic(false), underline(false), outline(false), shadow(false), condensed(false), expanded(false) {
+}
+
+bool TextStyleFlags::load(uint8 dataStyleFlags) {
+	bold = ((dataStyleFlags & 0x01) != 0);
+	italic = ((dataStyleFlags & 0x02) != 0);
+	underline = ((dataStyleFlags & 0x03) != 0);
+	outline = ((dataStyleFlags & 0x04) != 0);
+	shadow = ((dataStyleFlags & 0x10) != 0);
+	condensed = ((dataStyleFlags & 0x20) != 0);
+	expanded = ((dataStyleFlags & 0x40) != 0);
+	return true;
+}
+
 MacFontFormatting::MacFontFormatting() : fontID(0), fontFlags(0), size(12) {
 }
 
diff --git a/engines/mtropolis/render.h b/engines/mtropolis/render.h
index 3e5f9f0f8e7..3bb45f6bd86 100644
--- a/engines/mtropolis/render.h
+++ b/engines/mtropolis/render.h
@@ -47,6 +47,19 @@ enum TextAlignment {
 	kTextAlignmentRight,
 };
 
+struct TextStyleFlags {
+	bool bold : 1;
+	bool italic : 1;
+	bool underline : 1;
+	bool outline : 1;
+	bool shadow : 1;
+	bool condensed : 1;
+	bool expanded : 1;
+
+	TextStyleFlags();
+	bool load(uint8 dataStyleFlags);
+};
+
 struct MacFontFormatting {
 	MacFontFormatting();
 	MacFontFormatting(uint16 fontID, uint8 fontFlags, uint16 size);
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 96a3d632e69..bf892dc5d02 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -1559,7 +1559,7 @@ void DynamicValue::initFromOther(const DynamicValue &other) {
 
 	switch (other._type) {
 	case DynamicValueTypes::kNull:
-	case DynamicValueTypes::kIncomingData:
+	case DynamicValueTypes::kIncomingData:	// FIXME: Get rid of this
 		break;
 	case DynamicValueTypes::kInteger:
 		_value.asInt = other._value.asInt;
@@ -1860,8 +1860,11 @@ void MessengerSendSpec::resolveVariableObjectType(RuntimeObject *obj, Common::We
 	}
 }
 
-void MessengerSendSpec::sendFromMessenger(Runtime *runtime, Modifier *sender) const {
-	sendFromMessengerWithCustomData(runtime, sender, this->with);
+void MessengerSendSpec::sendFromMessenger(Runtime *runtime, Modifier *sender, const DynamicValue &incomingData) const {
+	if (this->with.getType() == DynamicValueTypes::kIncomingData)
+		sendFromMessengerWithCustomData(runtime, sender, incomingData);
+	else
+		sendFromMessengerWithCustomData(runtime, sender, this->with);
 }
 
 void MessengerSendSpec::sendFromMessengerWithCustomData(Runtime *runtime, Modifier *sender, const DynamicValue &data) const {
@@ -2230,6 +2233,13 @@ const Common::WeakPtr<RuntimeObject>& MessageProperties::getSource() const {
 	return _source;
 }
 
+void MessageProperties::setValue(const DynamicValue &value) {
+	if (value.getType() == DynamicValueTypes::kList)
+		_value.setList(value.getList()->clone());
+	else
+		_value = value;
+}
+
 WorldManagerInterface::WorldManagerInterface() {
 }
 
@@ -5986,6 +5996,10 @@ bool VisualElement::isVisual() const {
 	return true;
 }
 
+bool VisualElement::isTextLabel() const {
+	return false;
+}
+
 bool VisualElement::isVisible() const {
 	return _visible;
 }
@@ -6575,6 +6589,20 @@ bool Modifier::loadTypicalHeader(const Data::TypicalModifierHeader &typicalHeade
 	return true;
 }
 
+Structural *Modifier::findStructuralOwner() const {
+	RuntimeObject *scan = _parent.lock().get();
+	while (scan) {
+		if (scan->isModifier())
+			scan = static_cast<Modifier *>(scan)->_parent.lock().get();
+		else if (scan->isStructural())
+			return static_cast<Structural *>(scan);
+		else
+			return nullptr;
+	}
+
+	return nullptr;
+}
+
 void Modifier::linkInternalReferences(ObjectLinkingScope *scope) {
 }
 
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 3662d528f3d..36e837bbded 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -1047,7 +1047,7 @@ struct MessengerSendSpec {
 
 	static void resolveVariableObjectType(RuntimeObject *obj, Common::WeakPtr<Structural> &outStructuralDest, Common::WeakPtr<Modifier> &outModifierDest);
 
-	void sendFromMessenger(Runtime *runtime, Modifier *sender) const;
+	void sendFromMessenger(Runtime *runtime, Modifier *sender, const DynamicValue &incomingData) const;
 	void sendFromMessengerWithCustomData(Runtime *runtime, Modifier *sender, const DynamicValue &data) const;
 
 	Event send;
@@ -1769,6 +1769,8 @@ struct MessageProperties {
 	const DynamicValue &getValue() const;
 	const Common::WeakPtr<RuntimeObject> &getSource() const;
 
+	void setValue(const DynamicValue &value);
+
 private:
 	Event _evt;
 	DynamicValue _value;
@@ -2212,6 +2214,7 @@ public:
 	VisualElement();
 
 	bool isVisual() const override;
+	virtual bool isTextLabel() const;
 
 	bool isVisible() const;
 	bool isDirectToScreen() const;


Commit: c487268ab309e31c79b0d5439f375269bf0447fd
    https://github.com/scummvm/scummvm/commit/c487268ab309e31c79b0d5439f375269bf0447fd
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix some things that were spamming errors a lot

Changed paths:
    engines/mtropolis/assets.cpp
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/plugin/standard.cpp


diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index 7723430607e..98d3b306d90 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -732,7 +732,14 @@ const Common::SharedPtr<Graphics::Surface> &CachedImage::optimize(Runtime *runti
 			_optimizedSurface = _surface;	// Can't optimize
 		}
 	} else {
-		_surface->convertToInPlace(renderFmt, nullptr);
+		static const byte bwPalette[6] = {255, 255, 255, 0, 0, 0};
+
+		const byte *palette = nullptr;
+
+		if (_colorDepth == kColorDepthMode16Bit)
+			palette = bwPalette;
+
+		_surface->convertToInPlace(renderFmt, palette);
 		_optimizedSurface = _surface;
 	}
 
@@ -844,19 +851,19 @@ const Common::SharedPtr<CachedImage> &ImageAsset::loadAndCacheImage(Runtime *run
 	Graphics::PixelFormat pixelFmt;
 	switch (getColorDepth()) {
 	case kColorDepthMode1Bit:
-		bytesPerRow = (width + 7) / 8;
+		bytesPerRow = (width + 31) / 32 * 4;
 		pixelFmt = Graphics::PixelFormat::createFormatCLUT8();
 		break;
 	case kColorDepthMode2Bit:
-		bytesPerRow = (width + 3) / 4;
+		bytesPerRow = (width + 15) / 16 * 4;
 		pixelFmt = Graphics::PixelFormat::createFormatCLUT8();
 		break;
 	case kColorDepthMode4Bit:
-		bytesPerRow = (width + 1) / 2;
+		bytesPerRow = (width + 7) / 8 * 4;
 		pixelFmt = Graphics::PixelFormat::createFormatCLUT8();
 		break;
 	case kColorDepthMode8Bit:
-		bytesPerRow = width;
+		bytesPerRow = (width + 3) / 4 * 4;
 		pixelFmt = Graphics::PixelFormat::createFormatCLUT8();
 		break;
 	case kColorDepthMode16Bit:
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index f46d1736126..580c5143eff 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -203,6 +203,7 @@ bool MovieElement::load(ElementLoaderContext &context, const Data::MovieElement
 	_alternate = ((data.animationFlags & Data::AnimationFlags::kAlternate) != 0);
 	_playEveryFrame = ((data.animationFlags & Data::AnimationFlags::kPlayEveryFrame) != 0);
 	_assetID = data.assetID;
+	_volume = data.volume;
 
 	_runtime = context.runtime;
 
@@ -223,6 +224,10 @@ MiniscriptInstructionOutcome MovieElement::writeRefAttribute(MiniscriptThread *t
 		DynamicValueWriteOrRefAttribFuncHelper<MovieElement, &MovieElement::scriptSetRange, &MovieElement::scriptRangeWriteRefAttribute>::create(this, result);
 		return kMiniscriptInstructionOutcomeContinue;
 	}
+	if (attrib == "volume") {
+		DynamicValueWriteFuncHelper<MovieElement, &MovieElement::scriptSetVolume>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
 
 	return VisualElement::writeRefAttribute(thread, result, attrib);
 }
@@ -276,6 +281,7 @@ void MovieElement::activate() {
 
 	Video::QuickTimeDecoder *qtDecoder = new Video::QuickTimeDecoder();
 	qtDecoder->setChunkBeginOffset(movieAsset->getMovieDataPos());
+	qtDecoder->setVolume(_volume * 255 / 100);
 
 	_videoDecoder.reset(qtDecoder);
 
@@ -451,6 +457,25 @@ MiniscriptInstructionOutcome MovieElement::scriptSetRange(MiniscriptThread *thre
 	return scriptSetRangeTyped(thread, value.getIntRange());
 }
 
+MiniscriptInstructionOutcome MovieElement::scriptSetVolume(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger)) {
+		thread->error("Wrong type for movie element range");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	if (asInteger < 0)
+		asInteger = 0;
+	else if (asInteger > 100)
+		asInteger = 100;
+
+	_volume = asInteger;
+	if (_videoDecoder)
+		_videoDecoder->setVolume(_volume * 255 / 100);
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 MiniscriptInstructionOutcome MovieElement::scriptSetRangeStart(MiniscriptThread *thread, const DynamicValue &value) {
 	int32 asInteger = 0;
 	if (!value.roundToInt(asInteger)) {
@@ -826,8 +851,6 @@ void MToonElement::playMedia(Runtime *runtime, Project *project) {
 	uint64 timeSinceCelStart = playTime - _celStartTimeMSec;
 	uint64 framesAdvanced = timeSinceCelStart * static_cast<uint64>(absRateTimes100000) / static_cast<uint64>(100000000);
 
-	debug(3, "mToon frames advanced in %i msec since %i at rate %i: %i", static_cast<int>(timeSinceCelStart), static_cast<int>(_celStartTimeMSec), static_cast<int>(absRateTimes100000), static_cast<int>(framesAdvanced));
-
 	if (framesAdvanced > 0) {
 		// This needs to be handled correctly: Reaching the last frame triggers At Last Cel or At First Cel,
 		// but going PAST the end frame triggers automatic stop and pause. The Obsidian bureau filing cabinets
@@ -1156,7 +1179,6 @@ void TextLabelElement::render(Window *window) {
 			while (lineStr.size() > 0) {
 				size_t lineCommitted = 0;
 				bool prevWasWhitespace = true;
-				bool hasWhitespaceAtStart = false;
 				for (size_t i = 0; i <= lineStr.size(); i++) {
 					bool isWhitespace = (i == lineStr.size() || lineStr[i] < ' ');
 
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index 37f75194d7e..397996dcb98 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -98,6 +98,7 @@ private:
 	MiniscriptInstructionOutcome scriptSetRangeStart(MiniscriptThread *thread, const DynamicValue &value);
 	MiniscriptInstructionOutcome scriptSetRangeEnd(MiniscriptThread *thread, const DynamicValue &value);
 	MiniscriptInstructionOutcome scriptRangeWriteRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib);
+	MiniscriptInstructionOutcome scriptSetVolume(MiniscriptThread *thread, const DynamicValue &value);
 
 	MiniscriptInstructionOutcome scriptSetRangeTyped(MiniscriptThread *thread, const IntRange &range);
 
@@ -128,6 +129,7 @@ private:
 	uint32 _maxTimestamp;
 	uint32 _timeScale;
 	uint32 _currentTimestamp;
+	int32 _volume;
 	IntRange _playRange;
 
 	const Graphics::Surface *_displayFrame;
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 6070688ba8d..a787b8b2de5 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -814,10 +814,10 @@ VThreadState MidiModifier::consumeMessage(Runtime *runtime, const Common::Shared
 		}
 	}
 	if (_terminateWhen.respondsTo(msg->getEvent())) {
-#ifdef MTROPOLIS_DEBUG_ENABLE
-		if (Debugger *debugger = runtime->debugGetDebugger())
-			debugger->notify(kDebugSeverityWarning, "MIDI player ordered to terminate, which isn't supported yet");
-#endif
+		if (_filePlayer) {
+			_plugIn->getMidi()->deleteFilePlayer(_filePlayer);
+			_filePlayer = nullptr;
+		}
 	}
 
 	return kVThreadReturn;


Commit: fee75068d1b840af1f8581e3800395a0f25d325d
    https://github.com/scummvm/scummvm/commit/fee75068d1b840af1f8581e3800395a0f25d325d
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix temporal mToon sizes

Changed paths:
    engines/mtropolis/elements.cpp


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 580c5143eff..c32136b1716 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -778,12 +778,25 @@ void MToonElement::render(Window *window) {
 
 		_cachedMToon->getOrRenderFrame(_renderedFrame, _frame, _renderSurface);
 		_renderedFrame = _frame;
-	}
 
-	if (_renderSurface) {
-		Common::Rect srcRect(_renderSurface->w, _renderSurface->h);
-		Common::Rect destRect(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y, _cachedAbsoluteOrigin.x + _rect.getWidth(), _cachedAbsoluteOrigin.y + _rect.getHeight());
-		window->getSurface()->blitFrom(*_renderSurface, srcRect, destRect);
+		Rect16 frameRect = _metadata->frames[_frame].rect;
+
+		if (_renderSurface) {
+			Common::Rect srcRect;
+			Common::Rect destRect;
+
+			if (frameRect.getWidth() == _renderSurface->w && frameRect.getHeight() == _renderSurface->h) {
+				// Frame rect is the size of the render surface, meaning the frame rect is an offset
+				srcRect = Common::Rect(0, 0, frameRect.getWidth(), frameRect.getHeight());
+				destRect = Common::Rect(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y, _cachedAbsoluteOrigin.x + frameRect.getWidth(), _cachedAbsoluteOrigin.y + frameRect.getHeight());
+			} else {
+				// Frame rect is a sub-area of the rendered rect
+				srcRect = Common::Rect(frameRect.left, frameRect.top, frameRect.right, frameRect.bottom);
+				destRect = Common::Rect(_cachedAbsoluteOrigin.x + frameRect.left, _cachedAbsoluteOrigin.y + frameRect.left, _cachedAbsoluteOrigin.x + frameRect.right, _cachedAbsoluteOrigin.y + frameRect.bottom);
+			}
+
+			window->getSurface()->blitFrom(*_renderSurface, srcRect, destRect);
+		}
 	}
 }
 


Commit: 2dc4c8232ba401abd1c460f26ca5f093c96570f2
    https://github.com/scummvm/scummvm/commit/2dc4c8232ba401abd1c460f26ca5f093c96570f2
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Drag motion modifier

Changed paths:
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index dd307efff2e..0e7b6633f1d 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -535,17 +535,19 @@ bool DragMotionModifier::load(ModifierLoaderContext &context, const Data::DragMo
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
 
-	if (!_enableWhen.load(data.enableWhen) || !_disableWhen.load(data.disableWhen) || !_constraintMargin.loadUnchecked(data.constraintMargin))
+	_dragProps.reset(new DragMotionProperties());
+
+	if (!_enableWhen.load(data.enableWhen) || !_disableWhen.load(data.disableWhen) || !_dragProps->constraintMargin.loadUnchecked(data.constraintMargin))
 		return false;
 
 	bool constrainVertical = false;
 	bool constrainHorizontal = false;
 	if (data.haveMacPart) {
-		_constrainToParent = ((data.platform.mac.flags & Data::DragMotionModifier::MacPart::kConstrainToParent) != 0);
+		_dragProps->constrainToParent = ((data.platform.mac.flags & Data::DragMotionModifier::MacPart::kConstrainToParent) != 0);
 		constrainVertical = ((data.platform.mac.flags & Data::DragMotionModifier::MacPart::kConstrainHorizontal) != 0);
 		constrainHorizontal = ((data.platform.mac.flags & Data::DragMotionModifier::MacPart::kConstrainVertical) != 0);
 	} else if (data.haveWinPart) {
-		_constrainToParent = (data.platform.win.constrainToParent != 0);
+		_dragProps->constrainToParent = (data.platform.win.constrainToParent != 0);
 		constrainVertical = (data.platform.win.constrainVertical != 0);
 		constrainHorizontal = (data.platform.win.constrainHorizontal != 0);
 	} else {
@@ -556,19 +558,42 @@ bool DragMotionModifier::load(ModifierLoaderContext &context, const Data::DragMo
 		if (constrainHorizontal)
 			return false;	// ???
 		else
-			_constraintDirection = kConstraintDirectionVertical;
+			_dragProps->constraintDirection = kConstraintDirectionVertical;
 	} else {
 		if (constrainHorizontal)
-			_constraintDirection = kConstraintDirectionHorizontal;
+			_dragProps->constraintDirection = kConstraintDirectionHorizontal;
 		else
-			_constraintDirection = kConstraintDirectionNone;
+			_dragProps->constraintDirection = kConstraintDirectionNone;
 	}
 
 	return true;
 }
 
 Common::SharedPtr<Modifier> DragMotionModifier::shallowClone() const {
-	return Common::SharedPtr<Modifier>(new DragMotionModifier(*this));
+	Common::SharedPtr<DragMotionModifier> clone = Common::SharedPtr<DragMotionModifier>(new DragMotionModifier(*this));
+	clone->_dragProps.reset(new DragMotionProperties(*_dragProps));
+	return clone;
+}
+
+bool DragMotionModifier::respondsToEvent(const Event &evt) const {
+	return _enableWhen.respondsTo(evt) || _disableWhen.respondsTo(evt);
+}
+
+VThreadState DragMotionModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	if (_enableWhen.respondsTo(msg->getEvent())) {
+		Structural *owner = this->findStructuralOwner();
+		if (owner->isElement() && static_cast<Element *>(owner)->isVisual())
+			static_cast<VisualElement *>(owner)->setDragMotionProperties(_dragProps);
+		return kVThreadReturn;
+	}
+	if (_disableWhen.respondsTo(msg->getEvent())) {
+		Structural *owner = this->findStructuralOwner();
+		if (owner->isElement() && static_cast<Element *>(owner)->isVisual())
+			static_cast<VisualElement *>(owner)->setDragMotionProperties(nullptr);
+		return kVThreadReturn;
+	}
+
+	return kVThreadReturn;
 }
 
 bool VectorMotionModifier::load(ModifierLoaderContext &context, const Data::VectorMotionModifier &data) {
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index ed387fb3391..8157c51f6da 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -244,6 +244,9 @@ class DragMotionModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::DragMotionModifier &data);
 
+	bool respondsToEvent(const Event &evt) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Drag Motion Modifier"; }
 #endif
@@ -254,15 +257,7 @@ private:
 	Event _enableWhen;
 	Event _disableWhen;
 
-	enum ConstraintDirection {
-		kConstraintDirectionNone,
-		kConstraintDirectionHorizontal,
-		kConstraintDirectionVertical,
-	};
-
-	ConstraintDirection _constraintDirection;
-	Rect16 _constraintMargin;
-	bool _constrainToParent;
+	Common::SharedPtr<DragMotionProperties> _dragProps;
 };
 
 class VectorMotionModifier : public Modifier {
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index bf892dc5d02..b1a7ef2c487 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -4432,6 +4432,12 @@ VThreadState Runtime::updateMouseStateTask(const UpdateMouseStateTaskData &data)
 		Common::SharedPtr<Structural> tracked = _mouseOverObject.lock();
 		if (tracked) {
 			_mouseTrackingObject = tracked;
+			_mouseTrackingDragStart = _cachedMousePosition;
+			if (tracked->isElement() && static_cast<Element *>(tracked.get())->isVisual()) {
+				Rect16 initialRect = static_cast<VisualElement *>(tracked.get())->getRelativeRect();
+				_mouseTrackingObjectInitialOrigin = Point16::create(initialRect.left, initialRect.top);
+			} else
+				_mouseTrackingObjectInitialOrigin = Point16::create(0, 0);
 			_trackedMouseOutside = false;
 
 			MessageToSend msg;
@@ -4562,6 +4568,12 @@ VThreadState Runtime::updateMousePositionTask(const UpdateMousePositionTaskData
 
 			_trackedMouseOutside = mouseOutside;
 		}
+
+		// TODO: Figure out the right location for this
+		if (element->isVisual()) {
+			Point16 targetPoint = Point16::create(data.x - _mouseTrackingDragStart.x + _mouseTrackingObjectInitialOrigin.x, data.y - _mouseTrackingDragStart.y + _mouseTrackingObjectInitialOrigin.y);
+			static_cast<VisualElement *>(element)->handleDragMotion(this, _mouseTrackingObjectInitialOrigin, targetPoint);
+		}
 	}
 
 	DynamicValue mousePtValue;
@@ -6110,6 +6122,56 @@ void VisualElement::setCachedAbsoluteOrigin(const Point16 &absOrigin) {
 	_cachedAbsoluteOrigin = absOrigin;
 }
 
+void VisualElement::setDragMotionProperties(const Common::SharedPtr<DragMotionProperties>& dragProps) {
+	_dragProps = dragProps;
+}
+
+const Common::SharedPtr<DragMotionProperties> &VisualElement::getDragMotionProperties() const {
+	return _dragProps;
+}
+
+void VisualElement::handleDragMotion(Runtime *runtime, const Point16 &initialPoint, const Point16 &targetPointRef) {
+	if (!_dragProps)
+		return;
+
+	Point16 targetPoint = targetPointRef;
+
+	// NOTE: Constraints do not override insets if the object is out of bounds
+	if (_dragProps->constraintDirection == kConstraintDirectionHorizontal)
+		targetPoint.y = initialPoint.y;
+	if (_dragProps->constraintDirection == kConstraintDirectionVertical)
+		targetPoint.x = initialPoint.x;
+
+	const bool isConstrainedToParent = _dragProps->constrainToParent;
+
+	if (_dragProps->constrainToParent && _parent && _parent->isElement() && static_cast<Element *>(_parent)->isVisual()) {
+		Rect16 constrainInset = _dragProps->constraintMargin;
+
+		Rect16 parentRect = static_cast<VisualElement *>(_parent)->getRelativeRect();
+
+		// rect.width - inset.right
+		int32 minX = constrainInset.left;
+		int32 minY = constrainInset.top;
+		int32 maxX = parentRect.getWidth() - constrainInset.right - _rect.getWidth();
+		int32 maxY = parentRect.getHeight() - constrainInset.bottom - _rect.getHeight();
+
+		// TODO: Handle "squished" case where max < min, it does work but it's weird
+		if (targetPoint.x < minX)
+			targetPoint.x = minX;
+
+		if (targetPoint.y < minY)
+			targetPoint.y = minY;
+
+		if (targetPoint.x > maxX)
+			targetPoint.x = maxX;
+
+		if (targetPoint.y > maxY)
+			targetPoint.y = maxY;
+
+		offsetTranslate(targetPoint.x - _rect.left, targetPoint.y - _rect.top, false);
+	}
+}
+
 MiniscriptInstructionOutcome VisualElement::scriptSetVisibility(MiniscriptThread *thread, const DynamicValue &result) {
 	// FIXME: Need to make this fire Show/Hide events!
 	if (result.getType() == DynamicValueTypes::kBoolean) {
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 36e837bbded..c45d178a24b 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -148,6 +148,12 @@ enum TransitionDirection {
 	kTransitionDirectionRight = 0x387,
 };
 
+enum ConstraintDirection {
+	kConstraintDirectionNone,
+	kConstraintDirectionHorizontal,
+	kConstraintDirectionVertical,
+};
+
 namespace DynamicValueTypes {
 
 enum DynamicValueType {
@@ -1435,6 +1441,12 @@ private:
 	const Common::KeyState _keyEvt;
 };
 
+struct DragMotionProperties {
+	ConstraintDirection constraintDirection;
+	Rect16 constraintMargin;
+	bool constrainToParent;
+};
+
 class Runtime {
 public:
 	explicit Runtime(OSystem *system, Audio::Mixer *mixer, ISaveUIProvider *saveProvider, ILoadUIProvider *loadProvider);
@@ -1697,6 +1709,8 @@ private:
 	// object is either not clickable, or is behind another object with mouse collision.
 	Common::WeakPtr<Structural> _mouseOverObject;
 	Common::WeakPtr<Structural> _mouseTrackingObject;
+	Point16 _mouseTrackingDragStart;
+	Point16 _mouseTrackingObjectInitialOrigin;
 	bool _trackedMouseOutside;
 	bool _forceCursorRefreshOnce;
 
@@ -2237,6 +2251,11 @@ public:
 	const Point16 &getCachedAbsoluteOrigin() const;
 	void setCachedAbsoluteOrigin(const Point16 &absOrigin);
 
+	void setDragMotionProperties(const Common::SharedPtr<DragMotionProperties> &dragProps);
+	const Common::SharedPtr<DragMotionProperties> &getDragMotionProperties() const;
+
+	void handleDragMotion(Runtime *runtime, const Point16 &initialOrigin, const Point16 &targetOrigin);
+
 	virtual void render(Window *window) = 0;
 
 protected:
@@ -2272,6 +2291,8 @@ protected:
 	Rect16 _rect;
 	Point16 _cachedAbsoluteOrigin;
 	uint16 _layer;
+
+	Common::SharedPtr<DragMotionProperties> _dragProps;
 };
 
 class NonVisualElement : public Element {


Commit: 1ec0d2a4cdf58ea00584c3509b3215472aa607ae
    https://github.com/scummvm/scummvm/commit/1ec0d2a4cdf58ea00584c3509b3215472aa607ae
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Forward object variable attribute references

Changed paths:
    engines/mtropolis/elements.h
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/runtime.cpp


diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index 397996dcb98..cd3c4c01429 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -88,7 +88,7 @@ public:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Movie Element"; }
-	SupportStatus debugGetSupportStatus() const override { return kSupportStatusPartial; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
@@ -190,7 +190,7 @@ public:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "mToon Element"; }
-	SupportStatus debugGetSupportStatus() const override { return kSupportStatusPartial; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index a787b8b2de5..bbb3d67837f 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -467,7 +467,9 @@ MiniscriptInstructionOutcome ObjectReferenceVariableModifier::writeRefAttribute(
 		return kMiniscriptInstructionOutcomeContinue;
 	}
 	if (attrib == "object") {
-		DynamicValueWriteFuncHelper<ObjectReferenceVariableModifier, &ObjectReferenceVariableModifier::scriptSetObject>::create(this, result);
+		result.pod.ptrOrOffset = 0;
+		result.pod.objectRef = this;
+		result.pod.ifc = &ObjectWriteInterface::_instance;
 		return kMiniscriptInstructionOutcomeContinue;
 	}
 
@@ -538,6 +540,28 @@ MiniscriptInstructionOutcome ObjectReferenceVariableModifier::scriptSetObject(Mi
 		return kMiniscriptInstructionOutcomeFailed;
 }
 
+MiniscriptInstructionOutcome ObjectReferenceVariableModifier::scriptObjectRefAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib) {
+	resolve();
+
+	if (_object.object.expired()) {
+		thread->error("Attempted to reference an attribute of an object variable object, but the reference is dead");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	return _object.object.lock()->writeRefAttribute(thread, proxy, attrib);
+}
+
+MiniscriptInstructionOutcome ObjectReferenceVariableModifier::scriptObjectRefAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib, const DynamicValue &index) {
+	resolve();
+
+	if (_object.object.expired()) {
+		thread->error("Attempted to reference an attribute of an object variable object, but the reference is dead");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	return _object.object.lock()->writeRefAttributeIndexed(thread, proxy, attrib, index);
+}
+
 void ObjectReferenceVariableModifier::resolve() {
 	if (!_object.object.expired())
 		return;
@@ -707,10 +731,24 @@ RuntimeObject *ObjectReferenceVariableModifier::getObjectParent(RuntimeObject *o
 	return nullptr;
 }
 
+MiniscriptInstructionOutcome ObjectReferenceVariableModifier::ObjectWriteInterface::write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr ptrOrOffset) const {
+	return static_cast<ObjectReferenceVariableModifier *>(objectRef)->scriptSetObject(thread, value);
+}
+
+MiniscriptInstructionOutcome ObjectReferenceVariableModifier::ObjectWriteInterface::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const {
+	return static_cast<ObjectReferenceVariableModifier *>(objectRef)->scriptObjectRefAttrib(thread, proxy, attrib);
+}
+
+MiniscriptInstructionOutcome ObjectReferenceVariableModifier::ObjectWriteInterface::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const {
+	return static_cast<ObjectReferenceVariableModifier *>(objectRef)->scriptObjectRefAttribIndexed(thread, proxy, attrib, index);
+}
+
 ObjectReferenceVariableModifier::SaveLoad::SaveLoad(ObjectReferenceVariableModifier *modifier) : _modifier(modifier) {
 	_objectPath = _modifier->_objectPath;
 }
 
+ObjectReferenceVariableModifier::ObjectWriteInterface ObjectReferenceVariableModifier::ObjectWriteInterface::_instance;
+
 void ObjectReferenceVariableModifier::SaveLoad::commitLoad() const {
 	_modifier->_object.reset();
 	_modifier->_fullPath.clear();
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index 8f86b3ce5c9..fdeeede69d4 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -137,6 +137,14 @@ public:
 #endif
 
 private:
+	struct ObjectWriteInterface : public IDynamicValueWriteInterface {
+		MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr ptrOrOffset) const override;
+		MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const override;
+		MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
+
+		static ObjectWriteInterface _instance;
+	};
+
 	class SaveLoad : public ModifierSaveLoad {
 	public:
 		explicit SaveLoad(ObjectReferenceVariableModifier *modifier);
@@ -154,6 +162,8 @@ private:
 
 	MiniscriptInstructionOutcome scriptSetPath(MiniscriptThread *thread, const DynamicValue &value);
 	MiniscriptInstructionOutcome scriptSetObject(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptObjectRefAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib);
+	MiniscriptInstructionOutcome scriptObjectRefAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib, const DynamicValue &index);
 
 	void resolve();
 	void resolveRelativePath(RuntimeObject *obj, const Common::String &path, size_t startPos);
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index b1a7ef2c487..78172e3c63a 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -2549,6 +2549,21 @@ bool Structural::readAttribute(MiniscriptThread *thread, DynamicValue &result, c
 		return true;
 	}
 
+	// Traverse children (modifiers must be first)
+	for (const Common::SharedPtr<Modifier> &modifier : _modifiers) {
+		if (caseInsensitiveEqual(modifier->getName(), attrib)) {
+			result.setObject(modifier);
+			return true;
+		}
+	}
+
+	for (const Common::SharedPtr<Structural> &child : _children) {
+		if (caseInsensitiveEqual(child->getName(), attrib)) {
+			result.setObject(child);
+			return true;
+		}
+	}
+
 	return RuntimeObject::readAttribute(thread, result, attrib);
 }
 
@@ -6142,8 +6157,6 @@ void VisualElement::handleDragMotion(Runtime *runtime, const Point16 &initialPoi
 	if (_dragProps->constraintDirection == kConstraintDirectionVertical)
 		targetPoint.x = initialPoint.x;
 
-	const bool isConstrainedToParent = _dragProps->constrainToParent;
-
 	if (_dragProps->constrainToParent && _parent && _parent->isElement() && static_cast<Element *>(_parent)->isVisual()) {
 		Rect16 constrainInset = _dragProps->constraintMargin;
 


Commit: 52900575f144c1f6b50a27acd1b3bdafbd51adb1
    https://github.com/scummvm/scummvm/commit/52900575f144c1f6b50a27acd1b3bdafbd51adb1
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix some metal puzzle bugs

Changed paths:
    engines/mtropolis/elements.cpp
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index c32136b1716..c8c2b53339a 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -788,12 +788,11 @@ void MToonElement::render(Window *window) {
 			if (frameRect.getWidth() == _renderSurface->w && frameRect.getHeight() == _renderSurface->h) {
 				// Frame rect is the size of the render surface, meaning the frame rect is an offset
 				srcRect = Common::Rect(0, 0, frameRect.getWidth(), frameRect.getHeight());
-				destRect = Common::Rect(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y, _cachedAbsoluteOrigin.x + frameRect.getWidth(), _cachedAbsoluteOrigin.y + frameRect.getHeight());
 			} else {
 				// Frame rect is a sub-area of the rendered rect
 				srcRect = Common::Rect(frameRect.left, frameRect.top, frameRect.right, frameRect.bottom);
-				destRect = Common::Rect(_cachedAbsoluteOrigin.x + frameRect.left, _cachedAbsoluteOrigin.y + frameRect.left, _cachedAbsoluteOrigin.x + frameRect.right, _cachedAbsoluteOrigin.y + frameRect.bottom);
 			}
+			destRect = Common::Rect(_cachedAbsoluteOrigin.x + frameRect.left, _cachedAbsoluteOrigin.y + frameRect.top, _cachedAbsoluteOrigin.x + frameRect.right, _cachedAbsoluteOrigin.y + frameRect.bottom);
 
 			window->getSurface()->blitFrom(*_renderSurface, srcRect, destRect);
 		}
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 78172e3c63a..cc9f49c967c 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -4015,17 +4015,26 @@ void Runtime::recursiveActivateStructural(Structural *structural) {
 	}
 }
 
-bool Runtime::isStructuralMouseInteractive(Structural *structural) {
+bool Runtime::isStructuralMouseInteractive(Structural *structural, MouseInteractivityTestType testType) {
+	if (structural->isElement()) {
+		Element *element = static_cast<Element *>(structural);
+		if (element->isVisual()) {
+			VisualElement *visual = static_cast<VisualElement *>(element);
+			if (visual->getDragMotionProperties())
+				return true;	// Drag motion is always mouse interactive
+		}
+	}
+
 	for (const Common::SharedPtr<Modifier> &modifier : structural->getModifiers()) {
-		if (isModifierMouseInteractive(modifier.get()))
+		if (isModifierMouseInteractive(modifier.get(), testType))
 			return true;
 	}
 
 	return false;
 }
 
-bool Runtime::isModifierMouseInteractive(Modifier *modifier) {
-	const EventIDs::EventID evtIDs[] = {
+bool Runtime::isModifierMouseInteractive(Modifier *modifier, MouseInteractivityTestType testType) {
+	static const EventIDs::EventID allEventIDs[] = {
 		EventIDs::kMouseUp,
 		EventIDs::kMouseDown,
 		EventIDs::kMouseOver,
@@ -4037,7 +4046,30 @@ bool Runtime::isModifierMouseInteractive(Modifier *modifier) {
 		EventIDs::kMouseUpOutside
 	};
 
-	for (EventIDs::EventID evtID : evtIDs) {
+
+	static const EventIDs::EventID mouseClickEventIDs[] = {
+		EventIDs::kMouseUp,
+		EventIDs::kMouseDown,
+		EventIDs::kMouseTrackedInside,
+		EventIDs::kMouseTracking,
+		EventIDs::kMouseTrackedOutside,
+		EventIDs::kMouseUpInside,
+		EventIDs::kMouseUpOutside
+	};
+
+	const EventIDs::EventID *evtIDs = nullptr;
+	size_t numEventIDs = 0;
+
+	if (testType == kMouseInteractivityTestAnything) {
+		evtIDs = allEventIDs;
+		numEventIDs = ARRAYSIZE(allEventIDs);
+	} else if (testType == kMouseInteractivityTestMouseClick) {
+		evtIDs = mouseClickEventIDs;
+		numEventIDs = ARRAYSIZE(mouseClickEventIDs);
+	}
+
+	for (size_t i = 0; i < numEventIDs; i++) {
+		EventIDs::EventID evtID = evtIDs[i];
 		if (modifier->respondsToEvent(Event::create(evtID, 0)))
 			return true;
 	}
@@ -4045,7 +4077,7 @@ bool Runtime::isModifierMouseInteractive(Modifier *modifier) {
 	IModifierContainer *propagationContainer = modifier->getMessagePropagationContainer();
 	if (propagationContainer) {
 		for (const Common::SharedPtr<Modifier> &child : propagationContainer->getModifiers()) {
-			if (isModifierMouseInteractive(child.get()))
+			if (isModifierMouseInteractive(child.get(), testType))
 				return true;
 		}
 	}
@@ -4053,7 +4085,7 @@ bool Runtime::isModifierMouseInteractive(Modifier *modifier) {
 	return false;
 }
 
-void Runtime::recursiveFindMouseCollision(Structural *&bestResult, int &bestLayer, int &bestStackHeight, Structural *candidate, int stackHeight, int32 relativeX, int32 relativeY) {
+void Runtime::recursiveFindMouseCollision(Structural *&bestResult, int &bestLayer, int &bestStackHeight, Structural *candidate, int stackHeight, int32 relativeX, int32 relativeY, MouseInteractivityTestType testType) {
 	int32 childRelativeX = relativeX;
 	int32 childRelativeY = relativeY;
 	if (candidate->isElement()) {
@@ -4066,7 +4098,7 @@ void Runtime::recursiveFindMouseCollision(Structural *&bestResult, int &bestLaye
 			// Objects in a higher layer in lower scenes still have higher render order, so they're on top
 			const bool isInFront = (layer > bestLayer) || (layer == bestLayer && stackHeight > bestStackHeight);
 
-			if (isInFront && visual->isMouseInsideBox(relativeX, relativeY) && isStructuralMouseInteractive(visual) && visual->isMouseCollisionAtPoint(relativeX, relativeY)) {
+			if (isInFront && visual->isMouseInsideBox(relativeX, relativeY) && isStructuralMouseInteractive(visual, testType) && visual->isMouseCollisionAtPoint(relativeX, relativeY)) {
 				bestResult = candidate;
 				bestLayer = layer;
 				bestStackHeight = stackHeight;
@@ -4079,7 +4111,7 @@ void Runtime::recursiveFindMouseCollision(Structural *&bestResult, int &bestLaye
 
 
 	for (const Common::SharedPtr<Structural> &child : candidate->getChildren()) {
-		recursiveFindMouseCollision(bestResult, bestLayer, bestStackHeight, child.get(), stackHeight, childRelativeX, childRelativeY);
+		recursiveFindMouseCollision(bestResult, bestLayer, bestStackHeight, child.get(), stackHeight, childRelativeX, childRelativeY, testType);
 	}
 }
 
@@ -4444,12 +4476,20 @@ VThreadState Runtime::updateMouseStateTask(const UpdateMouseStateTaskData &data)
 
 	if (data.mouseDown) {
 		// Mouse down
-		Common::SharedPtr<Structural> tracked = _mouseOverObject.lock();
+		Structural *tracked = nullptr;
+		int bestSceneStack = INT_MIN;
+		int bestLayer = INT_MIN;
+
+		for (size_t ri = 0; ri < _sceneStack.size(); ri++) {
+			const SceneStackEntry &sceneStackEntry = _sceneStack[_sceneStack.size() - 1 - ri];
+			recursiveFindMouseCollision(tracked, bestSceneStack, bestLayer, sceneStackEntry.scene.get(), _sceneStack.size() - 1 - ri, _cachedMousePosition.x, _cachedMousePosition.y, kMouseInteractivityTestMouseClick);
+		}
+
 		if (tracked) {
-			_mouseTrackingObject = tracked;
+			_mouseTrackingObject = tracked->getSelfReference().staticCast<Structural>();
 			_mouseTrackingDragStart = _cachedMousePosition;
-			if (tracked->isElement() && static_cast<Element *>(tracked.get())->isVisual()) {
-				Rect16 initialRect = static_cast<VisualElement *>(tracked.get())->getRelativeRect();
+			if (tracked->isElement() && static_cast<Element *>(tracked)->isVisual()) {
+				Rect16 initialRect = static_cast<VisualElement *>(tracked)->getRelativeRect();
 				_mouseTrackingObjectInitialOrigin = Point16::create(initialRect.left, initialRect.top);
 			} else
 				_mouseTrackingObjectInitialOrigin = Point16::create(0, 0);
@@ -4457,7 +4497,7 @@ VThreadState Runtime::updateMouseStateTask(const UpdateMouseStateTaskData &data)
 
 			MessageToSend msg;
 			msg.eventID = EventIDs::kMouseDown;
-			msg.target = tracked.get();
+			msg.target = tracked;
 			messagesToSend.push_back(msg);
 		}
 	} else {
@@ -4516,7 +4556,7 @@ VThreadState Runtime::updateMousePositionTask(const UpdateMousePositionTaskData
 
 	for (size_t ri = 0; ri < _sceneStack.size(); ri++) {
 		const SceneStackEntry &sceneStackEntry = _sceneStack[_sceneStack.size() - 1 - ri];
-		recursiveFindMouseCollision(collisionItem, bestSceneStack, bestLayer, sceneStackEntry.scene.get(), _sceneStack.size() - 1 - ri, data.x, data.y);
+		recursiveFindMouseCollision(collisionItem, bestSceneStack, bestLayer, sceneStackEntry.scene.get(), _sceneStack.size() - 1 - ri, data.x, data.y, kMouseInteractivityTestAnything);
 	}
 
 	Common::SharedPtr<Structural> newMouseOver;
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index c45d178a24b..b63559a37a4 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -154,6 +154,11 @@ enum ConstraintDirection {
 	kConstraintDirectionVertical,
 };
 
+enum MouseInteractivityTestType {
+	kMouseInteractivityTestAnything,
+	kMouseInteractivityTestMouseClick,
+};
+
 namespace DynamicValueTypes {
 
 enum DynamicValueType {
@@ -1612,9 +1617,9 @@ private:
 	void recursiveDeactivateStructural(Structural *structural);
 	void recursiveActivateStructural(Structural *structural);
 
-	static bool isStructuralMouseInteractive(Structural *structural);
-	static bool isModifierMouseInteractive(Modifier *modifier);
-	static void recursiveFindMouseCollision(Structural *&bestResult, int &bestLayer, int &bestStackHeight, Structural *candidate, int stackHeight, int32 relativeX, int32 relativeY);
+	static bool isStructuralMouseInteractive(Structural *structural, MouseInteractivityTestType testType);
+	static bool isModifierMouseInteractive(Modifier *modifier, MouseInteractivityTestType testType);
+	static void recursiveFindMouseCollision(Structural *&bestResult, int &bestLayer, int &bestStackHeight, Structural *candidate, int stackHeight, int32 relativeX, int32 relativeY, MouseInteractivityTestType testType);
 
 	void queueEventAsLowLevelSceneStateTransitionAction(const Event &evt, Structural *root, bool cascade, bool relay);
 
@@ -1707,6 +1712,7 @@ private:
 	// responds to any mouse event.  The mouse tracking object is the object that was clicked.
 	// These can differ if the user holds down the mouse and moves it to a spot where the tracked
 	// object is either not clickable, or is behind another object with mouse collision.
+	// Note that mouseOverObject is also NOT necessarily what will receive mouse down events.
 	Common::WeakPtr<Structural> _mouseOverObject;
 	Common::WeakPtr<Structural> _mouseTrackingObject;
 	Point16 _mouseTrackingDragStart;


Commit: 435fddeb5580cc2a7e2ae4a887cc0fd2ec76f553
    https://github.com/scummvm/scummvm/commit/435fddeb5580cc2a7e2ae4a887cc0fd2ec76f553
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Stub out Movement/RectShift attributes

Changed paths:
    engines/mtropolis/plugin/obsidian.cpp
    engines/mtropolis/plugin/obsidian.h
    engines/mtropolis/plugin/obsidian_data.cpp
    engines/mtropolis/plugin/obsidian_data.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/plugin/obsidian.cpp b/engines/mtropolis/plugin/obsidian.cpp
index 92dac2eae1d..f5ccdba6caf 100644
--- a/engines/mtropolis/plugin/obsidian.cpp
+++ b/engines/mtropolis/plugin/obsidian.cpp
@@ -29,17 +29,63 @@ namespace MTropolis {
 namespace Obsidian {
 
 bool MovementModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::MovementModifier &data) {
+	// FIXME: Map these
+	_rate = 0;
+	_frequency = 0;
+	_type = false;
+	_dest = Point16::create(0, 0);
+
 	return true;
 }
 
+MiniscriptInstructionOutcome MovementModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
+	if (attrib == "type") {
+		DynamicValueWriteBoolHelper::create(&_type, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+	if (attrib == "dest") {
+		DynamicValueWritePointHelper::create(&_dest, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+	if (attrib == "rate") {
+		DynamicValueWriteIntegerHelper<int32>::create(&_rate, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+	if (attrib == "frequency") {
+		DynamicValueWriteIntegerHelper<int32>::create(&_frequency, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	return Modifier::writeRefAttribute(thread, result, attrib);
+}
+
 Common::SharedPtr<Modifier> MovementModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new MovementModifier(*this));
 }
 
 bool RectShiftModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::RectShiftModifier &data) {
+	if (data.rate.type != Data::PlugInTypeTaggedValue::kInteger)
+		return false;
+
+	_direction = 0;
+	_rate = data.rate.value.asInt;
+
 	return true;
 }
 
+MiniscriptInstructionOutcome RectShiftModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
+	if (attrib == "rate") {
+		DynamicValueWriteIntegerHelper<int32>::create(&_rate, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+	if (attrib == "direction") {
+		DynamicValueWriteIntegerHelper<int32>::create(&_direction, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	return Modifier::writeRefAttribute(thread, result, attrib);
+}
+
 Common::SharedPtr<Modifier> RectShiftModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new RectShiftModifier(*this));
 }
diff --git a/engines/mtropolis/plugin/obsidian.h b/engines/mtropolis/plugin/obsidian.h
index 624d895e469..e96ac431304 100644
--- a/engines/mtropolis/plugin/obsidian.h
+++ b/engines/mtropolis/plugin/obsidian.h
@@ -38,24 +38,36 @@ class MovementModifier : public Modifier {
 public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Obsidian::MovementModifier &data);
 
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Movement Modifier"; }
 #endif
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+
+	Point16 _dest;
+	bool _type;
+	int32 _rate;
+	int32 _frequency;
 };
 
 class RectShiftModifier : public Modifier {
 public:
 	bool load(const PlugInModifierLoaderContext &context, const Data::Obsidian::RectShiftModifier &data);
 
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib);
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Rect Shift Modifier"; }
 #endif
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+
+	int32 _rate;
+	int32 _direction;
 };
 
 class TextWorkModifier : public Modifier {
diff --git a/engines/mtropolis/plugin/obsidian_data.cpp b/engines/mtropolis/plugin/obsidian_data.cpp
index f779f3eac84..352534e7f41 100644
--- a/engines/mtropolis/plugin/obsidian_data.cpp
+++ b/engines/mtropolis/plugin/obsidian_data.cpp
@@ -52,7 +52,7 @@ DataReadErrorCode RectShiftModifier::load(PlugIn &plugIn, const PlugInModifier &
 	if (prefix.plugInRevision != 1)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!unknown1Event.load(reader) || !unknown2Event.load(reader) || !unknown3Int.load(reader))
+	if (!unknown1Event.load(reader) || !unknown2Event.load(reader) || !rate.load(reader))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
diff --git a/engines/mtropolis/plugin/obsidian_data.h b/engines/mtropolis/plugin/obsidian_data.h
index 230fb8254af..4e32b384452 100644
--- a/engines/mtropolis/plugin/obsidian_data.h
+++ b/engines/mtropolis/plugin/obsidian_data.h
@@ -60,7 +60,7 @@ protected:
 struct RectShiftModifier : public PlugInModifierData {
 	PlugInTypeTaggedValue unknown1Event; // Probably "enable when"
 	PlugInTypeTaggedValue unknown2Event; // Probably "disable when"
-	PlugInTypeTaggedValue unknown3Int;
+	PlugInTypeTaggedValue rate;
 
 protected:
 	DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override;
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index cc9f49c967c..ce50e1823cd 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -1634,6 +1634,43 @@ void DynamicValueWriteStringHelper::create(Common::String *strValue, DynamicValu
 
 DynamicValueWriteStringHelper DynamicValueWriteStringHelper::_instance;
 
+MiniscriptInstructionOutcome DynamicValueWritePointHelper::write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr ptrOrOffset) const {
+	if (value.getType() != DynamicValueTypes::kPoint) {
+		thread->error("Can't set point to invalid type");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	*static_cast<Point16 *>(objectRef) = value.getPoint();
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome DynamicValueWritePointHelper::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const {
+	if (attrib == "x") {
+		DynamicValueWriteIntegerHelper<int16>::create(&static_cast<Point16 *>(objectRef)->x, proxy);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+	if (attrib == "y") {
+		DynamicValueWriteIntegerHelper<int16>::create(&static_cast<Point16 *>(objectRef)->y, proxy);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	thread->error("Invalid attribute for point");
+	return kMiniscriptInstructionOutcomeFailed;
+}
+
+MiniscriptInstructionOutcome DynamicValueWritePointHelper::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const {
+	return kMiniscriptInstructionOutcomeFailed;
+}
+
+void DynamicValueWritePointHelper::create(Point16 *pointValue, DynamicValueWriteProxy &proxy) {
+	proxy.pod.ptrOrOffset = 0;
+	proxy.pod.objectRef = pointValue;
+	proxy.pod.ifc = &_instance;
+}
+
+DynamicValueWritePointHelper DynamicValueWritePointHelper::_instance;
+
 MiniscriptInstructionOutcome DynamicValueWriteBoolHelper::write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr ptrOrOffset) const {
 	bool &dest = *static_cast<bool *>(objectRef);
 	switch (value.getType()) {
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index b63559a37a4..6b080a0aa90 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -963,15 +963,15 @@ private:
 template<class TInteger>
 DynamicValueWriteIntegerHelper<TInteger> DynamicValueWriteIntegerHelper<TInteger>::_instance;
 
-struct DynamicValueWriteStringHelper : public IDynamicValueWriteInterface {
+struct DynamicValueWritePointHelper : public IDynamicValueWriteInterface {
 	MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr ptrOrOffset) const override;
 	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const override;
 	MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
 
-	static void create(Common::String *strValue, DynamicValueWriteProxy &proxy);
+	static void create(Point16 *pointValue, DynamicValueWriteProxy &proxy);
 
 private:
-	static DynamicValueWriteStringHelper _instance;
+	static DynamicValueWritePointHelper _instance;
 };
 
 struct DynamicValueWriteBoolHelper : public IDynamicValueWriteInterface {
@@ -985,6 +985,17 @@ private:
 	static DynamicValueWriteBoolHelper _instance;
 };
 
+struct DynamicValueWriteStringHelper : public IDynamicValueWriteInterface {
+	MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr ptrOrOffset) const override;
+	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const override;
+	MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
+
+	static void create(Common::String *strValue, DynamicValueWriteProxy &proxy);
+
+private:
+	static DynamicValueWriteStringHelper _instance;
+};
+
 template<class TClass, MiniscriptInstructionOutcome (TClass::*TWriteMethod)(MiniscriptThread *thread, const DynamicValue &dest), MiniscriptInstructionOutcome (TClass::*TRefAttribMethod)(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib)>
 struct DynamicValueWriteOrRefAttribFuncHelper : public IDynamicValueWriteInterface {
 	MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr ptrOrOffset) const override {


Commit: 939420f64e79be21b3216bedd9beec9b60df57b3
    https://github.com/scummvm/scummvm/commit/939420f64e79be21b3216bedd9beec9b60df57b3
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Make failed sends non-fatal (fixes being unable to click aircraft propulsion puzzle)

Changed paths:
    engines/mtropolis/miniscript.cpp


diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 4f16d94797c..093460b84be 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -543,24 +543,20 @@ MiniscriptInstructionOutcome Send::execute(MiniscriptThread *thread) const {
 	DynamicValue &payloadValue = thread->getStackValueFromTop(1).value;
 
 	if (targetValue.getType() != DynamicValueTypes::kObject) {
-		thread->error("Invalid message destination (target isn't an object reference)");
-		return kMiniscriptInstructionOutcomeFailed;
+		// Failed sends are non-fatal (Obsidian requires this in the aircraft propulsion room to enter the propulsion puzzle)
+		warning("Invalid message destination (target isn't an object reference)");
+		thread->popValues(2);
+		return kMiniscriptInstructionOutcomeContinue;
 	}
 
 	Common::SharedPtr<RuntimeObject> obj = targetValue.getObject().object.lock();
 
 	if (!obj) {
-		// HACK: Obsidian triggers NAV_Restart on Project Started, which triggers "<init globals> on NAV_Restart"
-		// which sends PRG_Toggle_Status_Display to sharedScene.  Apparently, mTropolis will not trigger an error
-		// on sends to sharedScene at that point even though the destination is invalid.
-		// Maybe invalid sends aren't even an error?  I don't know.
-		if (!thread->getRuntime()->getActiveSharedScene().get()) {
-			thread->popValues(2);
-			return kMiniscriptInstructionOutcomeContinue;
-		}
-
-		thread->error("Invalid message destination (object reference is invalid)");
-		return kMiniscriptInstructionOutcomeFailed;
+		// Obsidian also triggers NAV_Restart on Project Started, which triggers "<init globals> on NAV_Restart"
+		// which sends PRG_Toggle_Status_Display to sharedScene, even though at that point there is no shared scene.
+		warning("Invalid message destination (target object is invalid)");
+		thread->popValues(2);
+		return kMiniscriptInstructionOutcomeContinue;
 	}
 
 	Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(_evt, payloadValue, thread->getModifier()->getSelfReference()));
@@ -570,8 +566,8 @@ MiniscriptInstructionOutcome Send::execute(MiniscriptThread *thread) const {
 	else if (obj->isStructural())
 		dispatch.reset(new MessageDispatch(msgProps, static_cast<Structural *>(obj.get()), _messageFlags.cascade, _messageFlags.relay, true));
 	else {
-		thread->error("Message destination is not a structural object or modifier");
-		return kMiniscriptInstructionOutcomeFailed;
+		warning("Invalid message destination (target object is not a modifier or structural object)");
+		return kMiniscriptInstructionOutcomeContinue;
 	}
 
 	thread->popValues(2);


Commit: ea0406b6a55e0a6c25b38b91110ec67f2b448fb9
    https://github.com/scummvm/scummvm/commit/ea0406b6a55e0a6c25b38b91110ec67f2b448fb9
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add path motion modifier stub

Changed paths:
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/modifier_factory.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h


diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index f03e3cbf919..7ff00e2253b 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -1113,6 +1113,52 @@ DataReadErrorCode SoundEffectModifier::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
+bool PathMotionModifierV2::PointDef::load(DataReader &reader)
+{
+	if (!point.load(reader)
+		|| !reader.readU32(frame)
+		|| !reader.readU32(frameFlags)
+		|| !reader.readU32(messageFlags)
+		|| !send.load(reader)
+		|| !reader.readU16(unknown11)
+		|| !reader.readU32(destination)
+		|| !reader.readBytes(unknown13)
+		|| !with.load(reader)
+		|| !reader.readU8(withSourceLength)
+		|| !reader.readU8(withStringLength)
+		|| !reader.readNonTerminatedStr(withSource, withSourceLength)
+		|| !reader.readNonTerminatedStr(withString, withStringLength))
+		return false;
+
+	return true;
+}
+
+DataReadErrorCode PathMotionModifierV2::load(DataReader &reader) {
+	if (_revision != 0x3e9)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!modHeader.load(reader)
+		|| !reader.readU32(flags)
+		|| !executeWhen.load(reader)
+		|| !terminateWhen.load(reader)
+		|| !reader.readBytes(unknown2)
+		|| !reader.readU16(numPoints)
+		|| !reader.readBytes(unknown3)
+		|| !reader.readU32(frameDurationTimes10Million)
+		|| !reader.readBytes(unknown5)
+		|| !reader.readU32(unknown6))
+		return kDataReadErrorReadFailed;
+
+	points.resize(numPoints);
+
+	for (size_t i = 0; i < numPoints; i++) {
+		if (!points[i].load(reader))
+			return kDataReadErrorReadFailed;
+	}
+
+	return kDataReadErrorNone;
+}
+
 DataReadErrorCode DragMotionModifier::load(DataReader &reader) {
 	if (_revision != 0x3e8)
 		return kDataReadErrorUnsupportedRevision;
@@ -1894,6 +1940,9 @@ DataReadErrorCode loadDataObject(const PlugInModifierRegistry &registry, DataRea
 	case DataObjectTypes::kDragMotionModifier:
 		dataObject = new DragMotionModifier();
 		break;
+	case DataObjectTypes::kPathMotionModifierV2:
+		dataObject = new PathMotionModifierV2();
+		break;
 	case DataObjectTypes::kVectorMotionModifier:
 		dataObject = new VectorMotionModifier();
 		break;
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index 7f46fd6b82e..225c419501a 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -102,7 +102,7 @@ enum DataObjectType {
 	kSoundEffectModifier                 = 0x1a4,
 	kDragMotionModifier                  = 0x208,
 	kPathMotionModifierV1                = 0x21c,	// NYI - Obsolete version
-	kPathMotionModifierV2                = 0x21b,	// NYI
+	kPathMotionModifierV2                = 0x21b,
 	kVectorMotionModifier                = 0x226,
 	kSceneTransitionModifier             = 0x26c,
 	kElementTransitionModifier           = 0x276,
@@ -952,6 +952,56 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
+struct PathMotionModifierV2 : public DataObject {
+	struct PointDef {
+		enum FrameFlags {
+			kFrameFlagPlaySequentially = 1,
+		};
+
+		Point point;
+		uint32 frame;
+		uint32 frameFlags;
+		uint32 messageFlags;
+		Event send;
+		uint16 unknown11;
+		uint32 destination;
+		uint8 unknown13[10];
+		InternalTypeTaggedValue with;
+		uint8 withSourceLength;
+		uint8 withStringLength;
+
+		Common::String withSource;
+		Common::String withString;
+
+		bool load(DataReader &reader);
+	};
+
+	enum Flags {
+		kFlagReverse = 0x00100000,
+		kFlagLoop = 0x10000000,
+		kFlagAlternate = 0x02000000,
+		kFlagStartAtBeginning = 0x08000000,
+	};
+
+	TypicalModifierHeader modHeader;
+	uint32 flags;
+
+	Event executeWhen;
+	Event terminateWhen;
+
+	uint8 unknown2[2];
+	uint16 numPoints;
+	uint8 unknown3[4];
+	uint32 frameDurationTimes10Million;
+	uint8 unknown5[4];
+	uint32 unknown6;
+
+	Common::Array<PointDef> points;
+
+protected:
+	DataReadErrorCode load(DataReader &reader) override;
+};
+
 struct DragMotionModifier : public DataObject {
 	TypicalModifierHeader modHeader;
 
diff --git a/engines/mtropolis/modifier_factory.cpp b/engines/mtropolis/modifier_factory.cpp
index 48934f8107c..37fff85146b 100644
--- a/engines/mtropolis/modifier_factory.cpp
+++ b/engines/mtropolis/modifier_factory.cpp
@@ -75,6 +75,8 @@ IModifierFactory *getModifierFactoryForDataObjectType(const Data::DataObjectType
 		return ModifierFactory<ChangeSceneModifier, Data::ChangeSceneModifier>::getInstance();
 	case Data::DataObjectTypes::kSoundEffectModifier:
 		return ModifierFactory<SoundEffectModifier, Data::SoundEffectModifier>::getInstance();
+	case Data::DataObjectTypes::kPathMotionModifierV2:
+		return ModifierFactory<PathMotionModifierV2, Data::PathMotionModifierV2>::getInstance();
 	case Data::DataObjectTypes::kDragMotionModifier:
 		return ModifierFactory<DragMotionModifier, Data::DragMotionModifier>::getInstance();
 	case Data::DataObjectTypes::kVectorMotionModifier:
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 0e7b6633f1d..90e0564d08d 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -531,6 +531,67 @@ Common::SharedPtr<Modifier> SoundEffectModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new SoundEffectModifier(*this));
 }
 
+bool PathMotionModifierV2::load(ModifierLoaderContext &context, const Data::PathMotionModifierV2 &data) {
+	if (!loadTypicalHeader(data.modHeader))
+		return false;
+
+	if (!_executeWhen.load(data.executeWhen) || !_terminateWhen.load(data.terminateWhen))
+		return false;
+
+	_reverse = ((data.flags & Data::PathMotionModifierV2::kFlagReverse) != 0);
+	_loop = ((data.flags & Data::PathMotionModifierV2::kFlagLoop) != 0);
+	_alternate = ((data.flags & Data::PathMotionModifierV2::kFlagAlternate) != 0);
+	_startAtBeginning = ((data.flags & Data::PathMotionModifierV2::kFlagStartAtBeginning) != 0);
+
+	_frameDurationTimes10Million = data.frameDurationTimes10Million;
+
+	_points.resize(data.numPoints);
+
+	for (size_t i = 0; i < _points.size(); i++) {
+		const Data::PathMotionModifierV2::PointDef &inPoint = data.points[i];
+		PointDef &outPoint = _points[i];
+
+		outPoint.frame = inPoint.frame;
+		outPoint.useFrame = ((inPoint.frameFlags & Data::PathMotionModifierV2::PointDef::kFrameFlagPlaySequentially) != 0);
+		if (!outPoint.point.load(inPoint.point) || !outPoint.sendSpec.load(inPoint.send, inPoint.messageFlags, inPoint.with, inPoint.withSource, inPoint.withString, inPoint.destination))
+			return false;
+	}
+
+	return true;
+}
+
+bool PathMotionModifierV2::respondsToEvent(const Event &evt) const {
+	return _executeWhen.respondsTo(evt) || _terminateWhen.respondsTo(evt);
+}
+
+VThreadState PathMotionModifierV2::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	if (_executeWhen.respondsTo(msg->getEvent())) {
+#ifdef MTROPOLIS_DEBUG_ENABLE
+		if (Debugger *debugger = runtime->debugGetDebugger())
+			debugger->notify(kDebugSeverityWarning, "Path motion modifier was supposed to execute, but this isn't implemented yet");
+#endif
+		_incomingData = msg->getValue();
+
+		return kVThreadReturn;
+	}
+	if (_terminateWhen.respondsTo(msg->getEvent())) {
+#ifdef MTROPOLIS_DEBUG_ENABLE
+		if (Debugger *debugger = runtime->debugGetDebugger())
+			debugger->notify(kDebugSeverityWarning, "Path motion modifier was supposed to terminate, but this isn't implemented yet");
+#endif
+		return kVThreadReturn;
+	}
+
+	return kVThreadReturn;
+}
+
+
+Common::SharedPtr<Modifier> PathMotionModifierV2::shallowClone() const {
+	Common::SharedPtr<PathMotionModifierV2> clone(new PathMotionModifierV2(*this));
+	clone->_incomingData = DynamicValue();
+	return clone;
+}
+
 bool DragMotionModifier::load(ModifierLoaderContext &context, const Data::DragMotionModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 8157c51f6da..0315dd65f98 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -240,6 +240,44 @@ private:
 	uint32 _assetID;
 };
 
+class PathMotionModifierV2 : public Modifier {
+public:
+	bool load(ModifierLoaderContext &context, const Data::PathMotionModifierV2 &data);
+
+	bool respondsToEvent(const Event &evt) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Path Motion Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusPartial; }
+#endif
+
+private:
+	struct PointDef {
+		Point16 point;
+		uint32 frame;
+		bool useFrame;
+
+		MessengerSendSpec sendSpec;
+	};
+
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
+	Event _executeWhen;
+	Event _terminateWhen;
+
+	bool _reverse;
+	bool _loop;
+	bool _alternate;
+	bool _startAtBeginning;
+
+	uint32 _frameDurationTimes10Million;
+
+	Common::Array<PointDef> _points;
+
+	DynamicValue _incomingData;
+};
+
 class DragMotionModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::DragMotionModifier &data);


Commit: b8dde4b4c5f1d1a900fda72ba5eb079980ccbdbb
    https://github.com/scummvm/scummvm/commit/b8dde4b4c5f1d1a900fda72ba5eb079980ccbdbb
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add timevalue sets and vector motion modifier

Changed paths:
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index c8c2b53339a..75b649f6773 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -215,6 +215,10 @@ bool MovieElement::readAttribute(MiniscriptThread *thread, DynamicValue &result,
 		result.setIntRange(_playRange);
 		return true;
 	}
+	if (attrib == "timevalue") {
+		result.setInt(_currentTimestamp);
+		return true;
+	}
 
 	return VisualElement::readAttribute(thread, result, attrib);
 }
@@ -228,6 +232,10 @@ MiniscriptInstructionOutcome MovieElement::writeRefAttribute(MiniscriptThread *t
 		DynamicValueWriteFuncHelper<MovieElement, &MovieElement::scriptSetVolume>::create(this, result);
 		return kMiniscriptInstructionOutcomeContinue;
 	}
+	if (attrib == "timevalue") {
+		DynamicValueWriteFuncHelper<MovieElement, &MovieElement::scriptSetTimestamp>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
 
 	return VisualElement::writeRefAttribute(thread, result, attrib);
 }
@@ -476,6 +484,29 @@ MiniscriptInstructionOutcome MovieElement::scriptSetVolume(MiniscriptThread *thr
 	return kMiniscriptInstructionOutcomeContinue;
 }
 
+MiniscriptInstructionOutcome MovieElement::scriptSetTimestamp(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger)) {
+		thread->error("Wrong type for movie element timevalue");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	if (asInteger < _playRange.min)
+		asInteger = _playRange.min;
+	else if (asInteger > _playRange.max)
+		asInteger = _playRange.max;
+
+	if (asInteger != _currentTimestamp) {
+		SeekToTimeTaskData *taskData = thread->getRuntime()->getVThread().pushTask("MovieElement::seekToTimeTask", this, &MovieElement::seekToTimeTask);
+		taskData->runtime = _runtime;
+		taskData->timestamp = asInteger;
+
+		return kMiniscriptInstructionOutcomeYieldToVThreadNoRetry;
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 MiniscriptInstructionOutcome MovieElement::scriptSetRangeStart(MiniscriptThread *thread, const DynamicValue &value) {
 	int32 asInteger = 0;
 	if (!value.roundToInt(asInteger)) {
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index cd3c4c01429..13d364f48ed 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -99,6 +99,7 @@ private:
 	MiniscriptInstructionOutcome scriptSetRangeEnd(MiniscriptThread *thread, const DynamicValue &value);
 	MiniscriptInstructionOutcome scriptRangeWriteRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib);
 	MiniscriptInstructionOutcome scriptSetVolume(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetTimestamp(MiniscriptThread *thread, const DynamicValue &value);
 
 	MiniscriptInstructionOutcome scriptSetRangeTyped(MiniscriptThread *thread, const IntRange &range);
 
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 90e0564d08d..3ee7b6006f0 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -657,6 +657,13 @@ VThreadState DragMotionModifier::consumeMessage(Runtime *runtime, const Common::
 	return kVThreadReturn;
 }
 
+VectorMotionModifier::~VectorMotionModifier() {
+	if (_scheduledEvent) {
+		_scheduledEvent->cancel();
+		_scheduledEvent.reset();
+	}
+}
+
 bool VectorMotionModifier::load(ModifierLoaderContext &context, const Data::VectorMotionModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -667,8 +674,123 @@ bool VectorMotionModifier::load(ModifierLoaderContext &context, const Data::Vect
 	return true;
 }
 
+bool VectorMotionModifier::respondsToEvent(const Event &evt) const {
+	return _enableWhen.respondsTo(evt) || _disableWhen.respondsTo(evt);
+}
+
+VThreadState VectorMotionModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	if (_enableWhen.respondsTo(msg->getEvent())) {
+		DynamicValue vec;
+		if (_vec.getType() == DynamicValueTypes::kIncomingData) {
+			vec = msg->getValue();
+		} else if (!_vecVar.expired()) {
+			Modifier *modifier = _vecVar.lock().get();
+
+			if (!modifier->isVariable()) {
+#ifdef MTROPOLIS_DEBUG_ENABLE
+				if (Debugger *debugger = runtime->debugGetDebugger())
+					debugger->notify(kDebugSeverityError, "Vector variable reference was to a non-variable");
+#endif
+				return kVThreadError;
+			}
+
+			VariableModifier *varModifier = static_cast<VariableModifier *>(modifier);
+			varModifier->varGetValue(nullptr, vec);
+		} else {
+#ifdef MTROPOLIS_DEBUG_ENABLE
+			if (Debugger *debugger = runtime->debugGetDebugger())
+				debugger->notify(kDebugSeverityError, "Vector variable reference wasn't resolved");
+#endif
+			return kVThreadError;
+		}
+
+		if (vec.getType() != DynamicValueTypes::kVector) {
+#ifdef MTROPOLIS_DEBUG_ENABLE
+			if (Debugger *debugger = runtime->debugGetDebugger())
+				debugger->notify(kDebugSeverityError, "Vector value was not actually a vector");
+#endif
+			return kVThreadError;
+		}
+
+		_resolvedVector = vec.getVector();
+
+		if (!_scheduledEvent) {
+			_lastTickTime = runtime->getPlayTime();
+			_subpixelX = 0;
+			_subpixelY = 0;
+			_scheduledEvent = runtime->getScheduler().scheduleMethod<VectorMotionModifier, &VectorMotionModifier::trigger>(_lastTickTime + 1, this);
+			return kVThreadReturn;
+		}
+	}
+	if (_disableWhen.respondsTo(msg->getEvent())) {
+		if (_scheduledEvent) {
+			_scheduledEvent->cancel();
+			_scheduledEvent.reset();
+		}
+		return kVThreadReturn;
+	}
+
+	return kVThreadReturn;
+}
+
+void VectorMotionModifier::trigger(Runtime *runtime) {
+	uint64 currentTime = runtime->getPlayTime();
+	_scheduledEvent = runtime->getScheduler().scheduleMethod<VectorMotionModifier, &VectorMotionModifier::trigger>(currentTime + 1, this);
+
+	double radians = _resolvedVector.angleDegrees * (M_PI / 180.0);
+
+	// Distance is per-tick, which is 1/60 of a sec, so the multiplier is 60.0/1000.0 or 0.06
+	// We then scale the entire thing up by 2^64, so the result is 60*65536/1000
+	double distance = static_cast<double>(currentTime - _lastTickTime) * _resolvedVector.magnitude * 3932.16;
+
+	int32 dx = static_cast<int32>(cos(radians) * distance) + static_cast<int32>(_subpixelX);
+	int32 dy = static_cast<int32>(-sin(radians) * distance) + static_cast<int32>(_subpixelY);
+	_subpixelX = (dx & 0xffff);
+	_subpixelY = (dy & 0xffff);
+
+	dx -= _subpixelX;
+	dy -= _subpixelY;
+
+	dx /= 65536;
+	dy /= 65536;
+
+	Structural *structural = findStructuralOwner();
+	if (structural->isElement()) {
+		Element *element = static_cast<Element *>(structural);
+		if (element->isVisual()) {
+			VisualElement *visual = static_cast<VisualElement *>(element);
+
+			VisualElement::OffsetTranslateTaskData *taskData = runtime->getVThread().pushTask("VisualElement::offsetTranslateTask", visual, &VisualElement::offsetTranslateTask);
+			taskData->dx = dx;
+			taskData->dy = dy;
+		}
+	}
+
+	_lastTickTime = currentTime;
+}
+
 Common::SharedPtr<Modifier> VectorMotionModifier::shallowClone() const {
-	return Common::SharedPtr<Modifier>(new VectorMotionModifier(*this));
+	Common::SharedPtr<VectorMotionModifier> clone(new VectorMotionModifier(*this));
+	clone->_scheduledEvent.reset();
+	return clone;
+}
+
+void VectorMotionModifier::linkInternalReferences(ObjectLinkingScope *scope) {
+	if (_vec.getType() == DynamicValueTypes::kVariableReference) {
+		const VarReference &varRef = _vec.getVarReference();
+		Common::WeakPtr<RuntimeObject> objRef = scope->resolve(varRef.guid, *varRef.source, false);
+
+		RuntimeObject *obj = objRef.lock().get();
+		if (obj == nullptr || !obj->isModifier()) {
+			warning("Vector motion modifier source was set to a variable, but the variable reference was invalid");
+		} else {
+			_vecVar = objRef.staticCast<Modifier>();
+		}
+	}
+}
+
+void VectorMotionModifier::visitInternalReferences(IStructuralReferenceVisitor* visitor) {
+	visitor->visitWeakModifierRef(_vecVar);
 }
 
 bool SceneTransitionModifier::load(ModifierLoaderContext &context, const Data::SceneTransitionModifier &data) {
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 0315dd65f98..dffe9921aca 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -300,19 +300,36 @@ private:
 
 class VectorMotionModifier : public Modifier {
 public:
+	~VectorMotionModifier();
+
 	bool load(ModifierLoaderContext &context, const Data::VectorMotionModifier &data);
 
+	bool respondsToEvent(const Event &evt) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Vector Modifier"; }
 #endif
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	void linkInternalReferences(ObjectLinkingScope *scope) override;
+	void visitInternalReferences(IStructuralReferenceVisitor *visitor) override;
+
+	void trigger(Runtime *runtime);
 
 	Event _enableWhen;
 	Event _disableWhen;
 
 	DynamicValue _vec;
+	Common::WeakPtr<Modifier> _vecVar;
+
+	AngleMagVector _resolvedVector;
+	uint16 _subpixelX;
+	uint16 _subpixelY;
+
+	Common::SharedPtr<ScheduledEvent> _scheduledEvent;
+	uint64 _lastTickTime;
 };
 
 class SceneTransitionModifier : public Modifier {
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index ce50e1823cd..6200cfc016c 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -6262,6 +6262,11 @@ void VisualElement::handleDragMotion(Runtime *runtime, const Point16 &initialPoi
 	}
 }
 
+VThreadState VisualElement::offsetTranslateTask(const OffsetTranslateTaskData& data) {
+	offsetTranslate(data.dx, data.dy, false);
+	return kVThreadReturn;
+}
+
 MiniscriptInstructionOutcome VisualElement::scriptSetVisibility(MiniscriptThread *thread, const DynamicValue &result) {
 	// FIXME: Need to make this fire Show/Hide events!
 	if (result.getType() == DynamicValueTypes::kBoolean) {
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 6b080a0aa90..bf8379e0830 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -2273,6 +2273,13 @@ public:
 
 	void handleDragMotion(Runtime *runtime, const Point16 &initialOrigin, const Point16 &targetOrigin);
 
+	struct OffsetTranslateTaskData {
+		int32 dx;
+		int32 dy;
+	};
+
+	VThreadState offsetTranslateTask(const OffsetTranslateTaskData &data);
+
 	virtual void render(Window *window) = 0;
 
 protected:


Commit: 9b0fae37c922492921440e596f5fc85b6b31c9cd
    https://github.com/scummvm/scummvm/commit/9b0fae37c922492921440e596f5fc85b6b31c9cd
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add media cue messenger, workaround for garbled section GUIDs in scene change modifiers

Changed paths:
    engines/mtropolis/elements.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 75b649f6773..4e06f7ffc34 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -352,6 +352,7 @@ void MovieElement::playMedia(Runtime *runtime, Project *project) {
 		return;
 
 	if (_videoDecoder) {
+		bool checkContinuously = false;
 		if (_shouldPlayIfNotPaused) {
 			if (_paused) {
 				// Goal state is paused
@@ -367,6 +368,7 @@ void MovieElement::playMedia(Runtime *runtime, Project *project) {
 					_videoDecoder->pauseVideo(false);
 
 				_currentPlayState = kMediaStatePlaying;
+				checkContinuously = true;
 			}
 		} else {
 			// Goal state is stopped
@@ -418,6 +420,11 @@ void MovieElement::playMedia(Runtime *runtime, Project *project) {
 		if (targetTS != _currentTimestamp) {
 			assert(!_paused);
 
+			
+			// Check media cues
+			for (MediaCueState *mediaCue : _mediaCues)
+				mediaCue->checkTimestampChange(runtime, _currentTimestamp, targetTS, checkContinuously, true);
+
 			_currentTimestamp = targetTS;
 
 			if (_currentTimestamp == maxTS) {
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 3ee7b6006f0..df774a38d5f 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -418,14 +418,25 @@ VThreadState ChangeSceneModifier::consumeMessage(Runtime *runtime, const Common:
 		if (_sceneSelectionType == kSceneSelectionTypeSpecific) {
 			Structural *project = runtime->getProject();
 			Structural *section = nullptr;
-			for (Common::Array<Common::SharedPtr<Structural> >::const_iterator it = project->getChildren().begin(), itEnd = project->getChildren().end(); it != itEnd; ++it) {
-				Structural *candidate = it->get();
-				assert(candidate->isSection());
-				if (candidate->getStaticGUID() == _targetSectionGUID) {
-					section = candidate;
-					break;
+
+			if (_targetSectionGUID == 0xfffffffeu) {
+				// For some reason, some scene change modifiers have a garbled section ID.  In that case, look in the current section.
+				Structural *sectionSearch = findStructuralOwner();
+				while (sectionSearch && !sectionSearch->isSection())
+					sectionSearch = sectionSearch->getParent();
+
+				section = sectionSearch;
+			} else {
+				for (Common::Array<Common::SharedPtr<Structural> >::const_iterator it = project->getChildren().begin(), itEnd = project->getChildren().end(); it != itEnd; ++it) {
+					Structural *candidate = it->get();
+					assert(candidate->isSection());
+					if (candidate->getStaticGUID() == _targetSectionGUID) {
+						section = candidate;
+						break;
+					}
 				}
 			}
+			
 
 			if (section) {
 				Structural *subsection = nullptr;
@@ -498,7 +509,7 @@ VThreadState ChangeSceneModifier::consumeMessage(Runtime *runtime, const Common:
 		if (targetScene) {
 			runtime->addSceneStateTransition(HighLevelSceneTransition(targetScene, HighLevelSceneTransition::kTypeChangeToScene, _addToDestList, _addToReturnList));
 		} else {
-			warning("Change Scene Modifier failed, subsection could not be resolved");
+			warning("Change Scene Modifier failed, scene could not be resolved");
 		}
 	}
 
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index bbb3d67837f..1691df1d1de 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -380,8 +380,11 @@ MiniscriptInstructionOutcome STransCtModifier::scriptSetSteps(MiniscriptThread *
 	return kMiniscriptInstructionOutcomeContinue;
 }
 
+MediaCueMessengerModifier::MediaCueMessengerModifier() : _isActive(false) {
+	_mediaCue.sourceModifier = this;
+}
 
-bool MediaCueMessengerModifier::load(const PlugInModifierLoaderContext& context, const Data::Standard::MediaCueMessengerModifier& data) {
+bool MediaCueMessengerModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::MediaCueMessengerModifier &data) {
 	if (data.enableWhen.type != Data::PlugInTypeTaggedValue::kEvent)
 		return false;
 
@@ -395,7 +398,7 @@ bool MediaCueMessengerModifier::load(const PlugInModifierLoaderContext& context,
 	if (data.triggerTiming.type != Data::PlugInTypeTaggedValue::kInteger)
 		return false;
 
-	_triggerTiming = static_cast<TriggerTiming>(data.triggerTiming.value.asInt);
+	_mediaCue.triggerTiming = static_cast<MediaCueState::TriggerTiming>(data.triggerTiming.value.asInt);
 
 	if (data.nonStandardMessageFlags.type != Data::PlugInTypeTaggedValue::kInteger)
 		return false;
@@ -406,14 +409,134 @@ bool MediaCueMessengerModifier::load(const PlugInModifierLoaderContext& context,
 	messageFlags.immediate = ((msgFlags & Data::Standard::MediaCueMessengerModifier::kMessageFlagImmediate) != 0);
 	messageFlags.cascade = ((msgFlags & Data::Standard::MediaCueMessengerModifier::kMessageFlagCascade) != 0);
 	messageFlags.relay = ((msgFlags & Data::Standard::MediaCueMessengerModifier::kMessageFlagRelay) != 0);
-	if (!_send.load(data.sendEvent, messageFlags, data.with, data.destination))
+	if (!_mediaCue.send.load(data.sendEvent, messageFlags, data.with, data.destination))
 		return false;
 
+	switch (data.executeAt.type) {
+	case Data::PlugInTypeTaggedValue::kInteger:
+		_cueSourceType = kCueSourceInteger;
+		_cueSource.asInt = data.executeAt.value.asInt;
+		break;
+	case Data::PlugInTypeTaggedValue::kIntegerRange:
+		_cueSourceType = kCueSourceIntegerRange;
+		if (!_cueSource.asIntRange.load(data.executeAt.value.asIntRange))
+			return false;
+		break;
+	case Data::PlugInTypeTaggedValue::kVariableReference:
+		_cueSourceType = kCueSourceVariableReference;
+		_cueSource.asVarRefGUID = data.executeAt.value.asVarRefGUID;
+		break;
+	case Data::PlugInTypeTaggedValue::kLabel:
+		_cueSourceType = kCueSourceLabel;
+		if (!_cueSource.asLabel.load(data.executeAt.value.asLabel))
+			return false;
+		break;
+	default:
+		return false;
+	}
+
 	return true;
 }
 
+bool MediaCueMessengerModifier::respondsToEvent(const Event &evt) const {
+	return _enableWhen.respondsTo(evt) || _disableWhen.respondsTo(evt);
+}
+
+VThreadState MediaCueMessengerModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	if (_enableWhen.respondsTo(msg->getEvent())) {
+		Structural *owner = findStructuralOwner();
+		if (owner && owner->isElement()) {
+
+			Element *element = static_cast<Element *>(owner);
+
+			switch (_cueSourceType) {
+			case kCueSourceInteger:
+				_mediaCue.minTime = _mediaCue.maxTime = _cueSource.asInt;
+				break;
+			case kCueSourceIntegerRange:
+				_mediaCue.minTime = _cueSource.asIntRange.min;
+				_mediaCue.maxTime = _cueSource.asIntRange.max;
+				break;
+			case kCueSourceLabel: {
+					int32 resolved = 0;
+					if (element->resolveMediaMarkerLabel(_cueSource.asLabel, resolved))
+						_mediaCue.minTime = _mediaCue.maxTime = resolved;
+					else {
+						warning("Failed to resolve media cue marker label");
+						return kVThreadError;
+					}
+				} break;
+			case kCueSourceVariableReference: {
+					Modifier *modifier = _cueSourceModifier.lock().get();
+					if (!modifier->isVariable()) {
+						warning("Media cue source variable couldn't be resolved");
+						return kVThreadReturn;
+					}
+
+					DynamicValue value;
+					static_cast<VariableModifier *>(modifier)->varGetValue(nullptr, value);
+
+					switch (value.getType()) {
+					case DynamicValueTypes::kInteger:
+						_mediaCue.minTime = _mediaCue.maxTime = value.getInt();
+						break;
+					case DynamicValueTypes::kIntegerRange:
+						_mediaCue.minTime = value.getIntRange().min;
+						_mediaCue.maxTime = value.getIntRange().max;
+						break;
+					case DynamicValueTypes::kFloat:
+						_mediaCue.minTime = _mediaCue.maxTime = static_cast<int32>(round(value.getFloat()));
+						break;
+					default:
+						warning("Media cue variable was not a usable type");
+						return kVThreadError;
+					}
+
+				} break;
+			default:
+				assert(false);	// Something wasn't handled in the loader
+				return kVThreadReturn;
+			}
+
+			element->addMediaCue(&_mediaCue);
+			_isActive = true;
+		}
+	}
+	if (_disableWhen.respondsTo(msg->getEvent())) {
+		if (_isActive) {
+			Structural *owner = findStructuralOwner();
+			if (owner && owner->isElement())
+				static_cast<Element *>(owner)->removeMediaCue(&_mediaCue);
+
+			_isActive = false;
+		}
+	}
+
+	return kVThreadReturn;
+}
+
 Common::SharedPtr<Modifier> MediaCueMessengerModifier::shallowClone() const {
-	return Common::SharedPtr<Modifier>(new MediaCueMessengerModifier(*this));
+	Common::SharedPtr<MediaCueMessengerModifier> clone(new MediaCueMessengerModifier(*this));
+	clone->_isActive = false;
+	clone->_mediaCue.sourceModifier = clone.get();
+	clone->_mediaCue.incomingData = DynamicValue();
+	return clone;
+}
+
+void MediaCueMessengerModifier::linkInternalReferences(ObjectLinkingScope *scope) {
+	if (_cueSourceType == kCueSourceVariableReference) {
+		Common::WeakPtr<RuntimeObject> obj = scope->resolve(_cueSource.asVarRefGUID);
+		RuntimeObject *objPtr = obj.lock().get();
+		if (objPtr && objPtr->isModifier())
+			_cueSourceModifier = obj.staticCast<Modifier>();
+	}
+
+	_mediaCue.send.linkInternalReferences(scope);
+}
+
+void MediaCueMessengerModifier::visitInternalReferences(IStructuralReferenceVisitor *visitor) {
+	visitor->visitWeakModifierRef(_cueSourceModifier);
+	_mediaCue.send.visitInternalReferences(visitor);
 }
 
 ObjectReferenceVariableModifier::ObjectReferenceVariableModifier() {
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index fdeeede69d4..d16defc9d18 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -95,25 +95,47 @@ private:
 
 class MediaCueMessengerModifier : public Modifier {
 public:
+	MediaCueMessengerModifier();
+
 	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::MediaCueMessengerModifier &data);
 
+	bool respondsToEvent(const Event &evt) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Media Cue Modifier"; }
 #endif
 
 private:
-	Common::SharedPtr<Modifier> shallowClone() const override;
+	enum CueSourceType {
+		kCueSourceInteger,
+		kCueSourceIntegerRange,
+		kCueSourceVariableReference,
+		kCueSourceLabel,
+	};
 
-	enum TriggerTiming {
-		kTriggerTimingStart = 0,
-		kTriggerTimingDuring = 1,
-		kTriggerTimingEnd = 2,
+	union CueSourceUnion {
+		int32 asInt;
+		IntRange asIntRange;
+		uint32 asVarRefGUID;
+		Label asLabel;
 	};
 
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
+	void linkInternalReferences(ObjectLinkingScope *scope) override;
+	void visitInternalReferences(IStructuralReferenceVisitor *visitor) override;
+
+	CueSourceType _cueSourceType;
+	CueSourceUnion _cueSource;
+
+	Common::WeakPtr<Modifier> _cueSourceModifier;
+
 	Event _enableWhen;
 	Event _disableWhen;
-	TriggerTiming _triggerTiming;
-	MessengerSendSpec _send;
+
+	MediaCueState _mediaCue;
+	bool _isActive;
 };
 
 class ObjectReferenceVariableModifier : public VariableModifier {
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 6200cfc016c..f524af285b5 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -5261,6 +5261,32 @@ void KeyboardEventSignaller::removeReceiver(IKeyboardEventReceiver *receiver) {
 	}
 }
 
+void MediaCueState::checkTimestampChange(Runtime *runtime, uint32 oldTS, uint32 newTS, bool continuousTimestamps, bool canTriggerDuring) {
+	bool entersRange = (oldTS < minTime && newTS >= minTime);
+	bool exitsRange = (oldTS <= maxTime && newTS > maxTime);
+	bool endsInRange = (newTS >= minTime && newTS <= maxTime);
+
+	bool shouldTrigger = false;
+	switch (triggerTiming)
+	{
+	case kTriggerTimingStart:
+		shouldTrigger = continuousTimestamps ? entersRange : endsInRange;
+		break;
+	case kTriggerTimingEnd:
+		shouldTrigger = continuousTimestamps ? exitsRange : false;
+		break;
+	case kTriggerTimingDuring:
+		shouldTrigger = canTriggerDuring ? endsInRange : false;
+		break;
+	default:
+		break;
+	}
+
+	// Given the positioning of this, there's not really a way for the immediate flag to have any effect?
+	if (shouldTrigger)
+		send.sendFromMessenger(runtime, sourceModifier, incomingData);
+}
+
 Project::Segment::Segment() : weakStream(nullptr) {
 }
 
@@ -6093,6 +6119,23 @@ uint32 Element::getStreamLocator() const {
 	return _streamLocator;
 }
 
+void Element::addMediaCue(MediaCueState *mediaCue) {
+	_mediaCues.push_back(mediaCue);
+}
+
+void Element::removeMediaCue(const MediaCueState *mediaCue) {
+	for (size_t i = 0; i < _mediaCues.size(); i++) {
+		if (_mediaCues[i] == mediaCue) {
+			_mediaCues.remove_at(i);
+			break;
+		}
+	}
+}
+
+bool Element::resolveMediaMarkerLabel(const Label& label, int32 &outResolution) const {
+	return false;
+}
+
 VisualElement::VisualElement() : _rect(Rect16::create(0, 0, 0, 0)), _cachedAbsoluteOrigin(Point16::create(0, 0)) {
 }
 
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index bf8379e0830..df7053c6b5d 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -2052,6 +2052,24 @@ private:
 	Common::SharedPtr<KeyboardEventSignaller> _signaller;
 };
 
+struct MediaCueState {
+	enum TriggerTiming {
+		kTriggerTimingStart = 0,
+		kTriggerTimingDuring = 1,
+		kTriggerTimingEnd = 2,
+	};
+
+	int32 minTime;
+	int32 maxTime;
+
+	Modifier *sourceModifier;
+	TriggerTiming triggerTiming;
+	MessengerSendSpec send;
+	DynamicValue incomingData;
+
+	void checkTimestampChange(Runtime *runtime, uint32 oldTS, uint32 newTS, bool continuousTimestamps, bool canTriggerDuring);
+};
+
 class Project : public Structural {
 public:
 	explicit Project(Runtime *runtime);
@@ -2235,9 +2253,16 @@ public:
 
 	uint32 getStreamLocator() const;
 
+	void addMediaCue(MediaCueState *mediaCue);
+	void removeMediaCue(const MediaCueState *mediaCue);
+
+	virtual bool resolveMediaMarkerLabel(const Label &label, int32 &outResolution) const;
+
 protected:
 	uint32 _streamLocator;
 	uint16 _sectionID;
+
+	Common::Array<MediaCueState *> _mediaCues;
 };
 
 class VisualElement : public Element {


Commit: b013a3ce5de38ebca170def2b5487e30af09e9cd
    https://github.com/scummvm/scummvm/commit/b013a3ce5de38ebca170def2b5487e30af09e9cd
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix reversed mToons firing the wrong events

Changed paths:
    engines/mtropolis/elements.cpp
    engines/mtropolis/modifiers.cpp


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 4e06f7ffc34..f129eb3f630 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -921,13 +921,13 @@ void MToonElement::playMedia(Runtime *runtime, Project *project) {
 			_frame = targetFrame;
 
 			if (_frame == maxFrame) {
-				Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kAtLastCel, 0), DynamicValue(), getSelfReference()));
+				Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(isReversed ? EventIDs::kAtFirstCel : EventIDs::kAtLastCel, 0), DynamicValue(), getSelfReference()));
 				Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
 				runtime->queueMessage(dispatch);
 			}
 
 			if (_frame == minFrame) {
-				Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kAtFirstCel, 0), DynamicValue(), getSelfReference()));
+				Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(isReversed ? EventIDs::kAtLastCel : EventIDs::kAtFirstCel, 0), DynamicValue(), getSelfReference()));
 				Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
 				runtime->queueMessage(dispatch);
 			}
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index df774a38d5f..f462cd64568 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -436,7 +436,6 @@ VThreadState ChangeSceneModifier::consumeMessage(Runtime *runtime, const Common:
 					}
 				}
 			}
-			
 
 			if (section) {
 				Structural *subsection = nullptr;


Commit: 09ca43e78ea2d130bc2943b78214ccf1a9dc0e1f
    https://github.com/scummvm/scummvm/commit/09ca43e78ea2d130bc2943b78214ccf1a9dc0e1f
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add XOR mod support

Changed paths:
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/plugin/obsidian.cpp
    engines/mtropolis/plugin/obsidian.h
    engines/mtropolis/plugin/obsidian_data.cpp
    engines/mtropolis/plugin/obsidian_data.h
    engines/mtropolis/render.cpp
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index f129eb3f630..cdbb443ad65 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -177,7 +177,296 @@ bool GraphicElement::load(ElementLoaderContext &context, const Data::GraphicElem
 }
 
 void GraphicElement::render(Window *window) {
-	// todo
+	if (_renderProps.getInkMode() == VisualElementRenderProperties::kInkModeDefault || _renderProps.getInkMode() == VisualElementRenderProperties::kInkModeInvisible || !_rect.isValid()) {
+		// Not rendered at all
+		_mask.reset();
+		return;
+	}
+
+	if (!_visible)
+		return;
+
+	const bool needsMask = (_renderProps.getShape() != VisualElementRenderProperties::kShapeRect);
+	bool needsMaskRedraw = _renderProps.isDirty();
+
+	uint16 width = _rect.getWidth();
+	uint16 height = _rect.getHeight();
+
+	if (needsMask) {
+		if (!_mask || _mask->w != width || _mask->h != height) {
+			_mask.reset();
+			_mask.reset(new Graphics::ManagedSurface());
+			_mask->create(_rect.getWidth(), _rect.getHeight(), Graphics::PixelFormat::createFormatCLUT8());
+
+			needsMaskRedraw = true;
+		}
+	} else {
+		_mask.reset();
+	}
+
+	if (needsMaskRedraw) {
+		Common::Array<Point16> starPoints;
+		const Common::Array<Point16> *polyPoints = nullptr;
+
+		VisualElementRenderProperties::Shape shape = _renderProps.getShape();
+		if (shape == VisualElementRenderProperties::kShapeStar) {
+			starPoints.resize(10);
+			starPoints[0] = Point16::create(width / 2, 0);
+			starPoints[1] = Point16::create(width * 2 / 3, height / 3);
+			starPoints[2] = Point16::create(width, height / 3);
+			starPoints[3] = Point16::create(width * 3 / 4, height / 2);
+			starPoints[4] = Point16::create(width, height);
+			starPoints[5] = Point16::create(width / 2, height * 2 / 3);
+			starPoints[6] = Point16::create(0, height);
+			starPoints[7] = Point16::create(width / 4, height / 2);
+			starPoints[8] = Point16::create(0, height / 3);
+			starPoints[9] = Point16::create(width / 3, height / 3);
+			polyPoints = &starPoints;
+
+			shape = VisualElementRenderProperties::kShapePolygon;
+		} else if (shape == VisualElementRenderProperties::kShapePolygon) {
+			polyPoints = &_renderProps.getPolyPoints();
+		}
+
+		// Notes for future:
+		// Rounded rect corner arc size is fixed at 13x13 unless the graphic is smaller.
+
+		if (shape == VisualElementRenderProperties::kShapePolygon && polyPoints->size() >= 3) {
+			_mask->clear(0);
+
+			Point16 firstPoint = (*polyPoints)[0];
+			for (uint polyStart = 1; polyStart < polyPoints->size() - 1; polyStart++) {
+				Point16 points[3];
+				points[0] = firstPoint;
+				points[1] = (*polyPoints)[polyStart];
+				points[2] = (*polyPoints)[polyStart + 1];
+
+				// Sort poly points into height ascending order
+				for (int sortStart = 0; sortStart < 2; sortStart++) {
+					Point16 *thisPoint = &points[sortStart];
+					Point16 *lowestY = thisPoint;
+					for (int candidateIndex = sortStart + 1; candidateIndex < 3; candidateIndex++) {
+						Point16 *candidate = &points[candidateIndex];
+						if (candidate->y < lowestY->y)
+							lowestY = candidate;
+					}
+
+					if (lowestY != thisPoint) {
+						Point16 temp = *thisPoint;
+						*thisPoint = *lowestY;
+						*lowestY = temp;
+					}
+				}
+
+				if (points[0].y == points[2].y)
+					continue; // Degenerate triangle
+
+				// Bin into 2 sets
+				Point16 *triPoints[2][3] = {{&points[0], &points[1], &points[2]},
+											{&points[2], &points[1], &points[0]}};
+
+				int32 yRanges[2][2] = {{points[0].y, points[1].y},
+									   {points[1].y, points[2].y}};
+
+				for (int half = 0; half < 2; half++) {
+					Point16 *commonPoint = triPoints[half][0];
+					Point16 *leftVert = triPoints[half][1];
+					Point16 *rightVert = triPoints[half][2];
+
+					if (leftVert->x == rightVert->x || commonPoint->y == points[1].y)
+						continue; // Degenerate tri
+
+					if (leftVert->x > rightVert->x) {
+						Point16 *temp = leftVert;
+						leftVert = rightVert;
+						rightVert = temp;
+					}
+
+					int32 minY = yRanges[half][0];
+					if (minY < 0)
+						minY = 0;
+					int32 maxY = yRanges[half][1];
+					if (maxY > static_cast<int32>(height))
+						maxY = height;
+
+					// Compute scanline rays
+					// In theory we'd want rays that are x=y*scale+const
+					// But since we're operating on pixel center space, what we actually want is
+					// x=(y+0.5)*scale+const-0.5
+					int32 rayScaleNum[2];
+					int32 rayConstNum[2];
+					int32 rayDenom[2];
+
+					int32 verts[2][2] = {{leftVert->x, leftVert->y},
+										 {rightVert->x, rightVert->y}};
+
+					for (int ray = 0; ray < 2; ray++) {
+						int32 x0 = verts[ray][0];
+						int32 y0 = verts[ray][1];
+						int32 x1 = commonPoint->x;
+						int32 y1 = commonPoint->y;
+
+						// Compute the base function for:
+						// x0=y0*scale+const
+						// x1=y1*scale+const
+						// Where a=x0, b=y0, c=x1, d=y1, x=scale, y=const
+						rayScaleNum[ray] = x0 - x1;
+						rayConstNum[ray] = y0 * x1 - x0 * y1;
+						rayDenom[ray] = y0 - y1;
+
+						// Half-pixel nudge y: x=(y+1/2)*scale+const-1/2
+						// x = (y*scale + 1/2*scale + const)/denom - 1/2
+						// x = (y*2*scale + scale + 2*const - denom)/2*denom
+						rayConstNum[ray] = 2 * rayConstNum[ray] + rayScaleNum[ray] - rayDenom[ray];
+						rayScaleNum[ray] *= 2;
+						rayDenom[ray] *= 2;
+
+						// Ensure the denominator is positive
+						if (rayDenom[ray] < 0) {
+							rayDenom[ray] = -rayDenom[ray];
+							rayScaleNum[ray] = -rayScaleNum[ray];
+							rayConstNum[ray] = -rayConstNum[ray];
+						}
+					}
+
+					for (int32 y = minY; y < maxY; y++) {
+						int32 xSpan[2];
+						for (int32 ray = 0; ray < 2; ray++) {
+							int32 xNum = y * rayScaleNum[ray] + rayConstNum[ray];
+							// Round up.  If x < 0 then the divide will be towards zero (up)
+							if (xNum >= 0)
+								xNum += rayDenom[ray] - 1;
+
+							int32 resolved = xNum / rayDenom[ray];
+							if (resolved < 0)
+								resolved = 0;
+							else if (resolved > width)
+								resolved = width;
+
+							xSpan[ray] = resolved;
+						}
+
+						int32 spanWidth = xSpan[1] - xSpan[0];
+						uint8 *bits = static_cast<uint8 *>(_mask->getBasePtr(xSpan[0], y));
+						for (int32 i = 0; i < spanWidth; i++)
+							bits[i] ^= 0xff;
+					}
+				}
+			}
+		} else if (shape == VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri1) {
+			// Upper-left right angle tri
+			_mask->clear(0);
+			for (int32 y = 0; y < height; y++) {
+				uint8 *scanline = static_cast<uint8 *>(_mask->getBasePtr(0, y));
+				int32 lineStart = 0;
+				int32 lineEnd = 64 - y;
+				for (int32 x = lineStart; x < lineEnd; x++)
+					scanline[x] = 0xff;
+			}
+		} else if (shape == VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri2) {
+			// Lower-left right-angle tri
+			_mask->clear(0);
+			for (int32 y = 0; y < height; y++) {
+				uint8 *scanline = static_cast<uint8 *>(_mask->getBasePtr(0, y));
+				int32 lineStart = 0;
+				int32 lineEnd = y;
+				for (int32 x = lineStart; x < lineEnd; x++)
+					scanline[x] = 0xff;
+			}
+		} else if (shape == VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri3) {
+			// Upper-right right-angle tri
+			_mask->clear(0);
+			for (int32 y = 0; y < height; y++) {
+				uint8 *scanline = static_cast<uint8 *>(_mask->getBasePtr(0, y));
+				int32 lineStart = y;
+				int32 lineEnd = 64;
+				for (int32 x = lineStart; x < lineEnd; x++)
+					scanline[x] = 0xff;
+			}
+		} else if (shape == VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri4) {
+			// Lower-right right-angle tri
+			_mask->clear(0);
+			for (int32 y = 0; y < height; y++) {
+				uint8 *scanline = static_cast<uint8 *>(_mask->getBasePtr(0, y));
+				int32 lineStart = 64 - y;
+				int32 lineEnd = 64;
+				for (int32 x = lineStart; x < lineEnd; x++)
+					scanline[x] = 0xff;
+			}
+		} else if (shape != VisualElementRenderProperties::kShapeRect) {
+			warning("Unimplemented graphic shape");
+			return;
+		}
+	}
+
+	Rect16 srcRect = Rect16::create(0, 0, _rect.getWidth(), _rect.getHeight());
+	Rect16 drawRect = srcRect.translate(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y);
+
+	Rect16 windowRect = Rect16::create(0, 0, window->getWidth(), window->getHeight());
+	Rect16 clippedDrawRect = drawRect.intersect(windowRect);
+
+	Rect16 clippedSrcRect = srcRect;
+	clippedSrcRect.left += clippedDrawRect.left - drawRect.left;
+	clippedSrcRect.top += clippedDrawRect.top - drawRect.top;
+	clippedSrcRect.right += clippedDrawRect.right - drawRect.right;
+	clippedSrcRect.bottom += clippedDrawRect.bottom - drawRect.bottom;
+
+	if (!clippedSrcRect.isValid())
+		return;
+
+	int32 srcToDestX = drawRect.left - clippedSrcRect.left;
+	int32 srcToDestY = drawRect.top - clippedSrcRect.top;
+
+	switch (_renderProps.getInkMode()) {
+	case VisualElementRenderProperties::kInkModeCopy:
+		break;
+	case VisualElementRenderProperties::kInkModeXor: {
+			const Graphics::PixelFormat &pixFmt = window->getPixelFormat();
+			uint32 colorMask = 0xff;
+
+			if (pixFmt.bytesPerPixel > 1)
+				colorMask = pixFmt.ARGBToColor(0, 255, 255, 255);
+
+			for (int32 srcY = clippedSrcRect.top; srcY < clippedSrcRect.bottom; srcY++) {
+				int32 destY = srcY + srcToDestY;
+				int32 spanWidth = clippedDrawRect.getWidth();
+				void *destPixels = window->getSurface()->getBasePtr(clippedDrawRect.left, srcY + srcToDestY);
+				if (_mask) {
+					const uint8 *maskBytes = static_cast<const uint8 *>(_mask->getBasePtr(clippedSrcRect.left, srcY));
+					if (pixFmt.bytesPerPixel == 1) {
+						for (int32 x = 0; x < spanWidth; x++) {
+							if (maskBytes[x])
+								static_cast<uint8 *>(destPixels)[x] ^= 0xff;
+						}
+					} else if (pixFmt.bytesPerPixel == 2) {
+						for (int32 x = 0; x < spanWidth; x++) {
+							if (maskBytes[x])
+								static_cast<uint16 *>(destPixels)[x] ^= colorMask;
+						}
+					} else if (pixFmt.bytesPerPixel == 4) {
+						for (int32 x = 0; x < spanWidth; x++) {
+							if (maskBytes[x])
+								static_cast<uint32 *>(destPixels)[x] ^= colorMask;
+						}
+					}
+				} else {
+					if (pixFmt.bytesPerPixel == 1) {
+						for (int32 x = 0; x < spanWidth; x++)
+							static_cast<uint8 *>(destPixels)[x] ^= 0xff;
+					} else if (pixFmt.bytesPerPixel == 2) {
+						for (int32 x = 0; x < spanWidth; x++)
+							static_cast<uint16 *>(destPixels)[x] ^= colorMask;
+					} else if (pixFmt.bytesPerPixel == 4) {
+						for (int32 x = 0; x < spanWidth; x++)
+							static_cast<uint32 *>(destPixels)[x] ^= colorMask;
+					}
+				}
+			}
+		} break;
+	default:
+		warning("Unimplemented graphic ink mode");
+		return;
+	}
 }
 
 MovieElement::MovieElement()
@@ -393,6 +682,7 @@ void MovieElement::playMedia(Runtime *runtime, Project *project) {
 			// tries decoding past the end, so we're assuming that the decoded frame memory stays valid until we
 			// actually have a new frame and continuing to use it.
 			if (decodedFrame) {
+				_contentsDirty = true;
 				framesDecodedThisFrame++;
 				_displayFrame = decodedFrame;
 				if (_playEveryFrame)
@@ -614,6 +904,7 @@ VThreadState MovieElement::seekToTimeTask(const SeekToTimeTaskData &taskData) {
 		_currentPlayState = kMediaStateStopped;
 	}
 	_needsReset = true;
+	_contentsDirty = true;
 
 	if (_currentTimestamp == minTS) {
 		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kAtFirstCel, 0), DynamicValue(), getSelfReference()));
@@ -842,6 +1133,8 @@ VThreadState MToonElement::startPlayingTask(const StartPlayingTaskData &taskData
 	_paused = false;
 	_isPlaying = false;	// Reset play state, it starts for real in playMedia
 
+	_contentsDirty = true;
+
 	Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kPlay, 0), DynamicValue(), getSelfReference()));
 	Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
 	taskData.runtime->sendMessageOnVThread(dispatch);
@@ -857,6 +1150,8 @@ VThreadState MToonElement::changeFrameTask(const ChangeFrameTaskData &taskData)
 	uint32 maxFrame = _playRange.max;
 	_frame = taskData.frame;
 
+	_contentsDirty = true;
+
 	if (_frame == minFrame) {
 		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kAtFirstCel, 0), DynamicValue(), getSelfReference()));
 		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
@@ -920,6 +1215,8 @@ void MToonElement::playMedia(Runtime *runtime, Project *project) {
 		if (_frame != targetFrame) {
 			_frame = targetFrame;
 
+			_contentsDirty = true;
+
 			if (_frame == maxFrame) {
 				Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(isReversed ? EventIDs::kAtFirstCel : EventIDs::kAtLastCel, 0), DynamicValue(), getSelfReference()));
 				Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index 13d364f48ed..88e397610bd 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -66,6 +66,8 @@ public:
 
 private:
 	bool _cacheBitmap;
+
+	Common::SharedPtr<Graphics::ManagedSurface> _mask;
 };
 
 class MovieElement : public VisualElement, public ISegmentUnloadSignalReceiver, public IPlayMediaSignalReceiver {
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 093460b84be..796725222ff 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -1961,6 +1961,11 @@ VThreadState MiniscriptThread::resume(const ResumeTaskData &taskData) {
 	if (instrsArray.size() == 0)
 		return kVThreadReturn;
 
+	if (_modifier->getStaticGUID() == 0x009725aa || _modifier->getStaticGUID() == 0x00972a68)
+	{
+		int n = 0;
+	}
+
 	MiniscriptInstruction *const *instrs = &instrsArray[0];
 	size_t numInstrs = instrsArray.size();
 
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index f462cd64568..d26898b20d4 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -1321,28 +1321,61 @@ Common::SharedPtr<Modifier> TextStyleModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new TextStyleModifier(*this));
 }
 
-bool GraphicModifier::load(ModifierLoaderContext& context, const Data::GraphicModifier& data) {
+bool GraphicModifier::load(ModifierLoaderContext &context, const Data::GraphicModifier &data) {
+	ColorRGB8 foreColor;
+	ColorRGB8 backColor;
+	ColorRGB8 borderColor;
+	ColorRGB8 shadowColor;
+
 	if (!loadTypicalHeader(data.modHeader) || !_applyWhen.load(data.applyWhen) || !_removeWhen.load(data.removeWhen)
-		|| !_foreColor.load(data.foreColor) || !_backColor.load(data.backColor)
-		|| !_borderColor.load(data.borderColor) || !_shadowColor.load(data.shadowColor))
+		|| !foreColor.load(data.foreColor) || !backColor.load(data.backColor)
+		|| !borderColor.load(data.borderColor) || !shadowColor.load(data.shadowColor))
 		return false;
 
 	// We need the poly points even if this isn't a poly shape since I think it's possible to change the shape type at runtime
-	_polyPoints.resize(data.polyPoints.size());
+	Common::Array<Point16> &polyPoints = _renderProps.modifyPolyPoints();
+	polyPoints.resize(data.polyPoints.size());
 	for (size_t i = 0; i < data.polyPoints.size(); i++) {
-		_polyPoints[i].x = data.polyPoints[i].x;
-		_polyPoints[i].y = data.polyPoints[i].y;
+		polyPoints[i].x = data.polyPoints[i].x;
+		polyPoints[i].y = data.polyPoints[i].y;
 	}
 
-	_inkMode = static_cast<InkMode>(data.inkMode);
-	_shape = static_cast<Shape>(data.shape);
-
-	_borderSize = data.borderSize;
-	_shadowSize = data.shadowSize;
+	_renderProps.setInkMode(static_cast<VisualElementRenderProperties::InkMode>(data.inkMode));
+	_renderProps.setShape(static_cast<VisualElementRenderProperties::Shape>(data.shape));
+	_renderProps.setBorderSize(data.borderSize);
+	_renderProps.setShadowSize(data.shadowSize);
+	_renderProps.setForeColor(foreColor);
+	_renderProps.setBackColor(backColor);
+	_renderProps.setBorderColor(borderColor);
+	_renderProps.setShadowColor(shadowColor);
 
 	return true;
 }
 
+bool GraphicModifier::respondsToEvent(const Event &evt) const {
+	return _applyWhen.respondsTo(evt) || _removeWhen.respondsTo(evt);
+}
+
+VThreadState GraphicModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	Structural *owner = findStructuralOwner();
+	if (!owner)
+		return kVThreadError;
+
+	if (!owner->isElement())
+		return kVThreadReturn;
+
+	Element *element = static_cast<Element *>(owner);
+	if (!element->isVisual())
+		return kVThreadReturn;
+
+	if (_applyWhen.respondsTo(msg->getEvent()))
+		static_cast<VisualElement *>(element)->setRenderProperties(_renderProps);
+	if (_removeWhen.respondsTo(msg->getEvent()))
+		static_cast<VisualElement *>(element)->setRenderProperties(VisualElementRenderProperties());
+
+	return kVThreadReturn;
+}
+
 Common::SharedPtr<Modifier> GraphicModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new GraphicModifier(*this));
 }
@@ -1377,7 +1410,7 @@ const Common::Array<Common::SharedPtr<Modifier> > &CompoundVariableModifier::get
 	return _children;
 }
 
-void CompoundVariableModifier::appendModifier(const Common::SharedPtr<Modifier>& modifier) {
+void CompoundVariableModifier::appendModifier(const Common::SharedPtr<Modifier> &modifier) {
 	_children.push_back(modifier);
 	modifier->setParent(getSelfReference());
 }
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index dffe9921aca..efd43672525 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -599,31 +599,12 @@ class GraphicModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::GraphicModifier &data);
 
-	enum InkMode {
-		kInkModeCopy = 0x0,
-		kInkModeTransparent = 0x1,				// src*dest
-		kInkModeGhost = 0x3,					// (1-src)+dest
-		kInkModeReverseCopy = 0x4,				// 1-src
-		kInkModeReverseGhost = 0x7,				// src+dest
-		kInkModeReverseTransparent = 0x9,		// (1-src)*dest
-		kInkModeBlend = 0x20,					// (src*bgcolor)+(dest*(1-bgcolor)
-		kInkModeBackgroundTransparent = 0x24,	// BG color is transparent
-		kInkModeChameleonDark = 0x25,			// src+dest
-		kInkModeChameleonLight = 0x27,			// src*dest
-		kInkModeBackgroundMatte = 0x224,		// BG color is transparent and non-interactive
-		kInkModeInvisible = 0xffff,				// Not drawn, but interactive
-	};
-
-	enum Shape {
-		kShapeRect = 0x1,
-		kShapeRoundedRect = 0x2,
-		kShapeOval = 0x3,
-		kShapePolygon = 0x9,
-		kShapeStar = 0xb,	// 5-point star, horizontal arms are at (top+bottom*2)/3
-	};
+	bool respondsToEvent(const Event &evt) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Graphic Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
@@ -631,17 +612,8 @@ private:
 
 	Event _applyWhen;
 	Event _removeWhen;
-	InkMode _inkMode;
-	Shape _shape;
-
-	ColorRGB8 _foreColor;
-	ColorRGB8 _backColor;
-	uint16 _borderSize;
-	ColorRGB8 _borderColor;
-	uint16 _shadowSize;
-	ColorRGB8 _shadowColor;
 
-	Common::Array<Point16> _polyPoints;
+	VisualElementRenderProperties _renderProps;
 };
 
 // Compound variable modifiers are not true variable modifiers.
diff --git a/engines/mtropolis/plugin/obsidian.cpp b/engines/mtropolis/plugin/obsidian.cpp
index f5ccdba6caf..dba283c43ba 100644
--- a/engines/mtropolis/plugin/obsidian.cpp
+++ b/engines/mtropolis/plugin/obsidian.cpp
@@ -198,14 +198,13 @@ MiniscriptInstructionOutcome TextWorkModifier::scriptSetFirstWord(MiniscriptThre
 			}
 		}
 		lastWasWhitespace = isWhitespace;
-
 	}
 
 	thread->error("Invalid index for 'firstword'");
 	return kMiniscriptInstructionOutcomeFailed;
 }
 
-MiniscriptInstructionOutcome TextWorkModifier::scriptSetLastWord(MiniscriptThread* thread, const DynamicValue& value) {
+MiniscriptInstructionOutcome TextWorkModifier::scriptSetLastWord(MiniscriptThread *thread, const DynamicValue &value) {
 	int32 asInteger = 0;
 	if (!value.roundToInt(asInteger))
 		return kMiniscriptInstructionOutcomeFailed;
@@ -243,8 +242,6 @@ MiniscriptInstructionOutcome TextWorkModifier::scriptSetLastWord(MiniscriptThrea
 	return kMiniscriptInstructionOutcomeFailed;
 }
 
-
-
 DictionaryModifier::DictionaryModifier() : _plugIn(nullptr) {
 }
 
@@ -264,7 +261,6 @@ bool DictionaryModifier::load(const PlugInModifierLoaderContext &context, const
 	return true;
 }
 
-
 bool DictionaryModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
 	if (attrib == "index") {
 		resolveStringIndex();
@@ -535,9 +531,243 @@ Common::SharedPtr<Modifier> WordMixerModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new WordMixerModifier(*this));
 }
 
+XorModModifier::XorModModifier() {
+}
+
+bool XorModModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::XorModModifier &data) {
+	if (data.enableWhen.type != Data::PlugInTypeTaggedValue::kEvent)
+		return false;
+
+	if (!_enableWhen.load(data.enableWhen.value.asEvent))
+		return false;
+
+	if (data.disableWhen.type != Data::PlugInTypeTaggedValue::kEvent)
+		return false;
+
+	if (!_disableWhen.load(data.disableWhen.value.asEvent))
+		return false;
+
+	if (data.shapeID.type != Data::PlugInTypeTaggedValue::kInteger)
+		return false;
+
+	_shapeID = data.shapeID.value.asInt;
+
+	return true;
+}
+
+bool XorModModifier::respondsToEvent(const Event &evt) const {
+	return _enableWhen.respondsTo(evt) || _disableWhen.respondsTo(evt);
+}
+
+VThreadState XorModModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	Structural *owner = findStructuralOwner();
+	if (!owner)
+		return kVThreadError;
+
+	if (!owner->isElement())
+		return kVThreadReturn;
+
+	Element *element = static_cast<Element *>(owner);
+	if (!element->isVisual())
+		return kVThreadReturn;
+
+	VisualElement *visual = static_cast<VisualElement *>(element);
+
+	VisualElementRenderProperties renderProps = visual->getRenderProperties();
+	renderProps.setInkMode(VisualElementRenderProperties::kInkModeXor);
+
+	if (_shapeID == 0)
+		renderProps.setShape(VisualElementRenderProperties::kShapeRect);
+	else
+		renderProps.setShape(static_cast<VisualElementRenderProperties::Shape>(VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri1 + _shapeID - 1));
+
+	visual->setRenderProperties(renderProps);
+
+	return kVThreadReturn;
+}
+
+Common::SharedPtr<Modifier> XorModModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new XorModModifier(*this));
+}
+
+XorCheckModifier::XorCheckModifier() : _allClear(false) {
+}
+
+bool XorCheckModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::XorCheckModifier &data) {
+	return true;
+}
+
+bool XorCheckModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "allclear") {
+		result.setBool(_allClear);
+		return true;
+	}
+
+	return Modifier::readAttribute(thread, result, attrib);
+}
+
+MiniscriptInstructionOutcome XorCheckModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
+	if (attrib == "checknow") {
+		DynamicValueWriteFuncHelper<XorCheckModifier, &XorCheckModifier::scriptSetCheckNow>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	return Modifier::writeRefAttribute(thread, result, attrib);
+}
+
+Common::SharedPtr<Modifier> XorCheckModifier::shallowClone() const {
+	return Common::SharedPtr<Modifier>(new XorCheckModifier(*this));
+}
+
+MiniscriptInstructionOutcome XorCheckModifier::scriptSetCheckNow(MiniscriptThread *thread, const DynamicValue &value) {
+	if (value.getType() != DynamicValueTypes::kBoolean)
+		return kMiniscriptInstructionOutcomeFailed;
+
+	if (!value.getBool())
+		return kMiniscriptInstructionOutcomeContinue;
+
+	Structural *scene = findStructuralOwner();
+	while (!scene->getParent()->isSubsection())
+		scene = scene->getParent();
+
+	Common::Array<VisualElement *> xorElements;
+	recursiveFindXorElements(scene, xorElements);
+
+	Rect16 triRects[4];
+	for (int i = 0; i < 4; i++)
+		triRects[i] = Rect16::create(0, 0, 0, 0);
+
+	Common::Array<Rect16> pendingRects;
+
+	for (VisualElement *element : xorElements) {
+		VisualElementRenderProperties::Shape shape = element->getRenderProperties().getShape();
+		Rect16 rect = element->getRelativeRect();
+		Point16 absOrigin = element->getCachedAbsoluteOrigin();
+		Rect16 absRect = rect.translate(absOrigin.x - rect.left, absOrigin.y - rect.top);
+
+		if (shape >= VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri1 && shape <= VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri4)
+			triRects[shape - VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri1] = absRect;
+		else
+			pendingRects.push_back(absRect);
+	}
+
+	// The canvas puzzle has 4 triangles, right-angled in each corner, pairs 1-4 and 2-3 form rects.
+	// It isn't possible to solve the puzzle unless both rects are formed.  So, we do this by forming the rects and
+	// then eliminating overlaps.  If the rects can't be formed, the puzzle fails.
+	if (triRects[0] == triRects[3])
+		pendingRects.push_back(triRects[0]);
+	else {
+		_allClear = false;
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+	if (triRects[1] == triRects[2])
+		pendingRects.push_back(triRects[1]);
+	else {
+		_allClear = false;
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	Common::Array<Rect16> maskedRects;
+	while (pendingRects.size() > 0) {
+		const Rect16 pendingRect = pendingRects.back();
+		pendingRects.pop_back();
+
+		bool hasIntersection = false;
+		size_t intersectionIndex = 0;
+		for (size_t j = 0; j < maskedRects.size(); j++) {
+			if (maskedRects[j].intersect(pendingRect).isValid()) {
+				hasIntersection = true;
+				intersectionIndex = j;
+			}
+		}
+
+		if (!hasIntersection) {
+			maskedRects.push_back(pendingRect);
+			continue;
+		}
+
+		if (pendingRect == maskedRects[intersectionIndex]) {
+			// Total overlap
+			maskedRects.remove_at(intersectionIndex);
+			continue;
+		}
+
+		const Rect16 intersectingRect = maskedRects[intersectionIndex];
+
+		// Try to subdivide the intersecting rect using one of the axes of the incoming rect.
+		// If this succeeds, requeue the intersecting rect fragments and add the pending rect
+		// to the workspace.  Since that amounts to replacement, just replace the rect.
+		if (sliceRectX(intersectingRect, pendingRect.left, pendingRects)
+			|| sliceRectX(intersectingRect, pendingRect.right, pendingRects)
+			|| sliceRectY(intersectingRect, pendingRect.top, pendingRects)
+			|| sliceRectY(intersectingRect, pendingRect.bottom, pendingRects)) {
+			maskedRects[intersectionIndex] = pendingRect;
+			continue;
+		}
+
+		// Try to subdivide the pending rect using one of the axes of the intersecting rect.
+		// If this succeeds, the fragments will be requeued and no further action is needed.
+		if (sliceRectX(pendingRect, intersectingRect.left, pendingRects)
+			|| sliceRectX(pendingRect, intersectingRect.right, pendingRects)
+			|| sliceRectY(pendingRect, intersectingRect.top, pendingRects)
+			|| sliceRectY(pendingRect, intersectingRect.bottom, pendingRects)) {
+			continue;
+		}
+
+		// This should never happen
+		assert(false);
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	_allClear = (maskedRects.size() == 0);
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+void XorCheckModifier::recursiveFindXorElements(Structural *structural, Common::Array<VisualElement *> &elements) {
+	for (const Common::SharedPtr<Structural> &child : structural->getChildren())
+		recursiveFindXorElements(child.get(), elements);
+
+	if (!structural->isElement())
+		return;
+
+	Element *element = static_cast<Element *>(structural);
+	if (!element->isVisual())
+		return;
+
+	VisualElement *visual = static_cast<VisualElement *>(element);
+	if (visual->getRenderProperties().getInkMode() == VisualElementRenderProperties::kInkModeXor)
+		elements.push_back(visual);
+}
+
+bool XorCheckModifier::sliceRectX(const Rect16 &rect, int32 x, Common::Array<Rect16> &outSlices) {
+	if (x > rect.left && x < rect.right) {
+		Rect16 leftSlice = Rect16::create(rect.left, rect.top, x, rect.bottom);
+		Rect16 rightSlice = Rect16::create(x, rect.top, rect.right, rect.bottom);
+		outSlices.push_back(leftSlice);
+		outSlices.push_back(rightSlice);
+		return true;
+	}
+
+	return false;
+}
+
+bool XorCheckModifier::sliceRectY(const Rect16 &rect, int32 y, Common::Array<Rect16> &outSlices) {
+	if (y > rect.top && y < rect.bottom) {
+		Rect16 topSlice = Rect16::create(rect.left, rect.top, rect.right, y);
+		Rect16 bottomSlice = Rect16::create(rect.left, y, rect.right, rect.bottom);
+		outSlices.push_back(topSlice);
+		outSlices.push_back(bottomSlice);
+		return true;
+	}
+
+	return false;
+}
+
 ObsidianPlugIn::ObsidianPlugIn(const Common::SharedPtr<WordGameData> &wgData)
 	: _movementModifierFactory(this), _rectShiftModifierFactory(this), _textWorkModifierFactory(this),
-	  _dictionaryModifierFactory(this), _wordMixerModifierFactory(this), _wgData(wgData) {
+	  _dictionaryModifierFactory(this), _wordMixerModifierFactory(this), _xorModModifierFactory(this),
+	  _xorCheckModifierFactory(this), _wgData(wgData) {
 }
 
 void ObsidianPlugIn::registerModifiers(IPlugInModifierRegistrar *registrar) const {
@@ -546,6 +776,8 @@ void ObsidianPlugIn::registerModifiers(IPlugInModifierRegistrar *registrar) cons
 	registrar->registerPlugInModifier("TextWork", &_textWorkModifierFactory);
 	registrar->registerPlugInModifier("Dictionary", &_dictionaryModifierFactory);
 	registrar->registerPlugInModifier("WordMixer", &_wordMixerModifierFactory);
+	registrar->registerPlugInModifier("xorMod", &_xorModModifierFactory);
+	registrar->registerPlugInModifier("xorCheck", &_xorCheckModifierFactory);
 }
 
 const Common::SharedPtr<WordGameData>& ObsidianPlugIn::getWordGameData() const {
diff --git a/engines/mtropolis/plugin/obsidian.h b/engines/mtropolis/plugin/obsidian.h
index e96ac431304..dfe4ee1dead 100644
--- a/engines/mtropolis/plugin/obsidian.h
+++ b/engines/mtropolis/plugin/obsidian.h
@@ -151,6 +151,53 @@ private:
 	const ObsidianPlugIn *_plugIn;
 };
 
+class XorModModifier : public Modifier {
+public:
+	XorModModifier();
+
+	bool load(const PlugInModifierLoaderContext &context, const Data::Obsidian::XorModModifier &data);
+
+	bool respondsToEvent(const Event &evt) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Xor Mod Modifier"; }
+#endif
+
+private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
+	Event _enableWhen;
+	Event _disableWhen;
+
+	int32 _shapeID;
+};
+
+class XorCheckModifier : public Modifier {
+public:
+	XorCheckModifier();
+
+	bool load(const PlugInModifierLoaderContext &context, const Data::Obsidian::XorCheckModifier &data);
+
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib);
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "Xor Check Modifier"; }
+#endif
+
+private:
+	Common::SharedPtr<Modifier> shallowClone() const override;
+
+	MiniscriptInstructionOutcome scriptSetCheckNow(MiniscriptThread *thread, const DynamicValue &value);
+
+	static void recursiveFindXorElements(Structural *structural, Common::Array<VisualElement *> &elements);
+	static bool sliceRectX(const Rect16 &rect, int32 x, Common::Array<Rect16> &outSlices);
+	static bool sliceRectY(const Rect16 &rect, int32 y, Common::Array<Rect16> &outSlices);
+
+	bool _allClear;
+};
+
 class ObsidianPlugIn : public MTropolis::PlugIn {
 public:
 	ObsidianPlugIn(const Common::SharedPtr<WordGameData> &wgData);
@@ -165,6 +212,8 @@ private:
 	PlugInModifierFactory<TextWorkModifier, Data::Obsidian::TextWorkModifier> _textWorkModifierFactory;
 	PlugInModifierFactory<WordMixerModifier, Data::Obsidian::WordMixerModifier> _wordMixerModifierFactory;
 	PlugInModifierFactory<DictionaryModifier, Data::Obsidian::DictionaryModifier> _dictionaryModifierFactory;
+	PlugInModifierFactory<XorModModifier, Data::Obsidian::XorModModifier> _xorModModifierFactory;
+	PlugInModifierFactory<XorCheckModifier, Data::Obsidian::XorCheckModifier> _xorCheckModifierFactory;
 
 	Common::SharedPtr<WordGameData> _wgData;
 };
diff --git a/engines/mtropolis/plugin/obsidian_data.cpp b/engines/mtropolis/plugin/obsidian_data.cpp
index 352534e7f41..97b4529831a 100644
--- a/engines/mtropolis/plugin/obsidian_data.cpp
+++ b/engines/mtropolis/plugin/obsidian_data.cpp
@@ -86,6 +86,27 @@ DataReadErrorCode DictionaryModifier::load(PlugIn &plugIn, const PlugInModifier
 	return kDataReadErrorNone;
 }
 
+
+DataReadErrorCode XorModModifier::load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) {
+	if (prefix.plugInRevision != 1)
+		return kDataReadErrorUnsupportedRevision;
+	
+	if (!enableWhen.load(reader) || !disableWhen.load(reader) || !shapeID.load(reader))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
+DataReadErrorCode XorCheckModifier::load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) {
+	if (prefix.plugInRevision != 1)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!unknown1Event.load(reader) || !unknown2Event.load(reader))
+		return kDataReadErrorReadFailed;
+
+	return kDataReadErrorNone;
+}
+
 } // End of namespace Obsidian
 
 } // End of namespace Data
diff --git a/engines/mtropolis/plugin/obsidian_data.h b/engines/mtropolis/plugin/obsidian_data.h
index 4e32b384452..87f0ef29c9a 100644
--- a/engines/mtropolis/plugin/obsidian_data.h
+++ b/engines/mtropolis/plugin/obsidian_data.h
@@ -84,6 +84,23 @@ protected:
 	DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override;
 };
 
+struct XorModModifier : public PlugInModifierData {
+	PlugInTypeTaggedValue enableWhen; // Probably "enable when"
+	PlugInTypeTaggedValue disableWhen; // Probably "disable when"
+	PlugInTypeTaggedValue shapeID;
+
+protected:
+	DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override;
+};
+
+struct XorCheckModifier : public PlugInModifierData {
+	PlugInTypeTaggedValue unknown1Event; // Probably "enable when"
+	PlugInTypeTaggedValue unknown2Event; // Probably "disable when"
+
+protected:
+	DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override;
+};
+
 } // End of namespace Obsidian
 
 } // End of namespace Data
diff --git a/engines/mtropolis/render.cpp b/engines/mtropolis/render.cpp
index 25ef49db6ec..a92ce77da99 100644
--- a/engines/mtropolis/render.cpp
+++ b/engines/mtropolis/render.cpp
@@ -241,6 +241,7 @@ static bool renderItemLess(const RenderItem &a, const RenderItem &b) {
 
 static void renderNormalElement(const RenderItem &item, Window *mainWindow) {
 	item.element->render(mainWindow);
+	item.element->finalizeRender();
 }
 
 static void renderDirectElement(const RenderItem &item, Window *mainWindow) {
@@ -248,6 +249,8 @@ static void renderDirectElement(const RenderItem &item, Window *mainWindow) {
 }
 
 void renderProject(Runtime *runtime, Window *mainWindow) {
+	bool sceneChanged = runtime->isSceneGraphDirty();
+
 	Common::Array<Structural *> scenes;
 	runtime->getScenesInRenderOrder(scenes);
 
@@ -269,11 +272,29 @@ void renderProject(Runtime *runtime, Window *mainWindow) {
 			Common::sort(directBucket.begin() + directStart, directBucket.end(), renderItemLess);
 	}
 
-	for (Common::Array<RenderItem>::const_iterator it = normalBucket.begin(), itEnd = normalBucket.end(); it != itEnd; ++it)
-		renderNormalElement(*it, mainWindow);
+	if (!sceneChanged) {
+		for (Common::Array<RenderItem>::const_iterator it = normalBucket.begin(), itEnd = normalBucket.end(); it != itEnd; ++it) {
+			if (it->element->needsRender())
+				sceneChanged = true;
+		}
+	}
+
+	if (!sceneChanged) {
+		for (Common::Array<RenderItem>::const_iterator it = directBucket.begin(), itEnd = directBucket.end(); it != itEnd; ++it) {
+			if (it->element->needsRender())
+				sceneChanged = true;
+		}
+	}
+
+	if (sceneChanged) {
+		for (Common::Array<RenderItem>::const_iterator it = normalBucket.begin(), itEnd = normalBucket.end(); it != itEnd; ++it)
+			renderNormalElement(*it, mainWindow);
 
-	for (Common::Array<RenderItem>::const_iterator it = directBucket.begin(), itEnd = directBucket.end(); it != itEnd; ++it)
-		renderDirectElement(*it, mainWindow);
+		for (Common::Array<RenderItem>::const_iterator it = directBucket.begin(), itEnd = directBucket.end(); it != itEnd; ++it)
+			renderDirectElement(*it, mainWindow);
+	}
+
+	runtime->clearSceneGraphDirty();
 }
 
 void convert32To16(Graphics::Surface &destSurface, const Graphics::Surface &srcSurface) {
@@ -293,7 +314,6 @@ void convert32To16(Graphics::Surface &destSurface, const Graphics::Surface &srcS
 			ditherPattern[y][x] <<= 3;
 	}
 
-
 	size_t w = srcSurface.w;
 	size_t h = srcSurface.h;
 
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index f524af285b5..adaba496746 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -315,6 +315,10 @@ bool Rect16::loadUnchecked(const Data::Rect &rect) {
 	return true;
 }
 
+Common::Rect Rect16::toScummvmRect() const {
+	return Common::Rect(left, top, right, bottom);
+}
+
 bool IntRange::load(const Data::IntRange &range) {
 	max = range.max;
 	min = range.min;
@@ -356,6 +360,15 @@ bool ColorRGB8::load(const Data::ColorRGB16 &color) {
 	return true;
 }
 
+ColorRGB8 ColorRGB8::create(uint8 r, uint8 g, uint8 b) {
+	ColorRGB8 result;
+	result.r = r;
+	result.g = g;
+	result.b = b;
+	return result;
+}
+
+
 MessageFlags::MessageFlags() : relay(true), cascade(true), immediate(true) {
 }
 
@@ -3436,7 +3449,7 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, ISaveUIProvider *saveProv
 	_displayWidth(1024), _displayHeight(768), _realTimeBase(0), _playTimeBase(0), _sceneTransitionState(kSceneTransitionStateNotTransitioning),
 	_lastFrameCursor(nullptr), _defaultCursor(new DefaultCursorGraphic()), _platform(kProjectPlatformUnknown),
 	_cachedMousePosition(Point16::create(0, 0)), _realMousePosition(Point16::create(0, 0)), _trackedMouseOutside(false),
-	_forceCursorRefreshOnce(true), _haveModifierOverrideCursor(false) {
+	  _forceCursorRefreshOnce(true), _haveModifierOverrideCursor(false), _sceneGraphChanged(false) {
 	_random.reset(new Common::RandomSource("mtropolis"));
 
 	_vthread.reset(new VThread());
@@ -3600,6 +3613,7 @@ bool Runtime::runFrame() {
 				executeTeardown(*it);
 			}
 			_pendingTeardowns.clear();
+			_sceneGraphChanged = true;
 			continue;
 		}
 
@@ -3616,6 +3630,7 @@ bool Runtime::runFrame() {
 			_pendingSceneTransitions.remove_at(0);
 
 			executeHighLevelSceneTransition(transition);
+			_sceneGraphChanged = true;
 			continue;
 		}
 
@@ -3858,7 +3873,7 @@ void Runtime::executeCompleteTransitionToScene(const Common::SharedPtr<Structura
 		if (stackedScene == targetScene) {
 			sceneAlreadyInStack = true;
 		} else {
-			queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneEnded, 0), _activeMainScene.get(), false, true);
+			queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneEnded, 0), _activeMainScene.get(), true, true);
 			queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kParentDisabled, 0), _activeMainScene.get(), true, true);
 			_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(_activeMainScene, LowLevelSceneStateTransitionAction::kUnload));
 
@@ -3871,14 +3886,14 @@ void Runtime::executeCompleteTransitionToScene(const Common::SharedPtr<Structura
 
 	if (targetSharedScene != _activeSharedScene) {
 		if (_activeSharedScene) {
-			queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneEnded, 0), _activeSharedScene.get(), false, true);
+			queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneEnded, 0), _activeSharedScene.get(), true, true);
 			queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kParentDisabled, 0), _activeSharedScene.get(), true, true);
 			_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(_activeSharedScene, LowLevelSceneStateTransitionAction::kUnload));
 		}
 
 		_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(targetSharedScene, LowLevelSceneStateTransitionAction::kLoad));
 		queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kParentEnabled, 0), targetSharedScene.get(), true, true);
-		queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneStarted, 0), targetSharedScene.get(), false, true);
+		queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneStarted, 0), targetSharedScene.get(), true, true);
 
 		SceneStackEntry sharedSceneEntry;
 		sharedSceneEntry.scene = targetSharedScene;
@@ -3889,7 +3904,7 @@ void Runtime::executeCompleteTransitionToScene(const Common::SharedPtr<Structura
 	if (!sceneAlreadyInStack) {
 		_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(targetScene, LowLevelSceneStateTransitionAction::kLoad));
 		queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kParentEnabled, 0), targetScene.get(), true, true);
-		queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneStarted, 0), targetScene.get(), false, true);
+		queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneStarted, 0), targetScene.get(), true, true);
 
 		SceneStackEntry sceneEntry;
 		sceneEntry.scene = targetScene;
@@ -3933,11 +3948,11 @@ void Runtime::executeHighLevelSceneTransition(const HighLevelSceneTransition &tr
 
 				if (sceneReturn.isAddToDestinationSceneTransition) {
 					// In this case we unload the active main scene and reactivate the old main
-					queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneEnded, 0), _activeMainScene.get(), false, true);
+					queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneEnded, 0), _activeMainScene.get(), true, true);
 					queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kParentDisabled, 0), _activeMainScene.get(), true, true);
 					_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(_activeMainScene, LowLevelSceneStateTransitionAction::kUnload));
 
-					queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneReactivated, 0), sceneReturn.scene.get(), false, true);
+					queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneReactivated, 0), sceneReturn.scene.get(), true, true);
 
 					_activeMainScene = sceneReturn.scene;
 
@@ -3967,18 +3982,18 @@ void Runtime::executeHighLevelSceneTransition(const HighLevelSceneTransition &tr
 					if (_activeMainScene == targetSharedScene)
 						error("Transitioned into scene currently being used as a target scene, this is not supported");
 
-					queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneDeactivated, 0), _activeMainScene.get(), false, true);
+					queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneDeactivated, 0), _activeMainScene.get(), true, true);
 
 					if (targetSharedScene != _activeSharedScene) {
 						if (_activeSharedScene) {
-							queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneEnded, 0), _activeSharedScene.get(), false, true);
+							queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneEnded, 0), _activeSharedScene.get(), true, true);
 							queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kParentDisabled, 0), _activeSharedScene.get(), true, true);
 							_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(_activeSharedScene, LowLevelSceneStateTransitionAction::kUnload));
 						}
 
 						_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(targetSharedScene, LowLevelSceneStateTransitionAction::kLoad));
 						queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kParentEnabled, 0), targetSharedScene.get(), true, true);
-						queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneStarted, 0), targetSharedScene.get(), false, true);
+						queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneStarted, 0), targetSharedScene.get(), true, true);
 
 						SceneStackEntry sharedSceneEntry;
 						sharedSceneEntry.scene = targetScene;
@@ -4000,7 +4015,7 @@ void Runtime::executeHighLevelSceneTransition(const HighLevelSceneTransition &tr
 					if (!sceneAlreadyInStack) {
 						_pendingLowLevelTransitions.push_back(LowLevelSceneStateTransitionAction(targetScene, LowLevelSceneStateTransitionAction::kLoad));
 						queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kParentEnabled, 0), targetScene.get(), true, true);
-						queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneStarted, 0), targetScene.get(), false, true);
+						queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneStarted, 0), targetScene.get(), true, true);
 
 						SceneStackEntry sceneEntry;
 						sceneEntry.scene = targetScene;
@@ -4301,7 +4316,7 @@ void Runtime::sendMessageOnVThread(const Common::SharedPtr<MessageDispatch> &dis
 			break;
 
 		case EventIDs::kSceneStarted:
-			extType = "Mouse Up Outside";
+			extType = "Scene Started";
 			break;
 		case EventIDs::kSceneEnded:
 			extType = "Scene Ended";
@@ -4906,6 +4921,17 @@ const Hacks &Runtime::getHacks() const {
 	return _hacks;
 }
 
+void Runtime::setSceneGraphDirty() {
+	_sceneGraphChanged = true;
+}
+
+void Runtime::clearSceneGraphDirty() {
+	_sceneGraphChanged = false;
+}
+
+bool Runtime::isSceneGraphDirty() const {
+	return _sceneGraphChanged;
+}
 
 void Runtime::ensureMainWindowExists() {
 	// Maybe there's a better spot for this
@@ -6136,7 +6162,121 @@ bool Element::resolveMediaMarkerLabel(const Label& label, int32 &outResolution)
 	return false;
 }
 
-VisualElement::VisualElement() : _rect(Rect16::create(0, 0, 0, 0)), _cachedAbsoluteOrigin(Point16::create(0, 0)) {
+
+VisualElementRenderProperties::VisualElementRenderProperties()
+	: _inkMode(kInkModeDefault), _shape(kShapeRect), _foreColor(ColorRGB8::create(0, 0, 0)), _backColor(ColorRGB8::create(255, 255, 255)),
+	  _borderColor(ColorRGB8::create(0, 0, 0)), _shadowColor(ColorRGB8::create(0, 0, 0)), _borderSize(0), _shadowSize(0), _isDirty(true) {
+}
+
+VisualElementRenderProperties::InkMode VisualElementRenderProperties::getInkMode() const {
+	return _inkMode;
+}
+
+void VisualElementRenderProperties::setInkMode(InkMode inkMode) {
+	_isDirty = true;
+	_inkMode = inkMode;
+}
+
+void VisualElementRenderProperties::setShape(Shape shape) {
+	_isDirty = true;
+	_shape = shape;
+}
+
+VisualElementRenderProperties::Shape VisualElementRenderProperties::getShape() const {
+	return _shape;
+}
+
+const ColorRGB8 &VisualElementRenderProperties::getForeColor() const {
+	return _foreColor;
+}
+
+void VisualElementRenderProperties::setForeColor(const ColorRGB8 &color) {
+	_isDirty = true;
+	_foreColor = color;
+}
+
+const ColorRGB8 &VisualElementRenderProperties::getBackColor() const {
+	return _backColor;
+}
+
+void VisualElementRenderProperties::setBackColor(const ColorRGB8 &color) {
+	_isDirty = true;
+	_backColor = color;
+}
+
+const ColorRGB8 &VisualElementRenderProperties::getBorderColor() const {
+	return _borderColor;
+}
+
+void VisualElementRenderProperties::setBorderColor(const ColorRGB8 &color) {
+	_isDirty = true;
+	_borderColor = color;
+}
+
+const ColorRGB8 &VisualElementRenderProperties::getShadowColor() const {
+	return _shadowColor;
+}
+
+void VisualElementRenderProperties::setShadowColor(const ColorRGB8 &color) {
+	_isDirty = true;
+	_shadowColor = color;
+}
+
+uint16 VisualElementRenderProperties::getBorderSize() const {
+	return _borderSize;
+}
+
+void VisualElementRenderProperties::setBorderSize(uint16 size) {
+	_isDirty = true;
+	_borderSize = size;
+}
+
+uint16 VisualElementRenderProperties::getShadowSize() const {
+	return _shadowSize;
+}
+
+void VisualElementRenderProperties::setShadowSize(uint16 size) {
+	_isDirty = true;
+	_shadowSize = size;
+}
+
+const Common::Array<Point16> &VisualElementRenderProperties::getPolyPoints() const {
+	return _polyPoints;
+}
+
+Common::Array<Point16> &VisualElementRenderProperties::modifyPolyPoints() {
+	_isDirty = true;
+	return _polyPoints;
+}
+
+bool VisualElementRenderProperties::isDirty() const {
+	return _isDirty;
+}
+
+void VisualElementRenderProperties::clearDirty() {
+	_isDirty = false;
+}
+
+VisualElementRenderProperties &VisualElementRenderProperties::operator=(const VisualElementRenderProperties &other) {
+	_inkMode = other._inkMode;
+	_shape = other._shape;
+	_foreColor = other._foreColor;
+	_backColor = other._backColor;
+	_borderSize = other._borderSize;
+	_borderColor = other._borderColor;
+	_shadowSize = other._shadowSize;
+	_shadowColor = other._shadowColor;
+
+	_polyPoints = other._polyPoints;
+
+	_isDirty = true;
+
+	return *this;
+}
+
+VisualElement::VisualElement()
+	: _rect(Rect16::create(0, 0, 0, 0)), _cachedAbsoluteOrigin(Point16::create(0, 0))
+	, _contentsDirty(true) {
 }
 
 bool VisualElement::isVisual() const {
@@ -6257,7 +6397,7 @@ void VisualElement::setCachedAbsoluteOrigin(const Point16 &absOrigin) {
 	_cachedAbsoluteOrigin = absOrigin;
 }
 
-void VisualElement::setDragMotionProperties(const Common::SharedPtr<DragMotionProperties>& dragProps) {
+void VisualElement::setDragMotionProperties(const Common::SharedPtr<DragMotionProperties> &dragProps) {
 	_dragProps = dragProps;
 }
 
@@ -6305,11 +6445,32 @@ void VisualElement::handleDragMotion(Runtime *runtime, const Point16 &initialPoi
 	}
 }
 
-VThreadState VisualElement::offsetTranslateTask(const OffsetTranslateTaskData& data) {
+VThreadState VisualElement::offsetTranslateTask(const OffsetTranslateTaskData &data) {
 	offsetTranslate(data.dx, data.dy, false);
 	return kVThreadReturn;
 }
 
+void VisualElement::setRenderProperties(const VisualElementRenderProperties &props) {
+	_renderProps = props;
+}
+
+const VisualElementRenderProperties &VisualElement::getRenderProperties() const {
+	return _renderProps;
+}
+
+bool VisualElement::needsRender() const {
+	if (_renderProps.isDirty() || _prevRect != _rect || _contentsDirty)
+		return true;
+
+	return false;
+}
+
+void VisualElement::finalizeRender() {
+	_renderProps.clearDirty();
+	_prevRect = _rect;
+	_contentsDirty = false;
+}
+
 MiniscriptInstructionOutcome VisualElement::scriptSetVisibility(MiniscriptThread *thread, const DynamicValue &result) {
 	// FIXME: Need to make this fire Show/Hide events!
 	if (result.getType() == DynamicValueTypes::kBoolean) {
@@ -6436,6 +6597,8 @@ MiniscriptInstructionOutcome VisualElement::scriptSetLayer(MiniscriptThread *thr
 				collision->_layer = _layer;
 		}
 		_layer = asInteger;
+
+		thread->getRuntime()->setSceneGraphDirty();
 	}
 
 	return kMiniscriptInstructionOutcomeContinue;
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index df7053c6b5d..d670c831ec1 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -58,6 +58,7 @@ namespace Graphics {
 struct WinCursorGroup;
 class MacCursor;
 class MacFontManager;
+class ManagedSurface;
 class Cursor;
 struct PixelFormat;
 struct Surface;
@@ -336,6 +337,35 @@ struct Rect16 {
 		result.bottom = bottom;
 		return result;
 	}
+
+	inline bool isValid() const {
+		return right > left && bottom > top;
+	}
+
+	inline Rect16 intersect(const Rect16 &other) const {
+		Rect16 result = *this;
+		if (result.left < other.left)
+			result.left = other.left;
+		if (result.top < other.top)
+			result.top = other.top;
+		if (result.right > other.right)
+			result.right = other.right;
+		if (result.bottom > other.bottom)
+			result.bottom = other.bottom;
+
+		return result;
+	}
+
+	inline Rect16 translate(int32 dx, int32 dy) const {
+		Rect16 result = *this;
+		result.left += dx;
+		result.right += dx;
+		result.top += dy;
+		result.bottom += dy;
+		return result;
+	}
+
+	Common::Rect toScummvmRect() const;
 };
 
 struct IntRange {
@@ -478,6 +508,15 @@ struct ColorRGB8 {
 	uint8 b;
 
 	bool load(const Data::ColorRGB16 &color);
+	static ColorRGB8 create(uint8 r, uint8 g, uint8 b);
+
+	inline bool operator==(const ColorRGB8 &other) const {
+		return r == other.r && g == other.g && b == other.b;
+	}
+
+	inline bool operator!=(const ColorRGB8 &other) const {
+		return !((*this) == other);
+	}
 };
 
 struct MessageFlags {
@@ -1559,6 +1598,10 @@ public:
 	Hacks &getHacks();
 	const Hacks &getHacks() const;
 
+	void setSceneGraphDirty();
+	void clearSceneGraphDirty();
+	bool isSceneGraphDirty() const;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	void debugSetEnabled(bool enabled);
 	void debugBreak();
@@ -1734,6 +1777,9 @@ private:
 	uint32 _modifierOverrideCursorID;
 	bool _haveModifierOverrideCursor;
 
+	// True if any elements were added to the scene, removed from the scene, or reparented since last draw
+	bool _sceneGraphChanged;
+
 	Hacks _hacks;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
@@ -2265,6 +2311,89 @@ protected:
 	Common::Array<MediaCueState *> _mediaCues;
 };
 
+class VisualElementRenderProperties {
+public:
+	VisualElementRenderProperties();
+
+	enum InkMode {
+		kInkModeCopy = 0x0,
+		kInkModeTransparent = 0x1,				// src*dest
+		kInkModeGhost = 0x3,					// (1-src)+dest
+		kInkModeReverseCopy = 0x4,				// 1-src
+		kInkModeReverseGhost = 0x7,				// src+dest
+		kInkModeReverseTransparent = 0x9,		// (1-src)*dest
+		kInkModeBlend = 0x20,					// (src*bgcolor)+(dest*(1-bgcolor)
+		kInkModeBackgroundTransparent = 0x24,	// BG color is transparent
+		kInkModeChameleonDark = 0x25,			// src+dest
+		kInkModeChameleonLight = 0x27,			// src*dest
+		kInkModeBackgroundMatte = 0x224,		// BG color is transparent and non-interactive
+		kInkModeInvisible = 0xffff,				// Not drawn, but interactive
+
+		kInkModeXor = 0x7ffffff0,				// Fake ink mode for Obsidian canvas puzzle, not a valid value from data
+		kInkModeDefault = 0x7fffffff,			// Not a valid value from data
+	};
+
+	enum Shape {
+		kShapeRect = 0x1,
+		kShapeRoundedRect = 0x2,
+		kShapeOval = 0x3,
+		kShapePolygon = 0x9,
+		kShapeStar = 0xb, // 5-point star, horizontal arms are at (top+bottom*2)/3
+
+		// Fake shapes for Obsidian canvas puzzle, not a valid from data
+		kShapeObsidianCanvasPuzzleTri1 = 0x7ffffff1,
+		kShapeObsidianCanvasPuzzleTri2 = 0x7ffffff2,
+		kShapeObsidianCanvasPuzzleTri3 = 0x7ffffff3,
+		kShapeObsidianCanvasPuzzleTri4 = 0x7ffffff4,
+	};
+
+	InkMode getInkMode() const;
+	void setInkMode(InkMode inkMode);
+
+	Shape getShape() const;
+	void setShape(Shape shape);
+
+	const ColorRGB8 &getForeColor() const;
+	void setForeColor(const ColorRGB8 &color);
+
+	const ColorRGB8 &getBackColor() const;
+	void setBackColor(const ColorRGB8 &color);
+
+	const ColorRGB8 &getBorderColor() const;
+	void setBorderColor(const ColorRGB8 &color);
+
+	const ColorRGB8 &getShadowColor() const;
+	void setShadowColor(const ColorRGB8 &color);
+
+	uint16 getBorderSize() const;
+	void setBorderSize(uint16 size);
+
+	uint16 getShadowSize() const;
+	void setShadowSize(uint16 size);
+
+	const Common::Array<Point16> &getPolyPoints() const;
+	Common::Array<Point16> &modifyPolyPoints();
+
+	bool isDirty() const;
+	void clearDirty();
+
+	VisualElementRenderProperties &operator=(const VisualElementRenderProperties &other);
+
+private:
+	InkMode _inkMode;
+	Shape _shape;
+	ColorRGB8 _foreColor;
+	ColorRGB8 _backColor;
+	uint16 _borderSize;
+	ColorRGB8 _borderColor;
+	uint16 _shadowSize;
+	ColorRGB8 _shadowColor;
+
+	Common::Array<Point16> _polyPoints;
+
+	bool _isDirty;
+};
+
 class VisualElement : public Element {
 public:
 	VisualElement();
@@ -2305,7 +2434,12 @@ public:
 
 	VThreadState offsetTranslateTask(const OffsetTranslateTaskData &data);
 
+	void setRenderProperties(const VisualElementRenderProperties &props);
+	const VisualElementRenderProperties &getRenderProperties() const;
+
+	bool needsRender() const;
 	virtual void render(Window *window) = 0;
+	void finalizeRender();
 
 protected:
 	bool loadCommon(const Common::String &name, uint32 guid, const Data::Rect &rect, uint32 elementFlags, uint16 layer, uint32 streamLocator, uint16 sectionID);
@@ -2342,6 +2476,10 @@ protected:
 	uint16 _layer;
 
 	Common::SharedPtr<DragMotionProperties> _dragProps;
+
+	VisualElementRenderProperties _renderProps;
+	Rect16 _prevRect;
+	bool _contentsDirty;
 };
 
 class NonVisualElement : public Element {


Commit: 1a8a0ed29ccfd6f5b6c09cf513a0e3bd1e1e2cbb
    https://github.com/scummvm/scummvm/commit/1a8a0ed29ccfd6f5b6c09cf513a0e3bd1e1e2cbb
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add "celcount" attribute

Changed paths:
    engines/mtropolis/elements.cpp


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index cdbb443ad65..54092b0d95e 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -1024,6 +1024,12 @@ bool MToonElement::readAttribute(MiniscriptThread *thread, DynamicValue &result,
 	} else if (attrib == "range") {
 		result.setIntRange(_playRange);
 		return true;
+	} else if (attrib == "celcount") {
+		if (_cachedMToon)
+			result.setInt(_cachedMToon->getMetadata()->frames.size());
+		else
+			result.setInt(0);
+		return true;
 	}
 
 	return VisualElement::readAttribute(thread, result, attrib);


Commit: 1975bd6b3ccaa6ab8ac59878d6c0a3ebff13ad7c
    https://github.com/scummvm/scummvm/commit/1975bd6b3ccaa6ab8ac59878d6c0a3ebff13ad7c
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Inherit asset name from asset on unnamed elements.  Fixes Obsidian Piazza not working.

Changed paths:
    engines/mtropolis/data.cpp
    engines/mtropolis/elements.cpp
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index 7ff00e2253b..46d33548b33 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -688,7 +688,7 @@ DataReadErrorCode AssetCatalog::load(DataReader& reader) {
 		if (!reader.readU32(asset.flags1) || !reader.readU16(asset.nameLength) || !reader.readU16(asset.alwaysZero) || !reader.readU32(asset.unknown1) || !reader.readU32(asset.filePosition) || !reader.readU32(asset.assetType) || !reader.readU32(asset.flags2))
 			return kDataReadErrorReadFailed;
 
-		if (!reader.readNonTerminatedStr(asset.name, asset.nameLength))
+		if (!reader.readTerminatedStr(asset.name, asset.nameLength))
 			return kDataReadErrorReadFailed;
 	}
 
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 54092b0d95e..f0e601cd3d8 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -600,6 +600,9 @@ void MovieElement::activate() {
 		StartPlayingTaskData *startPlayingTaskData = _runtime->getVThread().pushTask("MovieElement::startPlayingTask", this, &MovieElement::startPlayingTask);
 		startPlayingTaskData->runtime = _runtime;
 	}
+
+	if (_name.empty())
+		_name = project->getAssetNameByID(_assetID);
 }
 
 void MovieElement::deactivate() {
@@ -973,6 +976,9 @@ void ImageElement::activate() {
 	}
 
 	_cachedImage = static_cast<ImageAsset *>(asset.get())->loadAndCacheImage(_runtime);
+
+	if (_name.empty())
+		_name = project->getAssetNameByID(_assetID);
 }
 
 void ImageElement::deactivate() {
@@ -1096,6 +1102,9 @@ void MToonElement::activate() {
 
 	_playMediaSignaller = project->notifyOnPlayMedia(this);
 	_playRange = IntRange::create(1, _metadata->frames.size());
+
+	if (_name.empty())
+		_name = project->getAssetNameByID(_assetID);
 }
 
 void MToonElement::deactivate() {
@@ -1773,6 +1782,9 @@ void SoundElement::activate() {
 		StartPlayingTaskData *startPlayingTaskData = _runtime->getVThread().pushTask("SoundElement::startPlayingTask", this, &SoundElement::startPlayingTask);
 		startPlayingTaskData->runtime = _runtime;
 	}
+
+	if (_name.empty())
+		_name = project->getAssetNameByID(_assetID);
 }
 
 
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 796725222ff..0515e97c035 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -1181,7 +1181,9 @@ MiniscriptInstructionOutcome BuiltinFunc::executeStr2Num(MiniscriptThread *threa
 	}
 
 	const Common::String &str = inputDynamicValue.getString();
-	if (str.size() == 0 || !sscanf(str.c_str(), "%lf", &result)) {
+	if (str.empty())
+		result = 0.0;
+	else if (str.size() == 0 || !sscanf(str.c_str(), "%lf", &result)) {
 		thread->error("Couldn't parse number");
 		return kMiniscriptInstructionOutcomeFailed;
 	}
@@ -1961,11 +1963,6 @@ VThreadState MiniscriptThread::resume(const ResumeTaskData &taskData) {
 	if (instrsArray.size() == 0)
 		return kVThreadReturn;
 
-	if (_modifier->getStaticGUID() == 0x009725aa || _modifier->getStaticGUID() == 0x00972a68)
-	{
-		int n = 0;
-	}
-
 	MiniscriptInstruction *const *instrs = &instrsArray[0];
 	size_t numInstrs = instrsArray.size();
 
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index adaba496746..7f80d3cfef8 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -5536,6 +5536,13 @@ bool Project::isProject() const {
 	return true;
 }
 
+Common::String Project::getAssetNameByID(uint32 assetID) const {
+	if (assetID >= _assetsByID.size())
+		return Common::String();
+
+	return _assetsByID[assetID]->name;
+}
+
 Common::WeakPtr<Asset> Project::getAssetByID(uint32 assetID) const {
 	if (assetID >= _assetsByID.size())
 		return Common::WeakPtr<Asset>();
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index d670c831ec1..5c45169c915 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -2131,6 +2131,7 @@ public:
 
 	bool isProject() const override;
 
+	Common::String getAssetNameByID(uint32 assetID) const;
 	Common::WeakPtr<Asset> getAssetByID(uint32 assetID) const;
 	size_t getSegmentForStreamIndex(size_t streamIndex) const;
 	void openSegmentStream(int segmentIndex);


Commit: 547e02c7499f1896a1fda5c7fc754c95e7cbeb93
    https://github.com/scummvm/scummvm/commit/547e02c7499f1896a1fda5c7fc754c95e7cbeb93
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Handle direct assignment to variables

Changed paths:
    engines/mtropolis/runtime.cpp


diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 7f80d3cfef8..135d9cf4668 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -1712,6 +1712,17 @@ void DynamicValueWriteBoolHelper::create(bool *boolValue, DynamicValueWriteProxy
 DynamicValueWriteBoolHelper DynamicValueWriteBoolHelper::_instance;
 
 MiniscriptInstructionOutcome DynamicValueWriteObjectHelper::write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr ptrOrOffset) const {
+	RuntimeObject *obj = static_cast<RuntimeObject *>(objectRef);
+	if (obj->isModifier() && static_cast<Modifier *>(obj)->isVariable()) {
+		VariableModifier *var = static_cast<VariableModifier *>(obj);
+		if (var->varSetValue(thread, value))
+			return kMiniscriptInstructionOutcomeContinue;
+		else {
+			thread->error("Failed to assign value to variable");
+			return kMiniscriptInstructionOutcomeFailed;
+		}
+	}
+
 	thread->error("Can't write to read-only object value");
 	return kMiniscriptInstructionOutcomeFailed;
 }
@@ -2667,6 +2678,22 @@ MiniscriptInstructionOutcome Structural::writeRefAttribute(MiniscriptThread *thr
 		return kMiniscriptInstructionOutcomeContinue;
 	}
 
+	// Attempt to resolve as a child object
+	// Modifiers are first, then structural
+	for (const Common::SharedPtr<Modifier> &modifier : _modifiers) {
+		if (caseInsensitiveEqual(modifier->getName(), attrib)) {
+			DynamicValueWriteObjectHelper::create(modifier.get(), result);
+			return kMiniscriptInstructionOutcomeContinue;
+		}
+	}
+
+	for (const Common::SharedPtr<Structural> &child : _children) {
+		if (caseInsensitiveEqual(child->getName(), attrib)) {
+			DynamicValueWriteObjectHelper::create(child.get(), result);
+			return kMiniscriptInstructionOutcomeContinue;
+		}
+	}
+
 	return RuntimeObject::writeRefAttribute(thread, result, attrib);
 }
 


Commit: e8812e94444bfff03cf2231dbf5ba3413e406866
    https://github.com/scummvm/scummvm/commit/e8812e94444bfff03cf2231dbf5ba3413e406866
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add enhanced color support for 32-bit rendering

Changed paths:
    engines/mtropolis/mtropolis.cpp


diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index 2d2903bd1e4..e9fb99e90ad 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -362,6 +362,7 @@ Common::Error MTropolisEngine::run() {
 	int preferredWidth = 1024;
 	int preferredHeight = 768;
 	ColorDepthMode preferredColorDepthMode = kColorDepthMode8Bit;
+	ColorDepthMode enhancedColorDepthMode = kColorDepthMode8Bit;
 
 	_runtime.reset(new Runtime(_system, _mixer, this, this));
 
@@ -369,6 +370,7 @@ Common::Error MTropolisEngine::run() {
 		preferredWidth = 640;
 		preferredHeight = 480;
 		preferredColorDepthMode = kColorDepthMode16Bit;
+		enhancedColorDepthMode = kColorDepthMode32Bit;
 
 		_runtime->getHacks().ignoreMismatchedProjectNameInObjectLookups = true;
 
@@ -416,6 +418,7 @@ Common::Error MTropolisEngine::run() {
 		preferredWidth = 640;
 		preferredHeight = 480;
 		preferredColorDepthMode = kColorDepthMode16Bit;
+		enhancedColorDepthMode = kColorDepthMode32Bit;
 
 		_runtime->getHacks().ignoreMismatchedProjectNameInObjectLookups = true;
 
@@ -496,15 +499,26 @@ Common::Error MTropolisEngine::run() {
 		}
 	}
 
-	// Figure out a pixel format.  First try to find one that's at least as good or better.
+	// Figure out a pixel format.  First try to find one that's at least as good or better than the enhanced mode
 	ColorDepthMode selectedMode = kColorDepthModeInvalid;
-	for (int i = preferredColorDepthMode; i < kColorDepthModeCount; i++) {
+
+	for (int i = enhancedColorDepthMode; i < kColorDepthModeCount; i++) {
 		if (haveExactMode[i] || haveCloseMode[i]) {
 			selectedMode = static_cast<ColorDepthMode>(i);
 			break;
 		}
 	}
 
+	// If that fails, find one that's at least as good as the preferred mode
+	if (selectedMode == kColorDepthModeInvalid) {
+		for (int i = preferredColorDepthMode; i < kColorDepthModeCount; i++) {
+			if (haveExactMode[i] || haveCloseMode[i]) {
+				selectedMode = static_cast<ColorDepthMode>(i);
+				break;
+			}
+		}
+	}
+
 	// If that fails, then try to find the best one available
 	if (selectedMode == kColorDepthModeInvalid) {
 		for (int i = preferredColorDepthMode - 1; i >= 0; i++) {
@@ -524,8 +538,12 @@ Common::Error MTropolisEngine::run() {
 			_runtime->setupDisplayMode(static_cast<ColorDepthMode>(i), modePixelFormats[i]);
 	}
 
+	ColorDepthMode fakeMode = selectedMode;
+	if (selectedMode == enhancedColorDepthMode)
+		fakeMode = preferredColorDepthMode;
+
 	// Set active mode
-	_runtime->switchDisplayMode(selectedMode, selectedMode);
+	_runtime->switchDisplayMode(selectedMode, fakeMode);
 	_runtime->setDisplayResolution(preferredWidth, preferredHeight);
 
 	initGraphics(preferredWidth, preferredHeight, &modePixelFormats[selectedMode]);


Commit: 5eb9c0cf8c9ba184daf70bfa2de6842f4783bcf5
    https://github.com/scummvm/scummvm/commit/5eb9c0cf8c9ba184daf70bfa2de6842f4783bcf5
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add transparent mToon blit mode

Changed paths:
    engines/mtropolis/debug.h
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h


diff --git a/engines/mtropolis/debug.h b/engines/mtropolis/debug.h
index 46a64a92f1e..80877f7491b 100644
--- a/engines/mtropolis/debug.h
+++ b/engines/mtropolis/debug.h
@@ -49,9 +49,9 @@ class RuntimeObject;
 struct IDebuggable;
 
 enum SupportStatus {
-	kSupportStatusNone,
-	kSupportStatusPartial,
-	kSupportStatusDone,
+	kSupportStatusNone,		// Only loads, not functional
+	kSupportStatusPartial,	// Partially-implemented, some use cases may silently fail
+	kSupportStatusDone,		// Not necessarily "done" but "done" enough that unsupported cases will trigger errors
 };
 
 struct IDebugInspectionReport {
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index f0e601cd3d8..f0aa6ccb98e 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -1138,7 +1138,17 @@ void MToonElement::render(Window *window) {
 			}
 			destRect = Common::Rect(_cachedAbsoluteOrigin.x + frameRect.left, _cachedAbsoluteOrigin.y + frameRect.top, _cachedAbsoluteOrigin.x + frameRect.right, _cachedAbsoluteOrigin.y + frameRect.bottom);
 
-			window->getSurface()->blitFrom(*_renderSurface, srcRect, destRect);
+			VisualElementRenderProperties::InkMode inkMode = _renderProps.getInkMode();
+			if (inkMode == VisualElementRenderProperties::kInkModeBackgroundMatte || inkMode == VisualElementRenderProperties::kInkModeBackgroundTransparent) {
+				ColorRGB8 transColorRGB8 = _renderProps.getBackColor();
+				uint32 transColor = _renderSurface->format.ARGBToColor(255, transColorRGB8.r, transColorRGB8.g, transColorRGB8.b);
+
+				window->getSurface()->transBlitFrom(*_renderSurface, srcRect, destRect, transColor);
+			} else if (inkMode == VisualElementRenderProperties::kInkModeCopy || inkMode == VisualElementRenderProperties::kInkModeDefault) {
+				window->getSurface()->blitFrom(*_renderSurface, srcRect, destRect);
+			} else {
+				warning("Unsupported mToon ink mode");
+			}
 		}
 	}
 }
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index 88e397610bd..9add227f438 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -61,7 +61,7 @@ public:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Graphic Element"; }
-	SupportStatus debugGetSupportStatus() const override { return kSupportStatusPartial; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:


Commit: 956c85c259df483c4469b72b7ae2524bc3870ad8
    https://github.com/scummvm/scummvm/commit/956c85c259df483c4469b72b7ae2524bc3870ad8
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add NoteNum attribute

Changed paths:
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h


diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 1691df1d1de..48b2940402b 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -1000,6 +1000,9 @@ MiniscriptInstructionOutcome MidiModifier::writeRefAttribute(MiniscriptThread *t
 	} else if (attrib == "notevelocity") {
 		DynamicValueWriteFuncHelper<MidiModifier, &MidiModifier::scriptSetNoteVelocity>::create(this, result);
 		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "notenum") {
+		DynamicValueWriteFuncHelper<MidiModifier, &MidiModifier::scriptSetNoteNum>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
 	}
 
 	return Modifier::writeRefAttribute(thread, result, attrib);
@@ -1052,6 +1055,24 @@ MiniscriptInstructionOutcome MidiModifier::scriptSetNoteVelocity(MiniscriptThrea
 	return kMiniscriptInstructionOutcomeContinue;
 }
 
+MiniscriptInstructionOutcome MidiModifier::scriptSetNoteNum(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger))
+		return kMiniscriptInstructionOutcomeFailed;
+
+	if (asInteger < 0)
+		asInteger = 0;
+	else if (asInteger > 255)
+		asInteger = 255;
+
+	if (_mode == kModeSingleNote) {
+		debug(2, "MIDI (%x '%s'): Changing note number to %i", getStaticGUID(), getName().c_str(), asInteger);
+		_modeSpecific.singleNote.note = asInteger;
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 ListVariableModifier::ListVariableModifier() : _list(new DynamicList()), _preferredContentType(DynamicValueTypes::kInteger) {
 }
 
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index d16defc9d18..004560b56b4 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -224,6 +224,7 @@ private:
 
 	MiniscriptInstructionOutcome scriptSetVolume(MiniscriptThread *thread, const DynamicValue &value);
 	MiniscriptInstructionOutcome scriptSetNoteVelocity(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetNoteNum(MiniscriptThread *thread, const DynamicValue &value);
 
 	struct FilePart {
 		bool loop;


Commit: 86eaedfa0c3c0bcca75dcd81c938ada8aced8e03
    https://github.com/scummvm/scummvm/commit/86eaedfa0c3c0bcca75dcd81c938ada8aced8e03
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Implement graphic modifiers, add close project command

Changed paths:
    engines/mtropolis/elements.cpp
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index f0aa6ccb98e..45cd9a7f99f 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -418,8 +418,47 @@ void GraphicElement::render(Window *window) {
 	int32 srcToDestY = drawRect.top - clippedSrcRect.top;
 
 	switch (_renderProps.getInkMode()) {
-	case VisualElementRenderProperties::kInkModeCopy:
-		break;
+	case VisualElementRenderProperties::kInkModeCopy: {
+			const Graphics::PixelFormat &pixFmt = window->getPixelFormat();
+			const ColorRGB8 fillColorRGB8 = _renderProps.getForeColor();
+			uint32 fillColor = pixFmt.ARGBToColor(255, fillColorRGB8.r, fillColorRGB8.g, fillColorRGB8.b);
+
+			for (int32 srcY = clippedSrcRect.top; srcY < clippedSrcRect.bottom; srcY++) {
+				int32 destY = srcY + srcToDestY;
+				int32 spanWidth = clippedDrawRect.getWidth();
+				void *destPixels = window->getSurface()->getBasePtr(clippedDrawRect.left, srcY + srcToDestY);
+				if (_mask) {
+					const uint8 *maskBytes = static_cast<const uint8 *>(_mask->getBasePtr(clippedSrcRect.left, srcY));
+					if (pixFmt.bytesPerPixel == 1) {
+						for (int32 x = 0; x < spanWidth; x++) {
+							if (maskBytes[x])
+								static_cast<uint8 *>(destPixels)[x] = fillColor;
+						}
+					} else if (pixFmt.bytesPerPixel == 2) {
+						for (int32 x = 0; x < spanWidth; x++) {
+							if (maskBytes[x])
+								static_cast<uint16 *>(destPixels)[x] = fillColor;
+						}
+					} else if (pixFmt.bytesPerPixel == 4) {
+						for (int32 x = 0; x < spanWidth; x++) {
+							if (maskBytes[x])
+								static_cast<uint32 *>(destPixels)[x] = fillColor;
+						}
+					}
+				} else {
+					if (pixFmt.bytesPerPixel == 1) {
+						for (int32 x = 0; x < spanWidth; x++)
+							static_cast<uint8 *>(destPixels)[x] = fillColor;
+					} else if (pixFmt.bytesPerPixel == 2) {
+						for (int32 x = 0; x < spanWidth; x++)
+							static_cast<uint16 *>(destPixels)[x] = fillColor;
+					} else if (pixFmt.bytesPerPixel == 4) {
+						for (int32 x = 0; x < spanWidth; x++)
+							static_cast<uint32 *>(destPixels)[x] = fillColor;
+					}
+				}
+			}
+		} break;
 	case VisualElementRenderProperties::kInkModeXor: {
 			const Graphics::PixelFormat &pixFmt = window->getPixelFormat();
 			uint32 colorMask = 0xff;
@@ -1613,6 +1652,7 @@ void TextLabelElement::render(Window *window) {
 
 void TextLabelElement::setTextStyle(uint16 macFontID, const Common::String &fontFamilyName, uint size, TextAlignment alignment, const TextStyleFlags &styleFlags) {
 	_needsRender = true;
+	_contentsDirty = true;
 
 	_macFontID = macFontID;
 	_fontFamilyName = fontFamilyName;
@@ -1629,6 +1669,7 @@ MiniscriptInstructionOutcome TextLabelElement::scriptSetText(MiniscriptThread *t
 
 	_text = value.getString();
 	_needsRender = true;
+	_contentsDirty = true;
 	_macFormattingSpans.clear();
 
 	return kMiniscriptInstructionOutcomeContinue;
@@ -1655,6 +1696,7 @@ MiniscriptInstructionOutcome TextLabelElement::scriptSetLine(MiniscriptThread *t
 	}
 
 	_needsRender = true;
+	_contentsDirty = true;
 	_macFormattingSpans.clear();
 	
 	return kMiniscriptInstructionOutcomeContinue;
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index e9fb99e90ad..491a50ab6c0 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -569,7 +569,9 @@ Common::Error MTropolisEngine::run() {
 			paused = _runtime->debugGetDebugger()->isPaused();
 #endif
 
-		_runtime->runFrame();
+		if (!_runtime->runFrame())
+			break;
+
 		_runtime->drawFrame();
 	}
 
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 135d9cf4668..a65d4da6552 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -2870,6 +2870,16 @@ void Structural::materializeDescendents(Runtime *runtime, ObjectLinkingScope *ou
 }
 
 VThreadState Structural::consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	if (Event::create(EventIDs::kUnpause, 0).respondsTo(msg->getEvent())) {
+		_paused = false;
+
+		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kUnpause, 0), DynamicValue(), getSelfReference()));
+		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+		runtime->sendMessageOnVThread(dispatch);
+
+		return kVThreadReturn;
+	}
+
 	warning("Command type %i was ignored", msg->getEvent().eventType);
 	return kVThreadReturn;
 }
@@ -3476,7 +3486,7 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, ISaveUIProvider *saveProv
 	_displayWidth(1024), _displayHeight(768), _realTimeBase(0), _playTimeBase(0), _sceneTransitionState(kSceneTransitionStateNotTransitioning),
 	_lastFrameCursor(nullptr), _defaultCursor(new DefaultCursorGraphic()), _platform(kProjectPlatformUnknown),
 	_cachedMousePosition(Point16::create(0, 0)), _realMousePosition(Point16::create(0, 0)), _trackedMouseOutside(false),
-	  _forceCursorRefreshOnce(true), _haveModifierOverrideCursor(false), _sceneGraphChanged(false) {
+	  _forceCursorRefreshOnce(true), _haveModifierOverrideCursor(false), _sceneGraphChanged(false), _isQuitting(false) {
 	_random.reset(new Common::RandomSource("mtropolis"));
 
 	_vthread.reset(new VThread());
@@ -3688,6 +3698,9 @@ bool Runtime::runFrame() {
 			}
 		}
 
+		if (_isQuitting)
+			return false;
+
 		// Ran out of actions
 		break;
 	}
@@ -5008,6 +5021,13 @@ void Runtime::queueProject(const Common::SharedPtr<ProjectDescription> &desc) {
 	_queuedProjectDesc = desc;
 }
 
+void Runtime::closeProject() {
+	// TODO: There are actually some elaborate cases here involving opening projects, project return list,
+	// Project Ended message, etc. and Obsidian actually attempts to stop MIDI playback on project end.
+	// For now we just quit.
+	_isQuitting = true;
+}
+
 void Runtime::addVolume(int volumeID, const char *name, bool isMounted) {
 	VolumeState volume;
 	volume.name = name;
@@ -5354,6 +5374,15 @@ Project::~Project() {
 		closeSegmentStream(i);
 }
 
+VThreadState Project::consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	if (Event::create(EventIDs::kCloseProject, 0).respondsTo(msg->getEvent())) {
+		runtime->closeProject();
+		return kVThreadReturn;
+	}
+
+	return Structural::consumeCommand(runtime, msg);
+}
+
 void Project::loadFromDescription(const ProjectDescription& desc) {
 	_resources = desc.getResources();
 	_cursorGraphics = desc.getCursorGraphics();
@@ -6382,7 +6411,7 @@ MiniscriptInstructionOutcome VisualElement::writeRefAttribute(MiniscriptThread *
 		DynamicValueWriteOrRefAttribFuncHelper<VisualElement, &VisualElement::scriptSetPosition, &VisualElement::scriptWriteRefPositionAttribute>::create(this, writeProxy);
 		return kMiniscriptInstructionOutcomeContinue;
 	} else if (attrib == "centerposition") {
-		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetCenterPosition>::create(this, writeProxy);
+		DynamicValueWriteOrRefAttribFuncHelper<VisualElement, &VisualElement::scriptSetCenterPosition, &VisualElement::scriptWriteRefCenterPositionAttribute>::create(this, writeProxy);
 		return kMiniscriptInstructionOutcomeContinue;
 	} else if (attrib == "width") {
 		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetWidth>::create(this, writeProxy);
@@ -6596,6 +6625,32 @@ MiniscriptInstructionOutcome VisualElement::scriptSetCenterPosition(MiniscriptTh
 	return kMiniscriptInstructionOutcomeFailed;
 }
 
+MiniscriptInstructionOutcome VisualElement::scriptSetCenterPositionX(MiniscriptThread *thread, const DynamicValue &dest) {
+	int32 asInteger = 0;
+	if (!dest.roundToInt(asInteger))
+		return kMiniscriptInstructionOutcomeFailed;
+
+	int32 xDelta = asInteger - getCenterPosition().x;
+
+	if (xDelta != 0)
+		offsetTranslate(xDelta, 0, false);
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome VisualElement::scriptSetCenterPositionY(MiniscriptThread *thread, const DynamicValue &dest) {
+	int32 asInteger = 0;
+	if (!dest.roundToInt(asInteger))
+		return kMiniscriptInstructionOutcomeFailed;
+
+	int32 yDelta = asInteger - getCenterPosition().y;
+
+	if (yDelta != 0)
+		offsetTranslate(0, yDelta, false);
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 MiniscriptInstructionOutcome VisualElement::scriptSetWidth(MiniscriptThread *thread, const DynamicValue &value) {
 	int32 asInteger = 0;
 	if (!value.roundToInt(asInteger))
@@ -6650,6 +6705,18 @@ MiniscriptInstructionOutcome VisualElement::scriptWriteRefPositionAttribute(Mini
 	return kMiniscriptInstructionOutcomeFailed;
 }
 
+MiniscriptInstructionOutcome VisualElement::scriptWriteRefCenterPositionAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) {
+	if (attrib == "x") {
+		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetCenterPositionX>::create(this, writeProxy);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "y") {
+		DynamicValueWriteFuncHelper<VisualElement, &VisualElement::scriptSetCenterPositionY>::create(this, writeProxy);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	return kMiniscriptInstructionOutcomeFailed;
+}
+
 void VisualElement::offsetTranslate(int32 xDelta, int32 yDelta, bool cachedOriginOnly) {
 	if (!cachedOriginOnly) {
 		_rect.left += xDelta;
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 5c45169c915..43a6303fa6f 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -1508,7 +1508,9 @@ public:
 
 	bool runFrame();
 	void drawFrame();
+
 	void queueProject(const Common::SharedPtr<ProjectDescription> &desc);
+	void closeProject();
 
 	void addVolume(int volumeID, const char *name, bool isMounted);
 	bool getVolumeState(const Common::String &name, int &outVolumeID, bool &outIsMounted) const;
@@ -1780,6 +1782,8 @@ private:
 	// True if any elements were added to the scene, removed from the scene, or reparented since last draw
 	bool _sceneGraphChanged;
 
+	bool _isQuitting;
+
 	Hacks _hacks;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
@@ -2121,6 +2125,8 @@ public:
 	explicit Project(Runtime *runtime);
 	~Project();
 
+	VThreadState consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg);
+
 	void loadFromDescription(const ProjectDescription &desc);
 	void loadSceneFromStream(const Common::SharedPtr<Structural> &structural, uint32 streamID);
 
@@ -2450,12 +2456,15 @@ protected:
 	MiniscriptInstructionOutcome scriptSetPositionX(MiniscriptThread *thread, const DynamicValue &dest);
 	MiniscriptInstructionOutcome scriptSetPositionY(MiniscriptThread *thread, const DynamicValue &dest);
 	MiniscriptInstructionOutcome scriptSetCenterPosition(MiniscriptThread *thread, const DynamicValue &dest);
+	MiniscriptInstructionOutcome scriptSetCenterPositionX(MiniscriptThread *thread, const DynamicValue &dest);
+	MiniscriptInstructionOutcome scriptSetCenterPositionY(MiniscriptThread *thread, const DynamicValue &dest);
 	MiniscriptInstructionOutcome scriptSetVisibility(MiniscriptThread *thread, const DynamicValue &result);
 	MiniscriptInstructionOutcome scriptSetWidth(MiniscriptThread *thread, const DynamicValue &dest);
 	MiniscriptInstructionOutcome scriptSetHeight(MiniscriptThread *thread, const DynamicValue &dest);
 	MiniscriptInstructionOutcome scriptSetLayer(MiniscriptThread *thread, const DynamicValue &dest);
 
 	MiniscriptInstructionOutcome scriptWriteRefPositionAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
+	MiniscriptInstructionOutcome scriptWriteRefCenterPositionAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib);
 
 	void offsetTranslate(int32 xDelta, int32 yDelta, bool cachedOriginOnly);
 


Commit: 9abae5362c24160a06cd813de59b2a6b481a4475
    https://github.com/scummvm/scummvm/commit/9abae5362c24160a06cd813de59b2a6b481a4475
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix mToon temporal decompression flag being set incorrectly

Changed paths:
    engines/mtropolis/assets.cpp


diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index 98d3b306d90..b78e8c8ed48 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -123,22 +123,25 @@ bool CachedMToon::loadFromStream(const Common::SharedPtr<MToonMetadata> &metadat
 
 	if (metadata->codecID == kMToonRLECodecID) {
 		loadRLEFrames(data);
+
+		uint16 fullWidth = metadata->rect.getWidth();
+		uint16 fullHeight = metadata->rect.getHeight();
+
 		uint16 firstFrameWidth = 0;
 		uint16 firstFrameHeight = 0;
 
 		bool haveAnyTemporalFrames = false;
 		bool haveDifferentDimensions = false;
-		_isRLETemporalCompressed = true;
+		_isRLETemporalCompressed = false;
+
 		for (size_t i = 0; i < metadata->frames.size(); i++) {
 			const MToonMetadata::FrameDef &frame = metadata->frames[i];
-			if (_rleData[i].isKeyframe)
+			if (!_rleData[i].isKeyframe)
 				haveAnyTemporalFrames = true;
 
-			if (i > 0) {
-				if (_rleData[i].width != _rleData[0].width || _rleData[i].height != _rleData[0].height) {
-					haveDifferentDimensions = true;
-					break;
-				}
+			if (_rleData[i].width != fullWidth || _rleData[i].height != fullHeight) {
+				haveDifferentDimensions = true;
+				break;
 			}
 		}
 


Commit: 8461f9c845a4b1588f7af5e4db609dfe7eb86a98
    https://github.com/scummvm/scummvm/commit/8461f9c845a4b1588f7af5e4db609dfe7eb86a98
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix up auto-play event behavior to hopefully fix Obsidian rebel VO not triggering

Changed paths:
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 45cd9a7f99f..cddb664e462 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -635,11 +635,6 @@ void MovieElement::activate() {
 	_playRange = IntRange::create(0, _maxTimestamp);
 	_currentTimestamp = 0;
 
-	if (!_paused && _visible) {
-		StartPlayingTaskData *startPlayingTaskData = _runtime->getVThread().pushTask("MovieElement::startPlayingTask", this, &MovieElement::startPlayingTask);
-		startPlayingTaskData->runtime = _runtime;
-	}
-
 	if (_name.empty())
 		_name = project->getAssetNameByID(_assetID);
 }
@@ -657,6 +652,10 @@ void MovieElement::deactivate() {
 	_videoDecoder.reset();
 }
 
+bool MovieElement::canAutoPlay() const {
+	return _visible && !_paused;
+}
+
 void MovieElement::render(Window *window) {
 	if (_needsReset) {
 		_videoDecoder->setReverse(_reversed);
@@ -1155,6 +1154,10 @@ void MToonElement::deactivate() {
 	_renderSurface.reset();
 }
 
+bool MToonElement::canAutoPlay() const {
+	return _visible && !_paused;
+}
+
 void MToonElement::render(Window *window) {
 	if (_cachedMToon) {
 		_cachedMToon->optimize(_runtime);
@@ -1762,6 +1765,10 @@ bool SoundElement::load(ElementLoaderContext &context, const Data::SoundElement
 	if (!NonVisualElement::loadCommon(data.name, data.guid, data.elementFlags))
 		return false;
 
+	if (getStaticGUID() == 0x69b65) {
+		int n = 0;
+	}
+
 	_paused = ((data.soundFlags & Data::SoundElement::kPaused) != 0);
 	_loop = ((data.soundFlags & Data::SoundElement::kLoop) != 0);
 	_leftVolume = data.leftVolume;
@@ -1830,11 +1837,6 @@ void SoundElement::activate() {
 
 	_playMediaSignaller = project->notifyOnPlayMedia(this);
 
-	if (!_paused) {
-		StartPlayingTaskData *startPlayingTaskData = _runtime->getVThread().pushTask("SoundElement::startPlayingTask", this, &SoundElement::startPlayingTask);
-		startPlayingTaskData->runtime = _runtime;
-	}
-
 	if (_name.empty())
 		_name = project->getAssetNameByID(_assetID);
 }
@@ -1851,6 +1853,10 @@ void SoundElement::deactivate() {
 	_player.reset();
 }
 
+bool SoundElement::canAutoPlay() const {
+	return !_paused;
+}
+
 void SoundElement::playMedia(Runtime *runtime, Project *project) {
 	if (_shouldPlayIfNotPaused) {
 		if (_paused) {
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index 9add227f438..1c6e1628594 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -85,6 +85,8 @@ public:
 	void activate() override;
 	void deactivate() override;
 
+	bool canAutoPlay() const override;
+
 	void render(Window *window) override;
 	void playMedia(Runtime *runtime, Project *project) override;
 
@@ -189,6 +191,8 @@ public:
 	void activate() override;
 	void deactivate() override;
 
+	bool canAutoPlay() const override;
+
 	void render(Window *window) override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
@@ -318,6 +322,8 @@ public:
 	void activate() override;
 	void deactivate() override;
 
+	bool canAutoPlay() const override;
+
 	void playMedia(Runtime *runtime, Project *project) override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index a65d4da6552..6b9a33a3299 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -3684,6 +3684,10 @@ bool Runtime::runFrame() {
 
 		if (_sceneTransitionState == kSceneTransitionStateTransitioning && _playTime >= _sceneTransitionEndTime) {
 			_sceneTransitionState = kSceneTransitionStateNotTransitioning;
+
+			for (const SceneStackEntry &sceneStackEntry : _sceneStack)
+				recursiveAutoPlayMedia(sceneStackEntry.scene.get());
+
 			queueEventAsLowLevelSceneStateTransitionAction(Event::create(EventIDs::kSceneTransitionEnded, 0), _activeMainScene.get(), true, true);
 			continue;
 		}
@@ -4099,6 +4103,15 @@ void Runtime::recursiveDeactivateStructural(Structural *structural) {
 	structural->deactivate();
 }
 
+void Runtime::recursiveAutoPlayMedia(Structural *structural) {
+	if (structural->isElement())
+		static_cast<Element *>(structural)->triggerAutoPlay(this);
+
+	for (const Common::SharedPtr<Structural> &child : structural->getChildren()) {
+		recursiveAutoPlayMedia(child.get());
+	}
+}
+
 void Runtime::recursiveActivateStructural(Structural *structural) {
 	structural->activate();
 
@@ -6200,6 +6213,13 @@ ObjectLinkingScope *Subsection::getPersistentModifierScope() {
 	return &_modifierScope;
 }
 
+Element::Element() : _streamLocator(0), _sectionID(0), _haveCheckedAutoPlay(false) {
+}
+
+bool Element::canAutoPlay() const {
+	return false;
+}
+
 bool Element::isElement() const {
 	return true;
 }
@@ -6221,6 +6241,19 @@ void Element::removeMediaCue(const MediaCueState *mediaCue) {
 	}
 }
 
+void Element::triggerAutoPlay(Runtime *runtime) {
+	if (_haveCheckedAutoPlay)
+		return;
+
+	_haveCheckedAutoPlay = true;
+
+	if (canAutoPlay()) {
+		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kPlay, 0), DynamicValue(), getSelfReference()));
+		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, false, true));
+		runtime->queueMessage(dispatch);
+	}
+}
+
 bool Element::resolveMediaMarkerLabel(const Label& label, int32 &outResolution) const {
 	return false;
 }
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 43a6303fa6f..736d5a81b75 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -1670,6 +1670,7 @@ private:
 	void executeCompleteTransitionToScene(const Common::SharedPtr<Structural> &scene);
 	void executeSharedScenePostSceneChangeActions();
 
+	void recursiveAutoPlayMedia(Structural *structural);
 	void recursiveDeactivateStructural(Structural *structural);
 	void recursiveActivateStructural(Structural *structural);
 
@@ -2301,7 +2302,10 @@ private:
 
 class Element : public Structural {
 public:
+	Element();
+
 	virtual bool isVisual() const = 0;
+	virtual bool canAutoPlay() const;
 	bool isElement() const override;
 
 	uint32 getStreamLocator() const;
@@ -2309,6 +2313,8 @@ public:
 	void addMediaCue(MediaCueState *mediaCue);
 	void removeMediaCue(const MediaCueState *mediaCue);
 
+	void triggerAutoPlay(Runtime *runtime);
+
 	virtual bool resolveMediaMarkerLabel(const Label &label, int32 &outResolution) const;
 
 protected:
@@ -2316,6 +2322,8 @@ protected:
 	uint16 _sectionID;
 
 	Common::Array<MediaCueState *> _mediaCues;
+
+	bool _haveCheckedAutoPlay;
 };
 
 class VisualElementRenderProperties {


Commit: cae3ae0dd596bf78ab415a427e04018a7f105991
    https://github.com/scummvm/scummvm/commit/cae3ae0dd596bf78ab415a427e04018a7f105991
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add trans blit for images

Changed paths:
    engines/mtropolis/elements.cpp


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index cddb664e462..588eb5a4715 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -418,6 +418,10 @@ void GraphicElement::render(Window *window) {
 	int32 srcToDestY = drawRect.top - clippedSrcRect.top;
 
 	switch (_renderProps.getInkMode()) {
+	case VisualElementRenderProperties::kInkModeBackgroundTransparent:
+	case VisualElementRenderProperties::kInkModeBackgroundMatte:
+		// Background transparent and background matte seem to have no effect on simple graphics,
+		// even if the foreground and background color are the same
 	case VisualElementRenderProperties::kInkModeCopy: {
 			const Graphics::PixelFormat &pixFmt = window->getPixelFormat();
 			const ColorRGB8 fillColorRGB8 = _renderProps.getForeColor();
@@ -1028,7 +1032,18 @@ void ImageElement::render(Window *window) {
 		Common::SharedPtr<Graphics::Surface> optimized = _cachedImage->optimize(_runtime);
 		Common::Rect srcRect(optimized->w, optimized->h);
 		Common::Rect destRect(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y, _cachedAbsoluteOrigin.x + _rect.getWidth(), _cachedAbsoluteOrigin.y + _rect.getHeight());
-		window->getSurface()->blitFrom(*optimized, srcRect, destRect);
+
+		VisualElementRenderProperties::InkMode inkMode = _renderProps.getInkMode();
+
+		if (inkMode == VisualElementRenderProperties::kInkModeBackgroundMatte || inkMode == VisualElementRenderProperties::kInkModeBackgroundTransparent) {
+			const ColorRGB8 transColorRGB8 = _renderProps.getBackColor();
+			uint32 transColor = optimized->format.ARGBToColor(255, transColorRGB8.r, transColorRGB8.g, transColorRGB8.b);
+			window->getSurface()->transBlitFrom(*optimized, srcRect, destRect, transColor);
+		} else if (inkMode == VisualElementRenderProperties::kInkModeDefault || inkMode == VisualElementRenderProperties::kInkModeCopy) {
+			window->getSurface()->blitFrom(*optimized, srcRect, destRect);
+		} else {
+			warning("Unimplemented image ink mode");
+		}
 	}
 }
 
@@ -1765,10 +1780,6 @@ bool SoundElement::load(ElementLoaderContext &context, const Data::SoundElement
 	if (!NonVisualElement::loadCommon(data.name, data.guid, data.elementFlags))
 		return false;
 
-	if (getStaticGUID() == 0x69b65) {
-		int n = 0;
-	}
-
 	_paused = ((data.soundFlags & Data::SoundElement::kPaused) != 0);
 	_loop = ((data.soundFlags & Data::SoundElement::kLoop) != 0);
 	_leftVolume = data.leftVolume;


Commit: 6db206acf4808db0029de8ecd4bfee2b3763c382
    https://github.com/scummvm/scummvm/commit/6db206acf4808db0029de8ecd4bfee2b3763c382
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add undocumented list "random" attrib

Changed paths:
    engines/mtropolis/plugin/standard.cpp


diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 48b2940402b..a78ee2a3d76 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -28,6 +28,7 @@
 #include "audio/midiplayer.h"
 #include "audio/midiparser.h"
 
+#include "common/random.h"
 
 namespace MTropolis {
 
@@ -1162,6 +1163,12 @@ bool ListVariableModifier::readAttribute(MiniscriptThread *thread, DynamicValue
 	if (attrib == "count") {
 		result.setInt(_list->getSize());
 		return true;
+	} else if (attrib == "random") {
+		if (_list->getSize() == 0)
+			return false;
+
+		size_t index = thread->getRuntime()->getRandom()->getRandomNumber(_list->getSize() - 1);
+		return _list->getAtIndex(index, result);
 	}
 
 	return Modifier::readAttribute(thread, result, attrib);


Commit: c60c554b0dcc0cfc9afc823462b319778935ec7f
    https://github.com/scummvm/scummvm/commit/c60c554b0dcc0cfc9afc823462b319778935ec7f
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Restart timer modifier when executed while the timer is already running

Changed paths:
    engines/mtropolis/modifiers.cpp


diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index d26898b20d4..b0c3a7fa41c 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -945,12 +945,16 @@ VThreadState TimerMessengerModifier::consumeMessage(Runtime *runtime, const Comm
 			realMilliseconds = 1;
 
 		debug(3, "Timer %x '%s' scheduled to execute in %i milliseconds", getStaticGUID(), getName().c_str(), realMilliseconds);
-		if (!_scheduledEvent) {
-			_scheduledEvent = runtime->getScheduler().scheduleMethod<TimerMessengerModifier, &TimerMessengerModifier::trigger>(runtime->getPlayTime() + realMilliseconds, this);
-			_incomingData = msg->getValue();
-			if (_incomingData.getType() == DynamicValueTypes::kList)
-				_incomingData.setList(_incomingData.getList()->clone());
+
+		if (_scheduledEvent) {
+			_scheduledEvent->cancel();
+			_scheduledEvent.reset();
 		}
+
+		_scheduledEvent = runtime->getScheduler().scheduleMethod<TimerMessengerModifier, &TimerMessengerModifier::trigger>(runtime->getPlayTime() + realMilliseconds, this);
+		_incomingData = msg->getValue();
+		if (_incomingData.getType() == DynamicValueTypes::kList)
+			_incomingData.setList(_incomingData.getList()->clone());
 	}
 
 	return kVThreadReturn;


Commit: 7977def14183fdf2d4b31204a94aaa7340be2314
    https://github.com/scummvm/scummvm/commit/7977def14183fdf2d4b31204a94aaa7340be2314
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Properly resolve messenger variable reference payloads

Changed paths:
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 6b9a33a3299..c873177856b 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -1802,10 +1802,10 @@ void MessengerSendSpec::linkInternalReferences(ObjectLinkingScope *outerScope) {
 			Common::SharedPtr<RuntimeObject> resolution = outerScope->resolve(destination).lock();
 			if (resolution) {
 				if (resolution->isModifier()) {
-					resolvedModifierDest = resolution.staticCast<Modifier>();
+					_resolvedModifierDest = resolution.staticCast<Modifier>();
 					_linkType = kLinkTypeModifier;
 				} else if (resolution->isStructural()) {
-					resolvedStructuralDest = resolution.staticCast<Structural>();
+					_resolvedStructuralDest = resolution.staticCast<Structural>();
 					_linkType = kLinkTypeStructural;
 				} else {
 					_linkType = kLinkTypeUnresolved;
@@ -1815,11 +1815,26 @@ void MessengerSendSpec::linkInternalReferences(ObjectLinkingScope *outerScope) {
 			}
 		} break;
 	}
+
+	if (this->with.getType() == DynamicValueTypes::kVariableReference) {
+		const VarReference &varRef = this->with.getVarReference();
+
+		Common::WeakPtr<RuntimeObject> resolution = outerScope->resolve(varRef.guid, *varRef.source, false);
+		if (!resolution.expired()) {
+			Common::SharedPtr<RuntimeObject> obj = resolution.lock();
+			if (obj->isModifier())
+				_resolvedVarSource = obj.staticCast<Modifier>();
+			else {
+				warning("Messenger variable source wasn't a variable");
+			}
+		}
+	}
 }
 
 void MessengerSendSpec::visitInternalReferences(IStructuralReferenceVisitor *visitor) {
-	visitor->visitWeakModifierRef(resolvedModifierDest);
-	visitor->visitWeakStructuralRef(resolvedStructuralDest);
+	visitor->visitWeakModifierRef(_resolvedModifierDest);
+	visitor->visitWeakStructuralRef(_resolvedStructuralDest);
+	visitor->visitWeakModifierRef(_resolvedVarSource);
 }
 
 void MessengerSendSpec::resolveDestination(Runtime *runtime, Modifier *sender, Common::WeakPtr<Structural> &outStructuralDest, Common::WeakPtr<Modifier> &outModifierDest) const {
@@ -1827,9 +1842,9 @@ void MessengerSendSpec::resolveDestination(Runtime *runtime, Modifier *sender, C
 	outModifierDest.reset();
 
 	if (_linkType == kLinkTypeModifier) {
-		outModifierDest = resolvedModifierDest;
+		outModifierDest = _resolvedModifierDest;
 	} else if (_linkType == kLinkTypeStructural) {
-		outStructuralDest = resolvedStructuralDest;
+		outStructuralDest = _resolvedStructuralDest;
 	} else if (_linkType == kLinkTypeCoded) {
 		switch (destination) {
 		case kMessageDestNone:
@@ -1922,9 +1937,17 @@ void MessengerSendSpec::resolveVariableObjectType(RuntimeObject *obj, Common::We
 }
 
 void MessengerSendSpec::sendFromMessenger(Runtime *runtime, Modifier *sender, const DynamicValue &incomingData) const {
-	if (this->with.getType() == DynamicValueTypes::kIncomingData)
+	const DynamicValueTypes::DynamicValueType withType = with.getType();
+	if (withType == DynamicValueTypes::kIncomingData)
 		sendFromMessengerWithCustomData(runtime, sender, incomingData);
-	else
+	else if (withType == DynamicValueTypes::kVariableReference) {
+		DynamicValue payload;
+		Modifier *modifier = _resolvedVarSource.lock().get();
+		if (modifier && modifier->isVariable())
+			static_cast<VariableModifier *>(modifier)->varGetValue(nullptr, payload);
+
+		sendFromMessengerWithCustomData(runtime, sender, payload);
+	} else
 		sendFromMessengerWithCustomData(runtime, sender, this->with);
 }
 
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 736d5a81b75..2d523f220f0 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -1125,8 +1125,9 @@ struct MessengerSendSpec {
 	};
 
 	LinkType _linkType;
-	Common::WeakPtr<Structural> resolvedStructuralDest;
-	Common::WeakPtr<Modifier> resolvedModifierDest;
+	Common::WeakPtr<Structural> _resolvedStructuralDest;
+	Common::WeakPtr<Modifier> _resolvedModifierDest;
+	Common::WeakPtr<Modifier> _resolvedVarSource;
 
 private:
 	void resolveHierarchyStructuralDestination(Runtime *runtime, Modifier *sender, Common::WeakPtr<Structural> &outStructuralDest, Common::WeakPtr<Modifier> &outModifierDest, bool (*compareFunc)(Structural *structural)) const;


Commit: a0ad61ab3f9f5ce62a97979f2a67d8fc3dd55fbb
    https://github.com/scummvm/scummvm/commit/a0ad61ab3f9f5ce62a97979f2a67d8fc3dd55fbb
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix graphics draw crash

Changed paths:
    engines/mtropolis/elements.cpp


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 588eb5a4715..90afeee519f 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -414,8 +414,8 @@ void GraphicElement::render(Window *window) {
 	if (!clippedSrcRect.isValid())
 		return;
 
-	int32 srcToDestX = drawRect.left - clippedSrcRect.left;
-	int32 srcToDestY = drawRect.top - clippedSrcRect.top;
+	int32 srcToDestX = clippedDrawRect.left - clippedSrcRect.left;
+	int32 srcToDestY = clippedDrawRect.top - clippedSrcRect.top;
 
 	switch (_renderProps.getInkMode()) {
 	case VisualElementRenderProperties::kInkModeBackgroundTransparent:


Commit: 2fed7638394773908640be2b0f39bde79e2247eb
    https://github.com/scummvm/scummvm/commit/2fed7638394773908640be2b0f39bde79e2247eb
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix backwards horizontal/vertical constraint flags on Mac

Changed paths:
    engines/mtropolis/modifiers.cpp


diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index b0c3a7fa41c..00a698fa32b 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -615,8 +615,8 @@ bool DragMotionModifier::load(ModifierLoaderContext &context, const Data::DragMo
 	bool constrainHorizontal = false;
 	if (data.haveMacPart) {
 		_dragProps->constrainToParent = ((data.platform.mac.flags & Data::DragMotionModifier::MacPart::kConstrainToParent) != 0);
-		constrainVertical = ((data.platform.mac.flags & Data::DragMotionModifier::MacPart::kConstrainHorizontal) != 0);
-		constrainHorizontal = ((data.platform.mac.flags & Data::DragMotionModifier::MacPart::kConstrainVertical) != 0);
+		constrainHorizontal = ((data.platform.mac.flags & Data::DragMotionModifier::MacPart::kConstrainHorizontal) != 0);
+		constrainVertical = ((data.platform.mac.flags & Data::DragMotionModifier::MacPart::kConstrainVertical) != 0);
 	} else if (data.haveWinPart) {
 		_dragProps->constrainToParent = (data.platform.win.constrainToParent != 0);
 		constrainVertical = (data.platform.win.constrainVertical != 0);


Commit: be1fc4c3ed109d5e46f0565291216edaddc266e6
    https://github.com/scummvm/scummvm/commit/be1fc4c3ed109d5e46f0565291216edaddc266e6
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add default names to modifiers so Piazza TextWork modifier resolves correctly.

Changed paths:
    engines/mtropolis/modifier_factory.cpp
    engines/mtropolis/modifier_factory.h
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/plugin/obsidian.cpp
    engines/mtropolis/plugin/obsidian.h
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/modifier_factory.cpp b/engines/mtropolis/modifier_factory.cpp
index 37fff85146b..0491c8ddeda 100644
--- a/engines/mtropolis/modifier_factory.cpp
+++ b/engines/mtropolis/modifier_factory.cpp
@@ -46,8 +46,12 @@ Common::SharedPtr<Modifier> ModifierFactory<TModifier, TModifierData>::createMod
 
 	if (!modifier->load(context, static_cast<const TModifierData &>(dataObject)))
 		modifier.reset();
-	else
-		modifier->setSelfReference(modifier);
+	else {
+		Modifier *downcastMod = modifier.get();
+		if (downcastMod->getName().empty())
+			downcastMod->setName(downcastMod->getDefaultName());
+		downcastMod->setSelfReference(modifier);
+	}
 
 	return Common::SharedPtr<Modifier>(modifier);
 }
diff --git a/engines/mtropolis/modifier_factory.h b/engines/mtropolis/modifier_factory.h
index ca63b957009..fa96d20ad9b 100644
--- a/engines/mtropolis/modifier_factory.h
+++ b/engines/mtropolis/modifier_factory.h
@@ -76,10 +76,14 @@ Common::SharedPtr<Modifier> PlugInModifierFactory<TModifier, TModifierData>::cre
 
 	PlugInModifierLoaderContext plugInContext(context, plugInModifierData, &_plugIn);
 
-	if (!static_cast<Modifier *>(modifier.get())->loadPlugInHeader(plugInContext) || !modifier->load(plugInContext, static_cast<const TModifierData &>(*plugInModifierData.plugInData.get())))
+	Modifier *downcastMod = static_cast<Modifier *>(modifier.get());
+	if (!downcastMod->loadPlugInHeader(plugInContext) || !modifier->load(plugInContext, static_cast<const TModifierData &>(*plugInModifierData.plugInData.get())))
 		modifier.reset();
-	else
-		modifier->setSelfReference(modifier);
+	else {
+		if (downcastMod->getName().empty())
+			downcastMod->setName(downcastMod->getDefaultName());
+		downcastMod->setSelfReference(modifier);
+	}
 
 	return Common::SharedPtr<Modifier>(modifier);
 }
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 00a698fa32b..17ed30289c1 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -197,6 +197,10 @@ Common::SharedPtr<Modifier> BehaviorModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new BehaviorModifier(*this));
 }
 
+const char *BehaviorModifier::getDefaultName() const {
+	return "Behavior";
+}
+
 void BehaviorModifier::linkInternalReferences(ObjectLinkingScope *scope) {
 	Modifier::linkInternalReferences(scope);
 }
@@ -241,6 +245,10 @@ Common::SharedPtr<Modifier> MiniscriptModifier::shallowClone() const {
 	return clone;
 }
 
+const char *MiniscriptModifier::getDefaultName() const {
+	return "Miniscript Modifier";
+}
+
 void MiniscriptModifier::linkInternalReferences(ObjectLinkingScope* scope) {
 	_references->linkInternalReferences(scope);
 }
@@ -307,6 +315,10 @@ Common::SharedPtr<Modifier> SaveAndRestoreModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new SaveAndRestoreModifier(*this));
 }
 
+const char *SaveAndRestoreModifier::getDefaultName() const {
+	return "Save And Restore Modifier";
+}
+
 bool MessengerModifier::load(ModifierLoaderContext &context, const Data::MessengerModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -341,6 +353,10 @@ Common::SharedPtr<Modifier> MessengerModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new MessengerModifier(*this));
 }
 
+const char *MessengerModifier::getDefaultName() const {
+	return "Messenger";
+}
+
 bool SetModifier::load(ModifierLoaderContext &context, const Data::SetModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -355,6 +371,10 @@ Common::SharedPtr<Modifier> SetModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new SetModifier(*this));
 }
 
+const char *SetModifier::getDefaultName() const {
+	return "Set Modifier";
+}
+
 bool AliasModifier::load(ModifierLoaderContext &context, const Data::AliasModifier &data) {
 	_guid = data.guid;
 	if (!_modifierFlags.load(data.modifierFlags))
@@ -370,6 +390,10 @@ Common::SharedPtr<Modifier> AliasModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new AliasModifier(*this));
 }
 
+const char *AliasModifier::getDefaultName() const {
+	return "";
+}
+
 uint32 AliasModifier::getAliasID() const {
 	return _aliasID;
 }
@@ -519,6 +543,10 @@ Common::SharedPtr<Modifier> ChangeSceneModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new ChangeSceneModifier(*this));
 }
 
+const char *ChangeSceneModifier::getDefaultName() const {
+	return "Change Scene Modifier";
+}
+
 bool SoundEffectModifier::load(ModifierLoaderContext &context, const Data::SoundEffectModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -541,6 +569,10 @@ Common::SharedPtr<Modifier> SoundEffectModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new SoundEffectModifier(*this));
 }
 
+const char *SoundEffectModifier::getDefaultName() const {
+	return "Sound Effect Modifier";
+}
+
 bool PathMotionModifierV2::load(ModifierLoaderContext &context, const Data::PathMotionModifierV2 &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -602,6 +634,10 @@ Common::SharedPtr<Modifier> PathMotionModifierV2::shallowClone() const {
 	return clone;
 }
 
+const char *PathMotionModifierV2::getDefaultName() const {
+	return "Path Motion Modifier";
+}
+
 bool DragMotionModifier::load(ModifierLoaderContext &context, const Data::DragMotionModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -646,6 +682,10 @@ Common::SharedPtr<Modifier> DragMotionModifier::shallowClone() const {
 	return clone;
 }
 
+const char *DragMotionModifier::getDefaultName() const {
+	return "Drag Motion Modifier";
+}
+
 bool DragMotionModifier::respondsToEvent(const Event &evt) const {
 	return _enableWhen.respondsTo(evt) || _disableWhen.respondsTo(evt);
 }
@@ -785,6 +825,10 @@ Common::SharedPtr<Modifier> VectorMotionModifier::shallowClone() const {
 	return clone;
 }
 
+const char *VectorMotionModifier::getDefaultName() const {
+	return "Vector Motion Modifier";
+}
+
 void VectorMotionModifier::linkInternalReferences(ObjectLinkingScope *scope) {
 	if (_vec.getType() == DynamicValueTypes::kVariableReference) {
 		const VarReference &varRef = _vec.getVarReference();
@@ -822,6 +866,10 @@ Common::SharedPtr<Modifier> SceneTransitionModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new SceneTransitionModifier(*this));
 }
 
+const char *SceneTransitionModifier::getDefaultName() const {
+	return "Scene Transition Modifier";
+}
+
 bool ElementTransitionModifier::load(ModifierLoaderContext &context, const Data::ElementTransitionModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -841,6 +889,10 @@ Common::SharedPtr<Modifier> ElementTransitionModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new ElementTransitionModifier(*this));
 }
 
+const char *ElementTransitionModifier::getDefaultName() const {
+	return "Element Transition Modifier";
+}
+
 bool IfMessengerModifier::load(ModifierLoaderContext &context, const Data::IfMessengerModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -882,6 +934,11 @@ Common::SharedPtr<Modifier> IfMessengerModifier::shallowClone() const {
 
 	return clone;
 }
+
+const char *IfMessengerModifier::getDefaultName() const {
+	return "If Messenger";
+}
+
 void IfMessengerModifier::linkInternalReferences(ObjectLinkingScope *scope) {
 	_sendSpec.linkInternalReferences(scope);
 	_references->linkInternalReferences(scope);
@@ -975,6 +1032,10 @@ Common::SharedPtr<Modifier> TimerMessengerModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(clone);
 }
 
+const char *TimerMessengerModifier::getDefaultName() const {
+	return "Timer Messenger";
+}
+
 void TimerMessengerModifier::trigger(Runtime *runtime) {
 	debug(3, "Timer %x '%s' triggered", getStaticGUID(), getName().c_str());
 	if (_looping) {
@@ -1013,6 +1074,10 @@ Common::SharedPtr<Modifier> BoundaryDetectionMessengerModifier::shallowClone() c
 	return Common::SharedPtr<Modifier>(new BoundaryDetectionMessengerModifier(*this));
 }
 
+const char *BoundaryDetectionMessengerModifier::getDefaultName() const {
+	return "Boundary Detection Messenger";
+}
+
 bool CollisionDetectionMessengerModifier::load(ModifierLoaderContext &context, const Data::CollisionDetectionMessengerModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -1050,6 +1115,10 @@ Common::SharedPtr<Modifier> CollisionDetectionMessengerModifier::shallowClone()
 	return Common::SharedPtr<Modifier>(new CollisionDetectionMessengerModifier(*this));
 }
 
+const char *CollisionDetectionMessengerModifier::getDefaultName() const {
+	return "Collision Messenger";
+}
+
 KeyboardMessengerModifier::~KeyboardMessengerModifier() {
 }
 
@@ -1126,6 +1195,10 @@ Common::SharedPtr<Modifier> KeyboardMessengerModifier::shallowClone() const {
 	return cloned;
 }
 
+const char *KeyboardMessengerModifier::getDefaultName() const {
+	return "Keyboard Messenger";
+}
+
 bool KeyboardMessengerModifier::checkKeyEventTrigger(Runtime *runtime, Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt, Common::String &outCharStr) const {
 	if (!_isEnabled)
 		return false;
@@ -1325,6 +1398,10 @@ Common::SharedPtr<Modifier> TextStyleModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new TextStyleModifier(*this));
 }
 
+const char *TextStyleModifier::getDefaultName() const {
+	return "Text Style Messenger";
+}
+
 bool GraphicModifier::load(ModifierLoaderContext &context, const Data::GraphicModifier &data) {
 	ColorRGB8 foreColor;
 	ColorRGB8 backColor;
@@ -1384,6 +1461,10 @@ Common::SharedPtr<Modifier> GraphicModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new GraphicModifier(*this));
 }
 
+const char *GraphicModifier::getDefaultName() const {
+	return "Graphic Modifier";
+}
+
 bool CompoundVariableModifier::load(ModifierLoaderContext &context, const Data::CompoundVariableModifier &data) {
 	if (data.numChildren > 0) {
 		ChildLoaderContext loaderContext;
@@ -1519,6 +1600,10 @@ Common::SharedPtr<Modifier> CompoundVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new CompoundVariableModifier(*this));
 }
 
+const char *CompoundVariableModifier::getDefaultName() const {
+	return "Compound Variable";
+}
+
 bool BooleanVariableModifier::load(ModifierLoaderContext &context, const Data::BooleanVariableModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
@@ -1566,6 +1651,10 @@ Common::SharedPtr<Modifier> BooleanVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new BooleanVariableModifier(*this));
 }
 
+const char *BooleanVariableModifier::getDefaultName() const {
+	return "Boolean Variable";
+}
+
 BooleanVariableModifier::SaveLoad::SaveLoad(BooleanVariableModifier *modifier) : _modifier(modifier) {
 	_value = _modifier->_value;
 }
@@ -1633,6 +1722,10 @@ Common::SharedPtr<Modifier> IntegerVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new IntegerVariableModifier(*this));
 }
 
+const char *IntegerVariableModifier::getDefaultName() const {
+	return "Integer Variable";
+}
+
 IntegerVariableModifier::SaveLoad::SaveLoad(IntegerVariableModifier *modifier) : _modifier(modifier) {
 	_value = _modifier->_value;
 }
@@ -1717,6 +1810,10 @@ Common::SharedPtr<Modifier> IntegerRangeVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new IntegerRangeVariableModifier(*this));
 }
 
+const char *IntegerRangeVariableModifier::getDefaultName() const {
+	return "Integer Range Variable";
+}
+
 IntegerRangeVariableModifier::SaveLoad::SaveLoad(IntegerRangeVariableModifier *modifier) : _modifier(modifier) {
 	_range = _modifier->_range;
 }
@@ -1803,6 +1900,10 @@ Common::SharedPtr<Modifier> VectorVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new VectorVariableModifier(*this));
 }
 
+const char *VectorVariableModifier::getDefaultName() const {
+	return "Vector Variable";
+}
+
 VectorVariableModifier::SaveLoad::SaveLoad(VectorVariableModifier *modifier) : _modifier(modifier) {
 	_vector = _modifier->_vector;
 }
@@ -1891,6 +1992,10 @@ Common::SharedPtr<Modifier> PointVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new PointVariableModifier(*this));
 }
 
+const char *PointVariableModifier::getDefaultName() const {
+	return "Point Variable";
+}
+
 PointVariableModifier::SaveLoad::SaveLoad(PointVariableModifier *modifier) : _modifier(modifier) {
 	_value = _modifier->_value;
 }
@@ -1954,6 +2059,10 @@ Common::SharedPtr<Modifier> FloatingPointVariableModifier::shallowClone() const
 	return Common::SharedPtr<Modifier>(new FloatingPointVariableModifier(*this));
 }
 
+const char *FloatingPointVariableModifier::getDefaultName() const {
+	return "Floating Point Variable";
+}
+
 FloatingPointVariableModifier::SaveLoad::SaveLoad(FloatingPointVariableModifier *modifier) : _modifier(modifier) {
 	_value = _modifier->_value;
 }
@@ -2013,6 +2122,10 @@ Common::SharedPtr<Modifier> StringVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new StringVariableModifier(*this));
 }
 
+const char *StringVariableModifier::getDefaultName() const {
+	return "String Variable";
+}
+
 StringVariableModifier::SaveLoad::SaveLoad(StringVariableModifier *modifier) : _modifier(modifier) {
 	_value = _modifier->_value;
 }
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index efd43672525..a6b2543ffb3 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -72,6 +72,7 @@ private:
 	VThreadState propagateTask(const PropagateTaskData &taskData);
 
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 	void linkInternalReferences(ObjectLinkingScope *scope) override;
 	void visitInternalReferences(IStructuralReferenceVisitor *visitor) override;
 
@@ -97,6 +98,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 	void linkInternalReferences(ObjectLinkingScope *scope) override;
 	void visitInternalReferences(IStructuralReferenceVisitor *visitor) override;
 
@@ -120,6 +122,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	Event _saveWhen;
 	Event _restoreWhen;
@@ -147,6 +150,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	Event _when;
 	MessengerSendSpec _sendSpec;
@@ -162,6 +166,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	Event _executeWhen;
 	DynamicValue _source;
@@ -182,6 +187,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	uint32 _aliasID;
 };
@@ -200,6 +206,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	enum SceneSelectionType {
 		kSceneSelectionTypeNext,
@@ -227,6 +234,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	enum SoundType {
 		kSoundTypeBeep,
@@ -262,6 +270,7 @@ private:
 	};
 
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	Event _executeWhen;
 	Event _terminateWhen;
@@ -291,6 +300,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	Event _enableWhen;
 	Event _disableWhen;
@@ -313,6 +323,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 	void linkInternalReferences(ObjectLinkingScope *scope) override;
 	void visitInternalReferences(IStructuralReferenceVisitor *visitor) override;
 
@@ -342,6 +353,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	Event _enableWhen;
 	Event _disableWhen;
@@ -374,6 +386,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	Event _enableWhen;
 	Event _disableWhen;
@@ -404,6 +417,7 @@ private:
 	};
 
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 	void linkInternalReferences(ObjectLinkingScope *scope) override;
 	void visitInternalReferences(IStructuralReferenceVisitor *visitor) override;
 
@@ -435,6 +449,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	void trigger(Runtime *runtime);
 
@@ -458,6 +473,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	enum ExitTriggerMode {
 		kExitTriggerExiting,
@@ -490,6 +506,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	enum DetectionMode {
 		kDetectionModeFirstContact,
@@ -531,6 +548,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	void visitInternalReferences(IStructuralReferenceVisitor *visitor) override;
 	void linkInternalReferences(ObjectLinkingScope *scope) override;
@@ -583,6 +601,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	uint16 _macFontID;
 	uint16 _size;
@@ -609,6 +628,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	Event _applyWhen;
 	Event _removeWhen;
@@ -657,6 +677,7 @@ private:
 	};
 
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const override;
 	void appendModifier(const Common::SharedPtr<Modifier> &modifier) override;
@@ -702,6 +723,7 @@ private:
 	};
 
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	bool _value;
 };
@@ -736,6 +758,7 @@ private:
 	};
 
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	int32 _value;
 };
@@ -773,6 +796,7 @@ private:
 	};
 
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	IntRange _range;
 };
@@ -810,6 +834,7 @@ private:
 	};
 
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	AngleMagVector _vector;
 };
@@ -847,6 +872,7 @@ private:
 	};
 
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	Point16 _value;
 };
@@ -881,6 +907,7 @@ private:
 	};
 
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	double _value;
 };
@@ -915,6 +942,7 @@ private:
 	};
 
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	Common::String _value;
 };
diff --git a/engines/mtropolis/plugin/obsidian.cpp b/engines/mtropolis/plugin/obsidian.cpp
index dba283c43ba..a3a0824addf 100644
--- a/engines/mtropolis/plugin/obsidian.cpp
+++ b/engines/mtropolis/plugin/obsidian.cpp
@@ -63,6 +63,10 @@ Common::SharedPtr<Modifier> MovementModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new MovementModifier(*this));
 }
 
+const char *MovementModifier::getDefaultName() const {
+	return "Movement";
+}
+
 bool RectShiftModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::RectShiftModifier &data) {
 	if (data.rate.type != Data::PlugInTypeTaggedValue::kInteger)
 		return false;
@@ -90,6 +94,10 @@ Common::SharedPtr<Modifier> RectShiftModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new RectShiftModifier(*this));
 }
 
+const char *RectShiftModifier::getDefaultName() const {
+	return "RectShift";
+}
+
 TextWorkModifier::TextWorkModifier() : _firstChar(0), _lastChar(0) {
 }
 
@@ -176,6 +184,10 @@ Common::SharedPtr<Modifier> TextWorkModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new TextWorkModifier(*this));
 }
 
+const char *TextWorkModifier::getDefaultName() const {
+	return "TextWork";
+}
+
 MiniscriptInstructionOutcome TextWorkModifier::scriptSetFirstWord(MiniscriptThread *thread, const DynamicValue &value) {
 	// This and lastword are only used in tandem with lastword, exact functionality is unclear since it's
 	// also used in tandem with "output" which is normally used with firstchar+lastchar.
@@ -378,6 +390,10 @@ Common::SharedPtr<Modifier> DictionaryModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new DictionaryModifier(*this));
 }
 
+const char *DictionaryModifier::getDefaultName() const {
+	return "Dictionary";
+}
+
 WordMixerModifier::WordMixerModifier() : _matches(0), _result(0) {
 }
 
@@ -531,6 +547,10 @@ Common::SharedPtr<Modifier> WordMixerModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new WordMixerModifier(*this));
 }
 
+const char *WordMixerModifier::getDefaultName() const {
+	return "WordMixer";
+}
+
 XorModModifier::XorModModifier() {
 }
 
@@ -590,6 +610,10 @@ Common::SharedPtr<Modifier> XorModModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new XorModModifier(*this));
 }
 
+const char *XorModModifier::getDefaultName() const {
+	return "XorMod";
+}
+
 XorCheckModifier::XorCheckModifier() : _allClear(false) {
 }
 
@@ -619,6 +643,10 @@ Common::SharedPtr<Modifier> XorCheckModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new XorCheckModifier(*this));
 }
 
+const char *XorCheckModifier::getDefaultName() const {
+	return "XorCheck";
+}
+
 MiniscriptInstructionOutcome XorCheckModifier::scriptSetCheckNow(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() != DynamicValueTypes::kBoolean)
 		return kMiniscriptInstructionOutcomeFailed;
diff --git a/engines/mtropolis/plugin/obsidian.h b/engines/mtropolis/plugin/obsidian.h
index dfe4ee1dead..2682d5f1a97 100644
--- a/engines/mtropolis/plugin/obsidian.h
+++ b/engines/mtropolis/plugin/obsidian.h
@@ -46,6 +46,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	Point16 _dest;
 	bool _type;
@@ -65,6 +66,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	int32 _rate;
 	int32 _direction;
@@ -85,6 +87,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	MiniscriptInstructionOutcome scriptSetFirstWord(MiniscriptThread *thread, const DynamicValue &value);
 	MiniscriptInstructionOutcome scriptSetLastWord(MiniscriptThread *thread, const DynamicValue &value);
@@ -116,6 +119,7 @@ private:
 	MiniscriptInstructionOutcome scriptSetIndex(MiniscriptThread *thread, const DynamicValue &value);
 
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	Common::String _str;
 
@@ -139,6 +143,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	MiniscriptInstructionOutcome scriptSetInput(MiniscriptThread *thread, const DynamicValue &value);
 	MiniscriptInstructionOutcome scriptSetSearch(MiniscriptThread *thread, const DynamicValue &value);
@@ -166,6 +171,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	Event _enableWhen;
 	Event _disableWhen;
@@ -188,6 +194,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	MiniscriptInstructionOutcome scriptSetCheckNow(MiniscriptThread *thread, const DynamicValue &value);
 
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index a78ee2a3d76..9eae700a366 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -291,6 +291,10 @@ Common::SharedPtr<Modifier> CursorModifier::shallowClone() const {
 	return clone;
 }
 
+const char *CursorModifier::getDefaultName() const {
+	return "Cursor Modifier";
+}
+
 bool STransCtModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::STransCtModifier &data) {
 	if (data.enableWhen.type != Data::PlugInTypeTaggedValue::kEvent ||
 		data.disableWhen.type != Data::PlugInTypeTaggedValue::kEvent ||
@@ -348,6 +352,10 @@ Common::SharedPtr<Modifier> STransCtModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new STransCtModifier(*this));
 }
 
+const char *STransCtModifier::getDefaultName() const {
+	return "STransCt";	// Probably wrong
+}
+
 MiniscriptInstructionOutcome STransCtModifier::scriptSetRate(MiniscriptThread *thread, const DynamicValue &value) {
 	int32 asInteger = 0;
 	if (!value.roundToInt(asInteger))
@@ -524,6 +532,10 @@ Common::SharedPtr<Modifier> MediaCueMessengerModifier::shallowClone() const {
 	return clone;
 }
 
+const char *MediaCueMessengerModifier::getDefaultName() const {
+	return "Media Cue Messenger";
+}
+
 void MediaCueMessengerModifier::linkInternalReferences(ObjectLinkingScope *scope) {
 	if (_cueSourceType == kCueSourceVariableReference) {
 		Common::WeakPtr<RuntimeObject> obj = scope->resolve(_cueSource.asVarRefGUID);
@@ -631,6 +643,10 @@ Common::SharedPtr<Modifier> ObjectReferenceVariableModifier::shallowClone() cons
 	return Common::SharedPtr<Modifier>(new ObjectReferenceVariableModifier(*this));
 }
 
+const char *ObjectReferenceVariableModifier::getDefaultName() const {
+	return "Object Reference Variable";
+}
+
 MiniscriptInstructionOutcome ObjectReferenceVariableModifier::scriptSetPath(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() != DynamicValueTypes::kString)
 		return kMiniscriptInstructionOutcomeFailed;
@@ -1017,6 +1033,10 @@ Common::SharedPtr<Modifier> MidiModifier::shallowClone() const {
 	return clone;
 }
 
+const char *MidiModifier::getDefaultName() const {
+	return "MIDI Modifier";
+}
+
 MiniscriptInstructionOutcome MidiModifier::scriptSetVolume(MiniscriptThread *thread, const DynamicValue &value) {
 	int32 asInteger = 0;
 	if (!value.roundToInt(asInteger))
@@ -1296,6 +1316,10 @@ Common::SharedPtr<Modifier> ListVariableModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new ListVariableModifier(*this));
 }
 
+const char *ListVariableModifier::getDefaultName() const {
+	return "List Variable";
+}
+
 ListVariableModifier::SaveLoad::SaveLoad(ListVariableModifier *modifier) : _modifier(modifier), _list(_modifier->_list) {
 }
 
@@ -1489,6 +1513,10 @@ Common::SharedPtr<Modifier> SysInfoModifier::shallowClone() const {
 	return Common::SharedPtr<Modifier>(new SysInfoModifier(*this));
 }
 
+const char *SysInfoModifier::getDefaultName() const {
+	return "SysInfo Modifier";
+}
+
 StandardPlugInHacks::StandardPlugInHacks() : allowGarbledListModData(false) {
 }
 
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index 004560b56b4..398387b0d7b 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -55,6 +55,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	Event _applyWhen;
 	Event _removeWhen;
@@ -79,6 +80,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	MiniscriptInstructionOutcome scriptSetRate(MiniscriptThread *thread, const DynamicValue &value);
 	MiniscriptInstructionOutcome scriptSetSteps(MiniscriptThread *thread, const DynamicValue &value);
@@ -122,6 +124,7 @@ private:
 	};
 
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	void linkInternalReferences(ObjectLinkingScope *scope) override;
 	void visitInternalReferences(IStructuralReferenceVisitor *visitor) override;
@@ -181,6 +184,7 @@ private:
 	};
 
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	MiniscriptInstructionOutcome scriptSetPath(MiniscriptThread *thread, const DynamicValue &value);
 	MiniscriptInstructionOutcome scriptSetObject(MiniscriptThread *thread, const DynamicValue &value);
@@ -221,6 +225,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	MiniscriptInstructionOutcome scriptSetVolume(MiniscriptThread *thread, const DynamicValue &value);
 	MiniscriptInstructionOutcome scriptSetNoteVelocity(MiniscriptThread *thread, const DynamicValue &value);
@@ -312,6 +317,7 @@ private:
 	MiniscriptInstructionOutcome scriptSetCount(MiniscriptThread *thread, const DynamicValue &value);
 
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 
 	Common::SharedPtr<DynamicList> _list;
 	DynamicValueTypes::DynamicValueType _preferredContentType;
@@ -329,6 +335,7 @@ public:
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
 };
 
 struct StandardPlugInHacks {
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 2d523f220f0..44ffa04922e 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -2570,6 +2570,10 @@ public:
 	// Shallow clones only need to copy the object.  Descendent copies are done using visitInternalReferences.
 	virtual Common::SharedPtr<Modifier> shallowClone() const = 0;
 
+	// Returns the default name of the modifier.  This isn't optional: It can cause behavioral changes, e.g.
+	// Obsidian depends on this working properly to resolve the TextWork modifier in the Piazza.
+	virtual const char *getDefaultName() const = 0;
+
 	// Visits any internal references in the object.
 	// Any references to other elements owned by the object MUST be SharedPtr, any references to non-owned objects
 	// MUST be WeakPtr, in order for the cloning and materialization logic to work correctly.


Commit: 69305a1a74ad7ff95f21e55b5a128c5397be8e41
    https://github.com/scummvm/scummvm/commit/69305a1a74ad7ff95f21e55b5a128c5397be8e41
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix multi-scene layer interleaving not working correctly

Changed paths:
    engines/mtropolis/render.cpp


diff --git a/engines/mtropolis/render.cpp b/engines/mtropolis/render.cpp
index a92ce77da99..51772ef2f0c 100644
--- a/engines/mtropolis/render.cpp
+++ b/engines/mtropolis/render.cpp
@@ -41,6 +41,7 @@ struct OrderedDitherGenerator<TNumber, 1> {
 
 struct RenderItem {
 	VisualElement *element;
+	size_t sceneStackDepth;
 };
 
 template<class TNumber, int TResolution>
@@ -205,7 +206,7 @@ uint32 resolveRGB(uint8 r, uint8 g, uint8 b, const Graphics::PixelFormat &fmt) {
 	return rPlaced | gPlaced | bPlaced | aPlaced;
 }
 
-static void recursiveCollectDrawElementsAndUpdateOrigins(const Point16 &parentOrigin, Structural *structural, Common::Array<RenderItem> &normalBucket, Common::Array<RenderItem> &directBucket) {
+static void recursiveCollectDrawElementsAndUpdateOrigins(const Point16 &parentOrigin, Structural *structural, size_t sceneStackDepth, Common::Array<RenderItem> &normalBucket, Common::Array<RenderItem> &directBucket) {
 	Point16 elementOrigin = parentOrigin;
 	if (structural->isElement()) {
 		Element *element = static_cast<Element *>(structural);
@@ -220,6 +221,7 @@ static void recursiveCollectDrawElementsAndUpdateOrigins(const Point16 &parentOr
 
 			RenderItem item;
 			item.element = visualElement;
+			item.sceneStackDepth = sceneStackDepth;
 
 			if (visualElement->isVisible()) {
 				if (visualElement->isDirectToScreen())
@@ -231,12 +233,18 @@ static void recursiveCollectDrawElementsAndUpdateOrigins(const Point16 &parentOr
 	}
 
 	for (Common::Array<Common::SharedPtr<Structural> >::const_iterator it = structural->getChildren().begin(), itEnd = structural->getChildren().end(); it != itEnd; ++it) {
-		recursiveCollectDrawElementsAndUpdateOrigins(elementOrigin, it->get(), normalBucket, directBucket);
+		recursiveCollectDrawElementsAndUpdateOrigins(elementOrigin, it->get(), sceneStackDepth, normalBucket, directBucket);
 	}
 }
 
 static bool renderItemLess(const RenderItem &a, const RenderItem &b) {
-	return a.element->getLayer() < b.element->getLayer();
+	const uint16 aLayer = a.element->getLayer();
+	const uint16 bLayer = b.element->getLayer();
+
+	if (aLayer != bLayer)
+		return aLayer < bLayer;
+
+	return a.sceneStackDepth < b.sceneStackDepth;
 }
 
 static void renderNormalElement(const RenderItem &item, Window *mainWindow) {
@@ -257,21 +265,15 @@ void renderProject(Runtime *runtime, Window *mainWindow) {
 	Common::Array<RenderItem> normalBucket;
 	Common::Array<RenderItem> directBucket;
 
+	size_t sceneStackDepth = 0;
 	for (Common::Array<Structural *>::const_iterator it = scenes.begin(), itEnd = scenes.end(); it != itEnd; ++it) {
-		size_t normalStart = normalBucket.size();
-		size_t directStart = directBucket.size();
-
-		recursiveCollectDrawElementsAndUpdateOrigins(Point16::create(0, 0), *it, normalBucket, directBucket);
-
-		size_t normalEnd = normalBucket.size();
-		size_t directEnd = directBucket.size();
-
-		if (normalEnd - normalStart > 1)
-			Common::sort(normalBucket.begin() + normalStart, normalBucket.end(), renderItemLess);
-		if (directEnd - directStart > 1)
-			Common::sort(directBucket.begin() + directStart, directBucket.end(), renderItemLess);
+		recursiveCollectDrawElementsAndUpdateOrigins(Point16::create(0, 0), *it, sceneStackDepth, normalBucket, directBucket);
+		sceneStackDepth++;
 	}
 
+	Common::sort(normalBucket.begin(), normalBucket.end(), renderItemLess);
+	Common::sort(directBucket.begin(), directBucket.end(), renderItemLess);
+
 	if (!sceneChanged) {
 		for (Common::Array<RenderItem>::const_iterator it = normalBucket.begin(), itEnd = normalBucket.end(); it != itEnd; ++it) {
 			if (it->element->needsRender())


Commit: a3f5b993e0d2ed6917a1fce4f12c7f966e3b14aa
    https://github.com/scummvm/scummvm/commit/a3f5b993e0d2ed6917a1fce4f12c7f966e3b14aa
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Promote sound element to Done

Changed paths:
    engines/mtropolis/elements.h


diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index 1c6e1628594..c2129983316 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -328,7 +328,7 @@ public:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Sound Element"; }
-	SupportStatus debugGetSupportStatus() const override { return kSupportStatusPartial; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 	void debugInspect(IDebugInspectionReport *report) const override;
 #endif
 


Commit: d00a058d3e176a71b1ddddbc280ad70d5e409fa2
    https://github.com/scummvm/scummvm/commit/d00a058d3e176a71b1ddddbc280ad70d5e409fa2
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Handle show/hide events, stub out element transition modifier.

Changed paths:
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 17ed30289c1..2fb3d15172e 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -885,14 +885,93 @@ bool ElementTransitionModifier::load(ModifierLoaderContext &context, const Data:
 	return true;
 }
 
+bool ElementTransitionModifier::respondsToEvent(const Event &evt) const {
+	return _enableWhen.respondsTo(evt) || _disableWhen.respondsTo(evt);
+}
+
+VThreadState ElementTransitionModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	// How element transition modifiers work:
+	// - When activated, if Reveal, then Show is sent (regardless of whether element is visible or not).
+	// - Then, for both reveal and conceal, Transition Started is sent by the modifier.
+	// - When a conceal transition completes, Hide is sent to the element.
+	// - Then, for both reveal and conceal, Transition Ended is sent by the modifier.
+	// - If a transition is active and the Disable signal is called, then the transition completes immediately.
+	//
+	// Q. Does that mean if an element has a Revealer followed by a Concealer and they take opposing messages,
+	//    that the element will be hidden if a reveal interrupts the concealer due to its hide message happening
+	//    second, but that won't happen if they're in Concealer-Revealer order?
+	// A. Yes.
+	if (_enableWhen.respondsTo(msg->getEvent())) {
+		if (_scheduledEvent) {
+			_scheduledEvent->cancel();
+			_scheduledEvent.reset();
+		}
+
+		_scheduledEvent = runtime->getScheduler().scheduleMethod<ElementTransitionModifier, &ElementTransitionModifier::continueTransition>(runtime->getPlayTime() + 1, this);
+
+		// Pushed tasks, so these are executed in reverse order (Show -> Transition Started)
+		{
+			Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kTransitionStarted, 0), DynamicValue(), getSelfReference()));
+			Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, findStructuralOwner(), false, true, false));
+			runtime->sendMessageOnVThread(dispatch);
+		}
+
+		if (_revealType == kRevealTypeReveal)
+		{
+			Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kElementShow, 0), DynamicValue(), getSelfReference()));
+			Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, findStructuralOwner(), false, false, true));
+			runtime->sendMessageOnVThread(dispatch);
+		}
+
+		return kVThreadReturn;
+	}
+
+	if (_disableWhen.respondsTo(msg->getEvent())) {
+		if (_scheduledEvent) {
+			_scheduledEvent->cancel();
+
+			completeTransition(runtime);
+		}
+
+		return kVThreadReturn;
+	}
+
+	return Modifier::consumeMessage(runtime, msg);
+}
+
 Common::SharedPtr<Modifier> ElementTransitionModifier::shallowClone() const {
-	return Common::SharedPtr<Modifier>(new ElementTransitionModifier(*this));
+	Common::SharedPtr<ElementTransitionModifier> clone(new ElementTransitionModifier(*this));
+	clone->_scheduledEvent.reset();
+
+	return clone;
 }
 
 const char *ElementTransitionModifier::getDefaultName() const {
 	return "Element Transition Modifier";
 }
 
+void ElementTransitionModifier::continueTransition(Runtime *runtime) {
+	// TODO: Make this functional
+	completeTransition(runtime);
+}
+
+void ElementTransitionModifier::completeTransition(Runtime *runtime) {
+	_scheduledEvent.reset();
+
+	// Pushed tasks, so these are executed in reverse order (Hide -> Transition Ended)
+	{
+		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kTransitionEnded, 0), DynamicValue(), getSelfReference()));
+		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, findStructuralOwner(), false, true, false));
+		runtime->sendMessageOnVThread(dispatch);
+	}
+
+	if (_revealType == kRevealTypeConceal) {
+		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kElementHide, 0), DynamicValue(), getSelfReference()));
+		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, findStructuralOwner(), false, false, true));
+		runtime->sendMessageOnVThread(dispatch);
+	}
+}
+
 bool IfMessengerModifier::load(ModifierLoaderContext &context, const Data::IfMessengerModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index a6b2543ffb3..f0b0d55e1bc 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -368,6 +368,9 @@ class ElementTransitionModifier : public Modifier {
 public:
 	bool load(ModifierLoaderContext &context, const Data::ElementTransitionModifier &data);
 
+	bool respondsToEvent(const Event &evt) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
+
 	enum TransitionType {
 		kTransitionTypeRectangularIris = 0x03e8,
 		kTransitionTypeOvalIris = 0x03f2,
@@ -382,12 +385,16 @@ public:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Element Transition Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusPartial; }
 #endif
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 	const char *getDefaultName() const override;
 
+	void continueTransition(Runtime *runtime);
+	void completeTransition(Runtime *runtime);
+
 	Event _enableWhen;
 	Event _disableWhen;
 
@@ -395,6 +402,8 @@ private:
 	uint16 _steps;
 	TransitionType _transitionType;
 	RevealType _revealType;
+
+	Common::SharedPtr<ScheduledEvent> _scheduledEvent;
 };
 
 class IfMessengerModifier : public Modifier {
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index c873177856b..d93b7eb7447 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -6418,6 +6418,36 @@ uint16 VisualElement::getLayer() const {
 	return _layer;
 }
 
+VThreadState VisualElement::consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	if (Event::create(EventIDs::kElementShow, 0).respondsTo(msg->getEvent())) {
+		if (!_visible) {
+			_visible = true;
+			runtime->setSceneGraphDirty();
+		}
+
+		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kElementShow, 0), DynamicValue(), getSelfReference()));
+		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+		runtime->sendMessageOnVThread(dispatch);
+
+		return kVThreadReturn;
+	}
+
+	if (Event::create(EventIDs::kElementHide, 0).respondsTo(msg->getEvent())) {
+		if (_visible) {
+			_visible = false;
+			runtime->setSceneGraphDirty();
+		}
+
+		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kElementHide, 0), DynamicValue(), getSelfReference()));
+		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+		runtime->sendMessageOnVThread(dispatch);
+
+		return kVThreadReturn;
+	}
+
+	return Element::consumeCommand(runtime, msg);
+}
+
 bool VisualElement::isMouseInsideBox(int32 relativeX, int32 relativeY) const {
 	return relativeX >= _rect.left && relativeX < _rect.right && relativeY >= _rect.top && relativeY < _rect.bottom;
 }
@@ -6593,7 +6623,11 @@ void VisualElement::finalizeRender() {
 MiniscriptInstructionOutcome VisualElement::scriptSetVisibility(MiniscriptThread *thread, const DynamicValue &result) {
 	// FIXME: Need to make this fire Show/Hide events!
 	if (result.getType() == DynamicValueTypes::kBoolean) {
-		_visible = result.getBool();
+		const bool targetValue = result.getBool();
+		if (_visible != targetValue) {
+			_visible = targetValue;
+			thread->getRuntime()->setSceneGraphDirty();
+		}
 		return kMiniscriptInstructionOutcomeContinue;
 	}
 
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 44ffa04922e..61d0fc8f65b 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -2417,6 +2417,8 @@ public:
 	bool isVisual() const override;
 	virtual bool isTextLabel() const;
 
+	VThreadState consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg);
+
 	bool isVisible() const;
 	bool isDirectToScreen() const;
 	uint16 getLayer() const;


Commit: 9a4e8cb007bdba5622947f36106dfa8a895fa4cc
    https://github.com/scummvm/scummvm/commit/9a4e8cb007bdba5622947f36106dfa8a895fa4cc
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add collision detection modifier

Changed paths:
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/notes/notes.txt
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 2fb3d15172e..b538dbfd1a2 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -335,7 +335,7 @@ bool MessengerModifier::respondsToEvent(const Event &evt) const {
 
 VThreadState MessengerModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
 	if (_when.respondsTo(msg->getEvent())) {
-		_sendSpec.sendFromMessenger(runtime, this, msg->getValue());
+		_sendSpec.sendFromMessenger(runtime, this, msg->getValue(), nullptr);
 	}
 
 	return kVThreadReturn;
@@ -1037,7 +1037,7 @@ VThreadState IfMessengerModifier::evaluateAndSendTask(const EvaluateAndSendTaskD
 		return kVThreadError;
 
 	if (isTrue)
-		_sendSpec.sendFromMessenger(taskData.runtime, this, taskData.incomingData);
+		_sendSpec.sendFromMessenger(taskData.runtime, this, taskData.incomingData, nullptr);
 
 	return kVThreadReturn;
 }
@@ -1125,7 +1125,7 @@ void TimerMessengerModifier::trigger(Runtime *runtime) {
 	} else
 		_scheduledEvent.reset();
 
-	_sendSpec.sendFromMessenger(runtime, this, _incomingData);
+	_sendSpec.sendFromMessenger(runtime, this, _incomingData, nullptr);
 }
 
 bool BoundaryDetectionMessengerModifier::load(ModifierLoaderContext &context, const Data::BoundaryDetectionMessengerModifier &data) {
@@ -1157,11 +1157,19 @@ const char *BoundaryDetectionMessengerModifier::getDefaultName() const {
 	return "Boundary Detection Messenger";
 }
 
+CollisionDetectionMessengerModifier::CollisionDetectionMessengerModifier() : _runtime(nullptr), _isActive(false) {
+}
+
+CollisionDetectionMessengerModifier::~CollisionDetectionMessengerModifier() {
+	if (_isActive)
+		_runtime->removeCollider(this);
+}
+
 bool CollisionDetectionMessengerModifier::load(ModifierLoaderContext &context, const Data::CollisionDetectionMessengerModifier &data) {
 	if (!loadTypicalHeader(data.modHeader))
 		return false;
 
-	if (!_enableWhen.load(data.enableWhen) || !this->_disableWhen.load(data.disableWhen))
+	if (!_enableWhen.load(data.enableWhen) || !_disableWhen.load(data.disableWhen))
 		return false;
 
 	if (!_sendSpec.load(data.send, data.messageAndModifierFlags, data.with, data.withSource, data.withString, data.destination))
@@ -1190,14 +1198,89 @@ bool CollisionDetectionMessengerModifier::load(ModifierLoaderContext &context, c
 	return true;
 }
 
+bool CollisionDetectionMessengerModifier::respondsToEvent(const Event &evt) const {
+	return _enableWhen.respondsTo(evt) || _disableWhen.respondsTo(evt);
+}
+
+VThreadState CollisionDetectionMessengerModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	if (_enableWhen.respondsTo(msg->getEvent())) {
+		if (!_isActive) {
+			_isActive = true;
+			_runtime = runtime;
+			_incomingData = msg->getValue();
+			if (_incomingData.getType() == DynamicValueTypes::kList)
+				_incomingData.setList(_incomingData.getList()->clone());
+
+			runtime->addCollider(this);
+		}
+	}
+	if (_disableWhen.respondsTo(msg->getEvent())) {
+		if (_isActive) {
+			_isActive = false;
+			_runtime->removeCollider(this);
+			_incomingData = DynamicValue();
+		}
+	}
+
+	return kVThreadReturn;
+}
+
+void CollisionDetectionMessengerModifier::linkInternalReferences(ObjectLinkingScope *scope) {
+	_sendSpec.linkInternalReferences(scope);
+}
+
+void CollisionDetectionMessengerModifier::visitInternalReferences(IStructuralReferenceVisitor *visitor) {
+	_sendSpec.visitInternalReferences(visitor);
+}
+
 Common::SharedPtr<Modifier> CollisionDetectionMessengerModifier::shallowClone() const {
-	return Common::SharedPtr<Modifier>(new CollisionDetectionMessengerModifier(*this));
+	Common::SharedPtr<CollisionDetectionMessengerModifier> clone(new CollisionDetectionMessengerModifier(*this));
+	clone->_isActive = false;
+	clone->_incomingData = DynamicValue();
+	return clone;
 }
 
 const char *CollisionDetectionMessengerModifier::getDefaultName() const {
 	return "Collision Messenger";
 }
 
+void CollisionDetectionMessengerModifier::getCollisionProperties(Modifier *&modifier, bool &collideInFront, bool &collideBehind, bool &excludeParents) const {
+	collideBehind = _detectBehind;
+	collideInFront = _detectInFront;
+	excludeParents = _ignoreParent;
+	modifier = const_cast<CollisionDetectionMessengerModifier *>(this);
+}
+
+void CollisionDetectionMessengerModifier::triggerCollision(Runtime *runtime, Structural *collidingElement, bool wasInContact, bool isInContact, bool &shouldStop) {
+	switch (_detectionMode) {
+	case kDetectionModeExiting:
+		if (isInContact || !wasInContact)
+			return;
+		break;
+	case kDetectionModeFirstContact:
+		if (!isInContact || wasInContact)
+			return;
+		break;
+	case kDetectionModeWhileInContact:
+		if (!isInContact)
+			return;
+		break;
+	default:
+		error("Unknown collision detection mode");
+		return;
+	}
+
+	RuntimeObject *customDestination = nullptr;
+	if (_sendToCollidingElement) {
+		if (_sendToOnlyFirstCollidingElement)
+			shouldStop = true;
+
+		customDestination = collidingElement;
+	}
+
+	_sendSpec.sendFromMessenger(runtime, this, _incomingData, customDestination);
+}
+
 KeyboardMessengerModifier::~KeyboardMessengerModifier() {
 }
 
@@ -1400,7 +1483,7 @@ void KeyboardMessengerModifier::dispatchMessage(Runtime *runtime, const Common::
 
 	DynamicValue charStrValue;
 	charStrValue.setString(charStr);
-	_sendSpec.sendFromMessenger(runtime, this, charStrValue);
+	_sendSpec.sendFromMessenger(runtime, this, charStrValue, nullptr);
 }
 
 void KeyboardMessengerModifier::visitInternalReferences(IStructuralReferenceVisitor *visitor) {
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index f0b0d55e1bc..c7b6a385632 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -505,18 +505,31 @@ private:
 	MessengerSendSpec _send;
 };
 
-class CollisionDetectionMessengerModifier : public Modifier {
+class CollisionDetectionMessengerModifier : public Modifier, public ICollider {
 public:
+	CollisionDetectionMessengerModifier();
+	~CollisionDetectionMessengerModifier();
+
 	bool load(ModifierLoaderContext &context, const Data::CollisionDetectionMessengerModifier &data);
 
+	bool respondsToEvent(const Event &evt) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
+
+	void linkInternalReferences(ObjectLinkingScope *scope) override;
+	void visitInternalReferences(IStructuralReferenceVisitor *visitor) override;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Collision Detection Messenger Modifier"; }
+	SupportStatus debugGetSupportStatus() const { return kSupportStatusPartial; }
 #endif
 
 private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 	const char *getDefaultName() const override;
 
+	void getCollisionProperties(Modifier *&modifier, bool &collideInFront, bool &collideBehind, bool &excludeParents) const override;
+	void triggerCollision(Runtime *runtime, Structural *collidingElement, bool wasInContact, bool isInContact, bool &outShouldStop) override;
+
 	enum DetectionMode {
 		kDetectionModeFirstContact,
 		kDetectionModeWhileInContact,
@@ -533,6 +546,11 @@ private:
 	bool _ignoreParent;
 	bool _sendToCollidingElement; // ... instead of to send spec destination, but send spec with/flags still apply!
 	bool _sendToOnlyFirstCollidingElement;
+
+	Runtime *_runtime;
+	bool _isActive;
+
+	DynamicValue _incomingData;
 };
 
 class KeyboardMessengerModifier : public Modifier {
diff --git a/engines/mtropolis/notes/notes.txt b/engines/mtropolis/notes/notes.txt
index 394e28d4895..5173fd46299 100644
--- a/engines/mtropolis/notes/notes.txt
+++ b/engines/mtropolis/notes/notes.txt
@@ -108,3 +108,23 @@ read the attribute from the element.
 
 Also, "this" is not reserved - if you look up the "this" attribute from ANYTHING
 then it will resolve to the modifier executing the script.
+
+
+Collision messengers:
+
+Collision messengers behave strangely and their exact logic hasn't been determined.
+
+If there are 2 collision detection modifiers on an object and the first one triggers
+on exit, and the second triggers while in contact, then the second will fire twice.
+
+The "First element only" option behaves kind of nonsensically and contrary to its
+description.  It only prevents multiple collisions from being sent THAT FRAME, but
+continuously moving the object will cause multiple detections to trigger if they
+occur on separate frames.
+
+Moving an object from the shared scene will trigger collision with collision
+messenger modifiers in the main scene, but moving a main scene object will not
+collide with the same object?  Needs more research.
+
+Collisions only occur with visible objects.  It seems they are also not capable
+of colliding with scenes.
\ No newline at end of file
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index d93b7eb7447..9b1a5f6215c 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -1837,10 +1837,21 @@ void MessengerSendSpec::visitInternalReferences(IStructuralReferenceVisitor *vis
 	visitor->visitWeakModifierRef(_resolvedVarSource);
 }
 
-void MessengerSendSpec::resolveDestination(Runtime *runtime, Modifier *sender, Common::WeakPtr<Structural> &outStructuralDest, Common::WeakPtr<Modifier> &outModifierDest) const {
+void MessengerSendSpec::resolveDestination(Runtime *runtime, Modifier *sender, Common::WeakPtr<Structural> &outStructuralDest, Common::WeakPtr<Modifier> &outModifierDest, RuntimeObject *customDestination) const {
 	outStructuralDest.reset();
 	outModifierDest.reset();
 
+	if (customDestination) {
+		if (customDestination->isStructural())
+			outStructuralDest = customDestination->getSelfReference().staticCast<Structural>();
+		else if (customDestination->isModifier())
+			outModifierDest = customDestination->getSelfReference().staticCast<Modifier>();
+		else
+			error("Custom destination was invalid");
+
+		return;
+	}
+
 	if (_linkType == kLinkTypeModifier) {
 		outModifierDest = _resolvedModifierDest;
 	} else if (_linkType == kLinkTypeStructural) {
@@ -1936,28 +1947,28 @@ void MessengerSendSpec::resolveVariableObjectType(RuntimeObject *obj, Common::We
 	}
 }
 
-void MessengerSendSpec::sendFromMessenger(Runtime *runtime, Modifier *sender, const DynamicValue &incomingData) const {
+void MessengerSendSpec::sendFromMessenger(Runtime *runtime, Modifier *sender, const DynamicValue &incomingData, RuntimeObject *customDestination) const {
 	const DynamicValueTypes::DynamicValueType withType = with.getType();
 	if (withType == DynamicValueTypes::kIncomingData)
-		sendFromMessengerWithCustomData(runtime, sender, incomingData);
+		sendFromMessengerWithCustomData(runtime, sender, incomingData, customDestination);
 	else if (withType == DynamicValueTypes::kVariableReference) {
 		DynamicValue payload;
 		Modifier *modifier = _resolvedVarSource.lock().get();
 		if (modifier && modifier->isVariable())
 			static_cast<VariableModifier *>(modifier)->varGetValue(nullptr, payload);
 
-		sendFromMessengerWithCustomData(runtime, sender, payload);
+		sendFromMessengerWithCustomData(runtime, sender, payload, customDestination);
 	} else
-		sendFromMessengerWithCustomData(runtime, sender, this->with);
+		sendFromMessengerWithCustomData(runtime, sender, this->with, customDestination);
 }
 
-void MessengerSendSpec::sendFromMessengerWithCustomData(Runtime *runtime, Modifier *sender, const DynamicValue &data) const {
+void MessengerSendSpec::sendFromMessengerWithCustomData(Runtime *runtime, Modifier *sender, const DynamicValue &data, RuntimeObject *customDestination) const {
 	Common::SharedPtr<MessageProperties> props(new MessageProperties(this->send, data, sender->getSelfReference()));
 
 	Common::WeakPtr<Modifier> modifierDestRef;
 	Common::WeakPtr<Structural> structuralDestRef;
 
-	resolveDestination(runtime, sender, structuralDestRef, modifierDestRef);
+	resolveDestination(runtime, sender, structuralDestRef, modifierDestRef, customDestination);
 
 	Common::SharedPtr<Modifier> modifierDest = modifierDestRef.lock();
 	Common::SharedPtr<Structural> structuralDest = structuralDestRef.lock();
@@ -3509,7 +3520,7 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, ISaveUIProvider *saveProv
 	_displayWidth(1024), _displayHeight(768), _realTimeBase(0), _playTimeBase(0), _sceneTransitionState(kSceneTransitionStateNotTransitioning),
 	_lastFrameCursor(nullptr), _defaultCursor(new DefaultCursorGraphic()), _platform(kProjectPlatformUnknown),
 	_cachedMousePosition(Point16::create(0, 0)), _realMousePosition(Point16::create(0, 0)), _trackedMouseOutside(false),
-	  _forceCursorRefreshOnce(true), _haveModifierOverrideCursor(false), _sceneGraphChanged(false), _isQuitting(false) {
+	_forceCursorRefreshOnce(true), _haveModifierOverrideCursor(false), _sceneGraphChanged(false), _isQuitting(false), _collisionCheckTime(0) {
 	_random.reset(new Common::RandomSource("mtropolis"));
 
 	_vthread.reset(new VThread());
@@ -3725,6 +3736,12 @@ bool Runtime::runFrame() {
 			}
 		}
 
+		if (_collisionCheckTime < _playTime) {
+			_collisionCheckTime = _playTime;
+
+			checkCollisions();
+		}
+
 		if (_isQuitting)
 			return false;
 
@@ -3735,7 +3752,6 @@ bool Runtime::runFrame() {
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	if (_debugger)
 		_debugger->runFrame(realMSec);
-
 #endif
 
 	// Frame completed
@@ -5009,6 +5025,188 @@ bool Runtime::isSceneGraphDirty() const {
 	return _sceneGraphChanged;
 }
 
+void Runtime::addCollider(ICollider *collider) {
+	Common::SharedPtr<CollisionCheckState> state(new CollisionCheckState());
+	state->collider = collider;
+
+	_colliders.push_back(state);
+}
+
+void Runtime::removeCollider(ICollider *collider) {
+	size_t numColliders = _colliders.size();
+	for (size_t i = 0; i < numColliders; i++) {
+		if (_colliders[i]->collider == collider) {
+			_colliders.remove_at(i);
+			return;
+		}
+	}
+}
+
+void Runtime::checkCollisions() {
+	if (!_colliders.size())
+		return;
+
+	Common::Array<ColliderInfo> collisionObjects;
+	for (size_t i = 0; i < _sceneStack.size(); i++)
+		recursiveFindColliders(_sceneStack[i].scene.get(), i, collisionObjects, 0, 0, true);
+
+	Common::sort(collisionObjects.begin(), collisionObjects.end(), sortColliderPredicate);
+
+	for (const Common::SharedPtr<CollisionCheckState> &collisionCheckPtr : _colliders) {
+		CollisionCheckState &colCheck = *collisionCheckPtr.get();
+
+		Modifier *modifier = nullptr;
+		bool collideInFront;
+		bool collideBehind;
+		bool excludeParents;
+		colCheck.collider->getCollisionProperties(modifier, collideInFront, collideBehind, excludeParents);
+
+		Structural *owner = modifier->findStructuralOwner();
+		if (!owner)
+			continue;
+
+		if (!owner->isElement())
+			continue;
+
+		Element *element = static_cast<Element *>(owner);
+		if (!element->isVisual())
+			continue;
+
+		Common::Array<Common::WeakPtr<VisualElement> > collidingElements;
+
+		VisualElement *visual = static_cast<VisualElement *>(element);
+		if (visual->isVisible()) {
+			bool foundSelf = false;
+			size_t selfIndex = 0;
+			Rect16 selfRect = Rect16::create(0, 0, 0, 0);
+
+			for (size_t i = 0; i < collisionObjects.size(); i++) {
+				if (collisionObjects[i].element == visual) {
+					selfIndex = i;
+					selfRect = collisionObjects[i].absRect;
+					foundSelf = true;
+					break;
+				}
+			}
+
+			// This should always be true
+			if (foundSelf) {
+				size_t minIndex = 0;
+				size_t maxIndex = collisionObjects.size();
+				if (!collideBehind)
+					minIndex = selfIndex + 1;
+				if (!collideInFront)
+					maxIndex = selfIndex;
+
+				for (size_t i = minIndex; i < maxIndex; i++) {
+					if (i == selfIndex)
+						continue;
+
+					const ColliderInfo &collisionObject = collisionObjects[i];
+					VisualElement *collisionObjectElement = collisionObject.element;
+
+					assert(collisionObjectElement != visual);
+
+					// Potential collision
+					if (!collisionObject.absRect.intersect(selfRect).isValid())
+						continue;
+
+					if (excludeParents) {
+						bool isParent = false;
+
+						Structural *parentSearch = visual->getParent();
+						while (parentSearch != nullptr) {
+							if (parentSearch == collisionObjectElement) {
+								isParent = true;
+								break;
+							}
+						}
+
+						if (isParent)
+							continue;
+					}
+
+					collidingElements.push_back(collisionObjectElement->getSelfReference().staticCast<VisualElement>());
+				}
+			}
+		}
+
+		Common::Array<Common::WeakPtr<VisualElement> > &oldCollidingElements = colCheck.activeElements;
+
+		bool shouldStop = false;
+
+		for (size_t oldIndex = 0; oldIndex < oldCollidingElements.size();) {
+			Common::SharedPtr<VisualElement> oldColElement = oldCollidingElements[oldIndex].lock();
+			if (oldColElement.get() == nullptr) {
+				collidingElements.remove_at(oldIndex);
+				continue;
+			}
+
+			bool isStillColliding = false;
+			for (size_t newIndex = 0; newIndex < collidingElements.size(); newIndex++) {
+				Common::SharedPtr<VisualElement> newColElement = collidingElements[newIndex].lock();
+				if (newColElement == oldColElement) {
+					isStillColliding = true;
+					collidingElements.remove_at(newIndex);
+					break;
+				}
+			}
+
+			if (!isStillColliding)
+				oldCollidingElements.remove_at(oldIndex);
+			else
+				oldIndex++;
+
+			if (!shouldStop)
+				colCheck.collider->triggerCollision(this, oldColElement.get(), true, isStillColliding, shouldStop);
+		}
+
+		for (size_t newIndex = 0; newIndex < collidingElements.size(); newIndex++) {
+			Common::SharedPtr<VisualElement> colElement = collidingElements[newIndex].lock();
+
+			if (!shouldStop)
+				colCheck.collider->triggerCollision(this, colElement.get(), false, true, shouldStop);
+
+			oldCollidingElements.push_back(colElement);
+		}
+	}
+}
+
+void Runtime::recursiveFindColliders(Structural *structural, size_t sceneStackDepth, Common::Array<ColliderInfo> &colliders, int32 parentOriginX, int32 parentOriginY, bool isRoot) {
+	int32 childOffsetX = parentOriginX;
+	int32 childOffsetY = parentOriginY;
+	if (structural->isElement()) {
+		Element *element = static_cast<Element *>(structural);
+		if (element->isVisual()) {
+			VisualElement *visual = static_cast<VisualElement *>(element);
+			const Rect16 &rect = visual->getRelativeRect();
+
+			childOffsetX += rect.left;
+			childOffsetY += rect.top;
+
+			// isRoot = Is a scene, and colliding with scenes is not allowed
+			if (!isRoot && visual->isVisible()) {
+				ColliderInfo colliderInfo;
+				colliderInfo.absRect = rect.translate(parentOriginX, parentOriginY);
+				colliderInfo.element = visual;
+				colliderInfo.layer = visual->getLayer();
+				colliderInfo.sceneStackDepth = sceneStackDepth;
+
+				colliders.push_back(colliderInfo);
+			}
+		}
+	}
+
+	for (const Common::SharedPtr<Structural> &child : structural->getChildren())
+		recursiveFindColliders(child.get(), sceneStackDepth, colliders, childOffsetX, childOffsetY, false);
+}
+
+bool Runtime::sortColliderPredicate(const ColliderInfo &a, const ColliderInfo &b) {
+	if (a.layer != b.layer)
+		return a.layer < b.layer;
+	return a.sceneStackDepth < b.sceneStackDepth;
+}
+
 void Runtime::ensureMainWindowExists() {
 	// Maybe there's a better spot for this
 	if (_mainWindow.expired() && _project) {
@@ -5393,7 +5591,7 @@ void MediaCueState::checkTimestampChange(Runtime *runtime, uint32 oldTS, uint32
 
 	// Given the positioning of this, there's not really a way for the immediate flag to have any effect?
 	if (shouldTrigger)
-		send.sendFromMessenger(runtime, sourceModifier, incomingData);
+		send.sendFromMessenger(runtime, sourceModifier, incomingData, nullptr);
 }
 
 Project::Segment::Segment() : weakStream(nullptr) {
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 61d0fc8f65b..1537847c0a8 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -90,6 +90,7 @@ class WorldManagerInterface;
 struct DynamicValue;
 struct DynamicValueReadProxy;
 struct DynamicValueWriteProxy;
+struct ICollider;
 struct ILoadUIProvider;
 struct IMessageConsumer;
 struct IModifierContainer;
@@ -1104,12 +1105,12 @@ struct MessengerSendSpec {
 
 	void linkInternalReferences(ObjectLinkingScope *outerScope);
 	void visitInternalReferences(IStructuralReferenceVisitor *visitor);
-	void resolveDestination(Runtime *runtime, Modifier *sender, Common::WeakPtr<Structural> &outStructuralDest, Common::WeakPtr<Modifier> &outModifierDest) const;
+	void resolveDestination(Runtime *runtime, Modifier *sender, Common::WeakPtr<Structural> &outStructuralDest, Common::WeakPtr<Modifier> &outModifierDest, RuntimeObject *customDestination) const;
 
 	static void resolveVariableObjectType(RuntimeObject *obj, Common::WeakPtr<Structural> &outStructuralDest, Common::WeakPtr<Modifier> &outModifierDest);
 
-	void sendFromMessenger(Runtime *runtime, Modifier *sender, const DynamicValue &incomingData) const;
-	void sendFromMessengerWithCustomData(Runtime *runtime, Modifier *sender, const DynamicValue &data) const;
+	void sendFromMessenger(Runtime *runtime, Modifier *sender, const DynamicValue &incomingData, RuntimeObject *customDestination) const;
+	void sendFromMessengerWithCustomData(Runtime *runtime, Modifier *sender, const DynamicValue &data, RuntimeObject *customDestination) const;
 
 	Event send;
 	MessageFlags messageFlags;
@@ -1605,6 +1606,10 @@ public:
 	void clearSceneGraphDirty();
 	bool isSceneGraphDirty() const;
 
+	void addCollider(ICollider *collider);
+	void removeCollider(ICollider *collider);
+	void checkCollisions();
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	void debugSetEnabled(bool enabled);
 	void debugBreak();
@@ -1664,6 +1669,18 @@ private:
 		int32 y;
 	};
 
+	struct CollisionCheckState {
+		Common::Array<Common::WeakPtr<VisualElement> > activeElements;
+		ICollider *collider;
+	};
+
+	struct ColliderInfo {
+		size_t sceneStackDepth;
+		uint16 layer;
+		VisualElement *element;
+		Rect16 absRect;
+	};
+
 	static Common::SharedPtr<Structural> findDefaultSharedSceneForScene(Structural *scene);
 	void executeTeardown(const Teardown &teardown);
 	void executeLowLevelSceneStateTransition(const LowLevelSceneStateTransitionAction &transitionAction);
@@ -1697,6 +1714,9 @@ private:
 
 	void updateMainWindowCursor();
 
+	static void recursiveFindColliders(Structural *structural, size_t sceneStackDepth, Common::Array<ColliderInfo> &colliders, int32 parentOriginX, int32 parentOriginY, bool isRoot);
+	static bool sortColliderPredicate(const ColliderInfo &a, const ColliderInfo &b);
+
 	Common::Array<VolumeState> _volumes;
 	Common::SharedPtr<ProjectDescription> _queuedProjectDesc;
 	Common::SharedPtr<Project> _project;
@@ -1786,6 +1806,9 @@ private:
 
 	bool _isQuitting;
 
+	Common::Array<Common::SharedPtr<CollisionCheckState> > _colliders;
+	uint32 _collisionCheckTime;
+
 	Hacks _hacks;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
@@ -2104,6 +2127,11 @@ private:
 	Common::SharedPtr<KeyboardEventSignaller> _signaller;
 };
 
+struct ICollider {
+	virtual void getCollisionProperties(Modifier *&modifier, bool &collideInFront, bool &collideBehind, bool &excludeParents) const = 0;
+	virtual void triggerCollision(Runtime *runtime, Structural *collidingElement, bool wasInContact, bool isInContact, bool &outShouldStop) = 0;
+};
+
 struct MediaCueState {
 	enum TriggerTiming {
 		kTriggerTimingStart = 0,
@@ -2585,6 +2613,8 @@ public:
 
 	void recursiveCollectObjectsMatchingCriteria(Common::Array<Common::WeakPtr<RuntimeObject> > &results, bool (*evalFunc)(void *userData, RuntimeObject *object), void *userData, bool onlyEnabled);
 
+	Structural *findStructuralOwner() const;
+
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	SupportStatus debugGetSupportStatus() const override;
 	const Common::String &debugGetName() const override;
@@ -2598,8 +2628,6 @@ protected:
 	// If you override this, you should override visitInternalReferences too
 	virtual void linkInternalReferences(ObjectLinkingScope *scope);
 
-	Structural *findStructuralOwner() const;
-
 	Common::String _name;
 	ModifierFlags _modifierFlags;
 	Common::WeakPtr<RuntimeObject> _parent;


Commit: 35a3ad904db92461ea51d6adfaa6b9125bbf9531
    https://github.com/scummvm/scummvm/commit/35a3ad904db92461ea51d6adfaa6b9125bbf9531
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix hang in Piazza tutorial

Changed paths:
    engines/mtropolis/runtime.cpp


diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 9b1a5f6215c..3209134f0c2 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -5120,6 +5120,7 @@ void Runtime::checkCollisions() {
 								isParent = true;
 								break;
 							}
+							parentSearch = parentSearch->getParent();
 						}
 
 						if (isParent)


Commit: 18466585a17a6875fbf202fe1f27c85dd2102a08
    https://github.com/scummvm/scummvm/commit/18466585a17a6875fbf202fe1f27c85dd2102a08
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix crash in Spider oil puzzle tower map viewer

Changed paths:
    engines/mtropolis/assets.cpp


diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index b78e8c8ed48..1ac53030cc9 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -739,7 +739,7 @@ const Common::SharedPtr<Graphics::Surface> &CachedImage::optimize(Runtime *runti
 
 		const byte *palette = nullptr;
 
-		if (_colorDepth == kColorDepthMode16Bit)
+		if (_colorDepth == kColorDepthMode16Bit || _colorDepth == kColorDepthMode32Bit)
 			palette = bwPalette;
 
 		_surface->convertToInPlace(renderFmt, palette);


Commit: 45147c0140e8324f23d08cd406a9f090c57ceb62
    https://github.com/scummvm/scummvm/commit/45147c0140e8324f23d08cd406a9f090c57ceb62
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Discard "flushpriority" sets on images

Changed paths:
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 90afeee519f..beb881c3c3c 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -998,6 +998,9 @@ MiniscriptInstructionOutcome ImageElement::writeRefAttribute(MiniscriptThread *t
 	if (attrib == "text") {
 		DynamicValueWriteStringHelper::create(&_text, writeProxy);
 		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "flushpriority") {
+		DynamicValueWriteFuncHelper<ImageElement, &ImageElement::scriptSetFlushPriority>::create(this, writeProxy);
+		return kMiniscriptInstructionOutcomeContinue;
 	}
 
 	return VisualElement::writeRefAttribute(thread, writeProxy, attrib);
@@ -1047,6 +1050,11 @@ void ImageElement::render(Window *window) {
 	}
 }
 
+MiniscriptInstructionOutcome ImageElement::scriptSetFlushPriority(MiniscriptThread *thread, const DynamicValue &value) {
+	// We don't support flushing media, and this value isn't readable, so just discard it
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 MToonElement::MToonElement() : _frame(0), _renderedFrame(0), _flushPriority(0), _celStartTimeMSec(0), _isPlaying(false), _playRange(IntRange::create(1, 1)) {
 }
 
@@ -1887,7 +1895,7 @@ void SoundElement::playMedia(Runtime *runtime, Project *project) {
 
 				_player.reset();
 
-				int normalizedVolume = (_leftVolume + _rightVolume) * 255 / 2;
+				int normalizedVolume = (_leftVolume + _rightVolume) * 255 / 200;
 				int normalizedBalance = _balance * 127 / 100;
 
 				// TODO: Support ranges
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index c2129983316..bee0b2c5294 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -166,6 +166,8 @@ public:
 #endif
 
 private:
+	MiniscriptInstructionOutcome scriptSetFlushPriority(MiniscriptThread *thread, const DynamicValue &value);
+
 	bool _cacheBitmap;
 	uint32 _assetID;
 


Commit: 0b47b7150c80115e173e4cc44f3989071bccdeed
    https://github.com/scummvm/scummvm/commit/0b47b7150c80115e173e4cc44f3989071bccdeed
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Promote save/restore modifier to Done

Changed paths:
    engines/mtropolis/modifiers.h


diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index c7b6a385632..ca9b4a981c1 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -117,7 +117,7 @@ public:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Save And Restore Modifier"; }
-	SupportStatus debugGetSupportStatus() const override { return kSupportStatusPartial; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:


Commit: 060fbb2e6fc660a443f902dba43e7a03dbc9cc42
    https://github.com/scummvm/scummvm/commit/060fbb2e6fc660a443f902dba43e7a03dbc9cc42
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add some missing attributes

Changed paths:
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/runtime.cpp


diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 9eae700a366..fb3fdc8a075 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -54,7 +54,7 @@ public:
 // separately for each input.
 class MidiFilePlayerImpl : public MidiFilePlayer, public MidiDriver_BASE {
 public:
-	explicit MidiFilePlayerImpl(MidiDriver_BASE *outputDriver, const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, uint32 baseTempo, uint8 volume);
+	explicit MidiFilePlayerImpl(MidiDriver_BASE *outputDriver, const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, uint32 baseTempo, uint8 volume, bool loop);
 	~MidiFilePlayerImpl();
 
 	// Do not call any of these directly since they're not thread-safe, expose them via MultiMidiPlayer
@@ -63,6 +63,7 @@ public:
 	void pause();
 	void resume();
 	void setVolume(uint8 volume);
+	void setLoop(bool loop);
 	void detach();
 	void onTimer();
 
@@ -73,6 +74,7 @@ private:
 	Common::SharedPtr<MidiParser> _parser;
 	MidiDriver_BASE *_outputDriver;
 	uint8 _volume;
+	bool _loop;
 };
 
 class MultiMidiPlayer : public Audio::MidiPlayer {
@@ -80,10 +82,11 @@ public:
 	MultiMidiPlayer();
 	~MultiMidiPlayer();
 
-	MidiFilePlayer *createFilePlayer(const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, uint8 volume);
+	MidiFilePlayer *createFilePlayer(const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, uint8 volume, bool loop);
 	void deleteFilePlayer(MidiFilePlayer *player);
 
 	void setPlayerVolume(MidiFilePlayer *player, uint8 volume);
+	void setPlayerLoop(MidiFilePlayer *player, bool loop);
 	void stopPlayer(MidiFilePlayer *player);
 	void playPlayer(MidiFilePlayer *player);
 	void pausePlayer(MidiFilePlayer *player);
@@ -105,8 +108,8 @@ private:
 MidiFilePlayer::~MidiFilePlayer() {
 }
 
-MidiFilePlayerImpl::MidiFilePlayerImpl(MidiDriver_BASE *outputDriver, const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, uint32 baseTempo, uint8 volume)
-	: _file(file), _outputDriver(outputDriver), _parser(nullptr), _volume(255) {
+MidiFilePlayerImpl::MidiFilePlayerImpl(MidiDriver_BASE *outputDriver, const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, uint32 baseTempo, uint8 volume, bool loop)
+	: _file(file), _outputDriver(outputDriver), _parser(nullptr), _volume(255), _loop(loop) {
 	Common::SharedPtr<MidiParser> parser(MidiParser::createParser_SMF());
 
 	if (file->contents.size() != 0 && parser->loadMusic(&file->contents[0], file->contents.size())) {
@@ -143,6 +146,10 @@ void MidiFilePlayerImpl::setVolume(uint8 volume) {
 	_volume = volume;
 }
 
+void MidiFilePlayerImpl::setLoop(bool loop) {
+	_loop = loop;
+}
+
 void MidiFilePlayerImpl::detach() {
 	if (_parser) {
 		_parser->setMidiDriver(nullptr);
@@ -197,8 +204,8 @@ void MultiMidiPlayer::onTimer() {
 }
 
 
-MidiFilePlayer *MultiMidiPlayer::createFilePlayer(const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, uint8 volume) {
-	Common::SharedPtr<MidiFilePlayerImpl> filePlayer(new MidiFilePlayerImpl(this, file, getBaseTempo(), volume));
+MidiFilePlayer *MultiMidiPlayer::createFilePlayer(const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, uint8 volume, bool loop) {
+	Common::SharedPtr<MidiFilePlayerImpl> filePlayer(new MidiFilePlayerImpl(this, file, getBaseTempo(), volume, loop));
 
 	{
 		Common::StackLock lock(_mutex);
@@ -232,6 +239,11 @@ void MultiMidiPlayer::setPlayerVolume(MidiFilePlayer *player, uint8 volume) {
 	static_cast<MidiFilePlayerImpl *>(player)->setVolume(volume);
 }
 
+void MultiMidiPlayer::setPlayerLoop(MidiFilePlayer *player, bool loop) {
+	Common::StackLock lock(_mutex);
+	static_cast<MidiFilePlayerImpl *>(player)->setLoop(loop);
+}
+
 void MultiMidiPlayer::stopPlayer(MidiFilePlayer *player) {
 	Common::StackLock lock(_mutex);
 	static_cast<MidiFilePlayerImpl *>(player)->stop();
@@ -984,7 +996,7 @@ VThreadState MidiModifier::consumeMessage(Runtime *runtime, const Common::Shared
 			if (_embeddedFile) {
 				debug(2, "MIDI (%x '%s'): Playing embedded file", getStaticGUID(), getName().c_str());
 				if (!_filePlayer)
-					_filePlayer = _plugIn->getMidi()->createFilePlayer(_embeddedFile, _volume * 255 / 100);
+					_filePlayer = _plugIn->getMidi()->createFilePlayer(_embeddedFile, _volume * 255 / 100, _modeSpecific.file.loop);
 				_plugIn->getMidi()->playPlayer(_filePlayer);
 			} else {
 				debug(2, "MIDI (%x '%s'): Digested execute event but don't have anything to play", getStaticGUID(), getName().c_str());
@@ -1020,6 +1032,9 @@ MiniscriptInstructionOutcome MidiModifier::writeRefAttribute(MiniscriptThread *t
 	} else if (attrib == "notenum") {
 		DynamicValueWriteFuncHelper<MidiModifier, &MidiModifier::scriptSetNoteNum>::create(this, result);
 		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "loop") {
+		DynamicValueWriteFuncHelper<MidiModifier, &MidiModifier::scriptSetLoop>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
 	}
 
 	return Modifier::writeRefAttribute(thread, result, attrib);
@@ -1094,6 +1109,25 @@ MiniscriptInstructionOutcome MidiModifier::scriptSetNoteNum(MiniscriptThread *th
 	return kMiniscriptInstructionOutcomeContinue;
 }
 
+MiniscriptInstructionOutcome MidiModifier::scriptSetLoop(MiniscriptThread *thread, const DynamicValue &value) {
+	if (value.getType() != DynamicValueTypes::kBoolean)
+		return kMiniscriptInstructionOutcomeFailed;
+
+	if (_mode == kModeFile) {
+		const bool loop = value.getBool();
+
+		debug(2, "MIDI (%x '%s'): Changing loop state to %s", getStaticGUID(), getName().c_str(), loop ? "true" : "false");
+		if (_modeSpecific.file.loop != loop) {
+			_modeSpecific.file.loop = loop;
+
+			if (_filePlayer)
+				_plugIn->getMidi()->setPlayerLoop(_filePlayer, loop);
+		}
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
 ListVariableModifier::ListVariableModifier() : _list(new DynamicList()), _preferredContentType(DynamicValueTypes::kInteger) {
 }
 
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index 398387b0d7b..08c2ca27dbf 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -230,6 +230,7 @@ private:
 	MiniscriptInstructionOutcome scriptSetVolume(MiniscriptThread *thread, const DynamicValue &value);
 	MiniscriptInstructionOutcome scriptSetNoteVelocity(MiniscriptThread *thread, const DynamicValue &value);
 	MiniscriptInstructionOutcome scriptSetNoteNum(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetLoop(MiniscriptThread *thread, const DynamicValue &value);
 
 	struct FilePart {
 		bool loop;
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 3209134f0c2..44c7f3e5def 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -7130,8 +7130,7 @@ bool Modifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, con
 	if (attrib == "parent") {
 		result.setObject(_parent);
 		return true;
-	}
-	if (attrib == "subsection") {
+	} else if (attrib == "subsection") {
 		RuntimeObject *scan = _parent.lock().get();
 		while (scan) {
 			if (scan->isSubsection()) {
@@ -7148,10 +7147,13 @@ bool Modifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, con
 		}
 
 		return false;
-	}
-	if (attrib == "name") {
+	} else if (attrib == "name") {
 		result.setString(_name);
 		return true;
+	} else if (attrib == "element") {
+		Structural *owner = findStructuralOwner();
+		result.setObject(owner ? owner->getSelfReference() : Common::WeakPtr<RuntimeObject>());
+		return true;
 	}
 
 	return false;


Commit: f54a8864a4ec6f89804119e4769004a87e33cbfa
    https://github.com/scummvm/scummvm/commit/f54a8864a4ec6f89804119e4769004a87e33cbfa
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix if messengers not firing if the condition was a variable

Changed paths:
    engines/mtropolis/miniscript.cpp


diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 0515e97c035..6cfd9e84347 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -1924,6 +1924,8 @@ bool MiniscriptThread::evaluateTruthOfResult(bool &isTrue) {
 		return false;
 	}
 
+	dereferenceRValue(0, false);
+
 	isTrue = miniscriptEvaluateTruth(_stack[0].value);
 	return true;
 }


Commit: 2bc8af92fa2a896bec48cc46c380ff79a7e5e03a
    https://github.com/scummvm/scummvm/commit/2bc8af92fa2a896bec48cc46c380ff79a7e5e03a
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Stub out MIDI mute tracks

Changed paths:
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h


diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index fb3fdc8a075..62c5cb7c0f7 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -54,7 +54,7 @@ public:
 // separately for each input.
 class MidiFilePlayerImpl : public MidiFilePlayer, public MidiDriver_BASE {
 public:
-	explicit MidiFilePlayerImpl(MidiDriver_BASE *outputDriver, const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, uint32 baseTempo, uint8 volume, bool loop);
+	explicit MidiFilePlayerImpl(MidiDriver_BASE *outputDriver, const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, uint32 baseTempo, uint8 volume, bool loop, uint16 mutedTracks);
 	~MidiFilePlayerImpl();
 
 	// Do not call any of these directly since they're not thread-safe, expose them via MultiMidiPlayer
@@ -64,6 +64,7 @@ public:
 	void resume();
 	void setVolume(uint8 volume);
 	void setLoop(bool loop);
+	void setMutedTracks(uint16 mutedTracks);
 	void detach();
 	void onTimer();
 
@@ -73,6 +74,7 @@ private:
 	Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> _file;
 	Common::SharedPtr<MidiParser> _parser;
 	MidiDriver_BASE *_outputDriver;
+	uint16 _mutedTracks;
 	uint8 _volume;
 	bool _loop;
 };
@@ -82,11 +84,12 @@ public:
 	MultiMidiPlayer();
 	~MultiMidiPlayer();
 
-	MidiFilePlayer *createFilePlayer(const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, uint8 volume, bool loop);
+	MidiFilePlayer *createFilePlayer(const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, uint8 volume, bool loop, uint16 mutedTracks);
 	void deleteFilePlayer(MidiFilePlayer *player);
 
 	void setPlayerVolume(MidiFilePlayer *player, uint8 volume);
 	void setPlayerLoop(MidiFilePlayer *player, bool loop);
+	void setPlayerMutedTracks(MidiFilePlayer *player, uint16 mutedTracks);
 	void stopPlayer(MidiFilePlayer *player);
 	void playPlayer(MidiFilePlayer *player);
 	void pausePlayer(MidiFilePlayer *player);
@@ -108,8 +111,8 @@ private:
 MidiFilePlayer::~MidiFilePlayer() {
 }
 
-MidiFilePlayerImpl::MidiFilePlayerImpl(MidiDriver_BASE *outputDriver, const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, uint32 baseTempo, uint8 volume, bool loop)
-	: _file(file), _outputDriver(outputDriver), _parser(nullptr), _volume(255), _loop(loop) {
+MidiFilePlayerImpl::MidiFilePlayerImpl(MidiDriver_BASE *outputDriver, const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, uint32 baseTempo, uint8 volume, bool loop, uint16 mutedTracks)
+	: _file(file), _outputDriver(outputDriver), _parser(nullptr), _volume(255), _loop(loop), _mutedTracks(mutedTracks) {
 	Common::SharedPtr<MidiParser> parser(MidiParser::createParser_SMF());
 
 	if (file->contents.size() != 0 && parser->loadMusic(&file->contents[0], file->contents.size())) {
@@ -146,6 +149,10 @@ void MidiFilePlayerImpl::setVolume(uint8 volume) {
 	_volume = volume;
 }
 
+void MidiFilePlayerImpl::setMutedTracks(uint16 mutedTracks) {
+	_mutedTracks = mutedTracks;
+}
+
 void MidiFilePlayerImpl::setLoop(bool loop) {
 	_loop = loop;
 }
@@ -204,8 +211,8 @@ void MultiMidiPlayer::onTimer() {
 }
 
 
-MidiFilePlayer *MultiMidiPlayer::createFilePlayer(const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, uint8 volume, bool loop) {
-	Common::SharedPtr<MidiFilePlayerImpl> filePlayer(new MidiFilePlayerImpl(this, file, getBaseTempo(), volume, loop));
+MidiFilePlayer *MultiMidiPlayer::createFilePlayer(const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, uint8 volume, bool loop, uint16 mutedTracks) {
+	Common::SharedPtr<MidiFilePlayerImpl> filePlayer(new MidiFilePlayerImpl(this, file, getBaseTempo(), volume, loop, mutedTracks));
 
 	{
 		Common::StackLock lock(_mutex);
@@ -244,6 +251,11 @@ void MultiMidiPlayer::setPlayerLoop(MidiFilePlayer *player, bool loop) {
 	static_cast<MidiFilePlayerImpl *>(player)->setLoop(loop);
 }
 
+void MultiMidiPlayer::setPlayerMutedTracks(MidiFilePlayer *player, uint16 mutedTracks) {
+	Common::StackLock lock(_mutex);
+	static_cast<MidiFilePlayerImpl *>(player)->setMutedTracks(mutedTracks);
+}
+
 void MultiMidiPlayer::stopPlayer(MidiFilePlayer *player) {
 	Common::StackLock lock(_mutex);
 	static_cast<MidiFilePlayerImpl *>(player)->stop();
@@ -932,7 +944,7 @@ bool ObjectReferenceVariableModifier::SaveLoad::loadInternal(Common::ReadStream
 	return true;
 }
 
-MidiModifier::MidiModifier() : _plugIn(nullptr), _filePlayer(nullptr) {
+MidiModifier::MidiModifier() : _plugIn(nullptr), _filePlayer(nullptr), _mutedTracks(0) {
 }
 
 MidiModifier::~MidiModifier() {
@@ -996,7 +1008,7 @@ VThreadState MidiModifier::consumeMessage(Runtime *runtime, const Common::Shared
 			if (_embeddedFile) {
 				debug(2, "MIDI (%x '%s'): Playing embedded file", getStaticGUID(), getName().c_str());
 				if (!_filePlayer)
-					_filePlayer = _plugIn->getMidi()->createFilePlayer(_embeddedFile, _volume * 255 / 100, _modeSpecific.file.loop);
+					_filePlayer = _plugIn->getMidi()->createFilePlayer(_embeddedFile, _volume * 255 / 100, _modeSpecific.file.loop, _mutedTracks);
 				_plugIn->getMidi()->playPlayer(_filePlayer);
 			} else {
 				debug(2, "MIDI (%x '%s'): Digested execute event but don't have anything to play", getStaticGUID(), getName().c_str());
@@ -1040,6 +1052,24 @@ MiniscriptInstructionOutcome MidiModifier::writeRefAttribute(MiniscriptThread *t
 	return Modifier::writeRefAttribute(thread, result, attrib);
 }
 
+MiniscriptInstructionOutcome MidiModifier::writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib, const DynamicValue &index) {
+	if (attrib == "mutetrack") {
+		int32 asInteger = 0;
+		if (!index.roundToInt(asInteger) || asInteger < 1) {
+			thread->error("Invalid index for mutetrack");
+			return kMiniscriptInstructionOutcomeFailed;
+		}
+
+		result.pod.objectRef = this;
+		result.pod.ptrOrOffset = asInteger - 1;
+		result.pod.ifc = &MuteTrackProxyInterface::_instance;
+
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	return Modifier::writeRefAttributeIndexed(thread, result, attrib, index);
+}
+
 Common::SharedPtr<Modifier> MidiModifier::shallowClone() const {
 	Common::SharedPtr<MidiModifier> clone(new MidiModifier(*this));
 
@@ -1128,6 +1158,49 @@ MiniscriptInstructionOutcome MidiModifier::scriptSetLoop(MiniscriptThread *threa
 	return kMiniscriptInstructionOutcomeContinue;
 }
 
+MiniscriptInstructionOutcome MidiModifier::scriptSetMuteTrack(MiniscriptThread *thread, size_t trackIndex, bool muted) {
+	if (trackIndex >= 16) {
+		thread->error("Invalid track index for mutetrack");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	uint16 mutedTracks = _mutedTracks;
+	uint16 trackMask = 1 << trackIndex;
+
+	if (muted)
+		mutedTracks |= trackMask;
+	else
+		mutedTracks -= (mutedTracks & trackMask);
+
+	if (mutedTracks != _mutedTracks) {
+		_mutedTracks = mutedTracks;
+
+		if (_filePlayer)
+			_plugIn->getMidi()->setPlayerMutedTracks(_filePlayer, mutedTracks);
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome MidiModifier::MuteTrackProxyInterface::write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr ptrOrOffset) const {
+	if (value.getType() != DynamicValueTypes::kBoolean) {
+		thread->error("Invalid type for mutetrack");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	return static_cast<MidiModifier *>(objectRef)->scriptSetMuteTrack(thread, ptrOrOffset, value.getBool());
+}
+
+MiniscriptInstructionOutcome MidiModifier::MuteTrackProxyInterface::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const {
+	return kMiniscriptInstructionOutcomeFailed;
+}
+
+MiniscriptInstructionOutcome MidiModifier::MuteTrackProxyInterface::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const {
+	return kMiniscriptInstructionOutcomeFailed;
+}
+
+MidiModifier::MuteTrackProxyInterface MidiModifier::MuteTrackProxyInterface::_instance;
+
 ListVariableModifier::ListVariableModifier() : _list(new DynamicList()), _preferredContentType(DynamicValueTypes::kInteger) {
 }
 
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index 08c2ca27dbf..b95070dcc62 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -106,6 +106,7 @@ public:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Media Cue Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
@@ -215,8 +216,9 @@ public:
 	bool respondsToEvent(const Event &evt) const override;
 	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
 
-	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib);
-	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib);
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) override;
+	MiniscriptInstructionOutcome writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib, const DynamicValue &index) override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "MIDI Modifier"; }
@@ -224,6 +226,14 @@ public:
 #endif
 
 private:
+	struct MuteTrackProxyInterface : public IDynamicValueWriteInterface {
+		MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr ptrOrOffset) const override;
+		MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const override;
+		MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
+
+		static MuteTrackProxyInterface _instance;
+	};
+
 	Common::SharedPtr<Modifier> shallowClone() const override;
 	const char *getDefaultName() const override;
 
@@ -232,6 +242,8 @@ private:
 	MiniscriptInstructionOutcome scriptSetNoteNum(MiniscriptThread *thread, const DynamicValue &value);
 	MiniscriptInstructionOutcome scriptSetLoop(MiniscriptThread *thread, const DynamicValue &value);
 
+	MiniscriptInstructionOutcome scriptSetMuteTrack(MiniscriptThread *thread, size_t trackIndex, bool muted);
+
 	struct FilePart {
 		bool loop;
 		bool overrideTempo;
@@ -267,6 +279,7 @@ private:
 
 	Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> _embeddedFile;
 
+	uint16 _mutedTracks;
 	bool _isActive;
 
 	StandardPlugIn *_plugIn;


Commit: b8e510f9e4cd2f991d53641d401dea08cfe00c47
    https://github.com/scummvm/scummvm/commit/b8e510f9e4cd2f991d53641d401dea08cfe00c47
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Replicate a ton of quirks needed to get the Bureau booth hint room working correctly

Changed paths:
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/modifiers.h
    engines/mtropolis/notes/notes.txt
    engines/mtropolis/notes/obsidian.txt
    engines/mtropolis/runtime.cpp


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index beb881c3c3c..9c7feb847a9 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -1055,7 +1055,7 @@ MiniscriptInstructionOutcome ImageElement::scriptSetFlushPriority(MiniscriptThre
 	return kMiniscriptInstructionOutcomeContinue;
 }
 
-MToonElement::MToonElement() : _frame(0), _renderedFrame(0), _flushPriority(0), _celStartTimeMSec(0), _isPlaying(false), _playRange(IntRange::create(1, 1)) {
+MToonElement::MToonElement() : _cel(1), _renderedFrame(0), _flushPriority(0), _celStartTimeMSec(0), _isPlaying(false), _playRange(IntRange::create(1, 1)) {
 }
 
 MToonElement::~MToonElement() {
@@ -1080,7 +1080,7 @@ bool MToonElement::load(ElementLoaderContext &context, const Data::MToonElement
 
 bool MToonElement::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
 	if (attrib == "cel") {
-		result.setInt(_frame + 1);
+		result.setInt(_cel);
 		return true;
 	} else if (attrib == "flushpriority") {
 		result.setInt(_flushPriority);
@@ -1104,7 +1104,6 @@ bool MToonElement::readAttribute(MiniscriptThread *thread, DynamicValue &result,
 
 MiniscriptInstructionOutcome MToonElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
 	if (attrib == "cel") {
-		// TODO proper support
 		DynamicValueWriteFuncHelper<MToonElement, &MToonElement::scriptSetCel>::create(this, result);
 		return kMiniscriptInstructionOutcomeContinue;
 	} else if (attrib == "flushpriority") {
@@ -1117,7 +1116,7 @@ MiniscriptInstructionOutcome MToonElement::writeRefAttribute(MiniscriptThread *t
 		DynamicValueWriteFuncHelper<MToonElement, &MToonElement::scriptSetRate>::create(this, result);
 		return kMiniscriptInstructionOutcomeContinue;
 	} else if (attrib == "range") {
-		DynamicValueWriteFuncHelper<MToonElement, &MToonElement::scriptSetRange>::create(this, result);
+		DynamicValueWriteOrRefAttribFuncHelper<MToonElement, &MToonElement::scriptSetRange, &MToonElement::scriptRangeWriteRefAttribute>::create(this, result);
 		return kMiniscriptInstructionOutcomeContinue;
 	}
 
@@ -1141,7 +1140,7 @@ VThreadState MToonElement::consumeCommand(Runtime *runtime, const Common::Shared
 		return kVThreadReturn;
 	}
 
-	return kVThreadReturn;
+	return VisualElement::consumeCommand(runtime, msg);
 }
 
 void MToonElement::activate() {
@@ -1185,10 +1184,14 @@ void MToonElement::render(Window *window) {
 	if (_cachedMToon) {
 		_cachedMToon->optimize(_runtime);
 
-		_cachedMToon->getOrRenderFrame(_renderedFrame, _frame, _renderSurface);
-		_renderedFrame = _frame;
+		uint32 frame = _cel - 1;
+		assert(frame < _metadata->frames.size());
+
+		_cachedMToon->getOrRenderFrame(_renderedFrame, frame, _renderSurface);
+
+		_renderedFrame = frame;
 
-		Rect16 frameRect = _metadata->frames[_frame].rect;
+		Rect16 frameRect = _metadata->frames[frame].rect;
 
 		if (_renderSurface) {
 			Common::Rect srcRect;
@@ -1219,37 +1222,21 @@ void MToonElement::render(Window *window) {
 }
 
 VThreadState MToonElement::startPlayingTask(const StartPlayingTaskData &taskData) {
-	_frame = _playRange.min;
+	_cel = _playRange.min;
 	_paused = false;
 	_isPlaying = false;	// Reset play state, it starts for real in playMedia
 
 	_contentsDirty = true;
 
-	Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kPlay, 0), DynamicValue(), getSelfReference()));
-	Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
-	taskData.runtime->sendMessageOnVThread(dispatch);
-
-	return kVThreadReturn;
-}
-
-VThreadState MToonElement::changeFrameTask(const ChangeFrameTaskData &taskData) {
-	if (taskData.frame == _frame)
-		return kVThreadReturn;
-
-	uint32 minFrame = _playRange.min;
-	uint32 maxFrame = _playRange.max;
-	_frame = taskData.frame;
-
-	_contentsDirty = true;
-
-	if (_frame == minFrame) {
+	// These send in reverse order
+	{
 		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kAtFirstCel, 0), DynamicValue(), getSelfReference()));
 		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
 		taskData.runtime->sendMessageOnVThread(dispatch);
 	}
 
-	if (_frame == maxFrame) {
-		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kAtLastCel, 0), DynamicValue(), getSelfReference()));
+	{
+		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kPlay, 0), DynamicValue(), getSelfReference()));
 		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
 		taskData.runtime->sendMessageOnVThread(dispatch);
 	}
@@ -1258,13 +1245,13 @@ VThreadState MToonElement::changeFrameTask(const ChangeFrameTaskData &taskData)
 }
 
 void MToonElement::playMedia(Runtime *runtime, Project *project) {
-	uint32 targetFrame = _frame;
-
 	if (_paused)
 		return;
 
-	uint32 minFrame = _playRange.min - 1;
-	uint32 maxFrame = _playRange.max - 1;
+	int32 minCel = _playRange.min;
+	int32 maxCel = _playRange.max;
+	int32 sanitizeMaxCel = _metadata->frames.size();
+	int32 targetCel = _cel;
 
 	uint64 playTime = runtime->getPlayTime();
 	if (!_isPlaying) {
@@ -1292,32 +1279,38 @@ void MToonElement::playMedia(Runtime *runtime, Project *project) {
 		// depend on this, since they reset the cel when reaching the last cel but do not unpause.
 		bool ranPastEnd = false;
 
-		size_t framesRemainingToOnePastEnd = isReversed ? (_frame - minFrame + 1) : (maxFrame + 1 - _frame);
+		size_t framesRemainingToOnePastEnd = isReversed ? (_cel - minCel + 1) : (maxCel + 1 - _cel);
 		if (framesRemainingToOnePastEnd <= framesAdvanced) {
 			ranPastEnd = true;
 			if (_loop)
-				targetFrame = isReversed ? maxFrame : minFrame;
+				targetCel = isReversed ? maxCel : minCel;
 			else
-				targetFrame = isReversed ? minFrame : maxFrame;
+				targetCel = isReversed ? minCel : maxCel;
 		} else
-			targetFrame = isReversed ? (_frame - framesAdvanced) : (_frame + framesAdvanced);
+			targetCel = isReversed ? (_cel - framesAdvanced) : (_cel + framesAdvanced);
 
-		if (_frame != targetFrame) {
-			_frame = targetFrame;
+		if (targetCel < 1)
+			targetCel = 1;
+		if (targetCel > sanitizeMaxCel)
+			targetCel = sanitizeMaxCel;
 
+		if (_cel != targetCel) {
+			_cel = targetCel;
 			_contentsDirty = true;
+		}
 
-			if (_frame == maxFrame) {
-				Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(isReversed ? EventIDs::kAtFirstCel : EventIDs::kAtLastCel, 0), DynamicValue(), getSelfReference()));
-				Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
-				runtime->queueMessage(dispatch);
-			}
+		// Events play control events even if no cel advance occurs
+		const bool atFirstCel = (_cel == (isReversed ? maxCel : minCel));
+		const bool atLastCel = (_cel == (isReversed ? minCel : maxCel));
 
-			if (_frame == minFrame) {
-				Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(isReversed ? EventIDs::kAtLastCel : EventIDs::kAtFirstCel, 0), DynamicValue(), getSelfReference()));
-				Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
-				runtime->queueMessage(dispatch);
-			}
+		if (atFirstCel) {
+			Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kAtFirstCel, 0), DynamicValue(), getSelfReference()));
+			Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+			runtime->queueMessage(dispatch);
+		} else if (atLastCel) {		// These can not fire from the same frame transition (see notes)
+			Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kAtLastCel, 0), DynamicValue(), getSelfReference()));
+			Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+			runtime->queueMessage(dispatch);
 		}
 
 		if (ranPastEnd && !_loop) {
@@ -1336,26 +1329,27 @@ void MToonElement::playMedia(Runtime *runtime, Project *project) {
 }
 
 MiniscriptInstructionOutcome MToonElement::scriptSetCel(MiniscriptThread *thread, const DynamicValue &value) {
-	int32 asInteger = 0;
-	if (!value.roundToInt(asInteger)) {
+	int32 newCel = 0;
+	if (!value.roundToInt(newCel)) {
 		thread->error("Attempted to set mToon cel to an invalid value");
 		return kMiniscriptInstructionOutcomeFailed;
 	}
 
-	if (asInteger < _playRange.min)
-		asInteger = _playRange.min;
-	else if (asInteger > _playRange.max)
-		asInteger = _playRange.max;
+	int32 maxCel = _metadata->frames.size();
 
-	uint32 frame = asInteger - 1;
-	_celStartTimeMSec = thread->getRuntime()->getPlayTime();
+	// Intentially ignore play range.  The cel may be set to an out-of-range cel here and will
+	// in fact play from that cel even if it's out of range.  The mariachi hint room near the
+	// Bureau booths in Obsidian depends on this behavior, since it sets the mToon cel and then
+	// sets the range based on the cel value.
 
-	if (frame != _frame) {
-		ChangeFrameTaskData *taskData = thread->getRuntime()->getVThread().pushTask("MToonElement::changeFrameTask", this, &MToonElement::changeFrameTask);
-		taskData->runtime = _runtime;
-		taskData->frame = frame;
+	if (newCel < 1)
+		newCel = 1;
+	if (newCel > maxCel)
+		newCel = maxCel;
 
-		return kMiniscriptInstructionOutcomeYieldToVThreadNoRetry;
+	if (newCel != _cel) {
+		_cel = newCel;
+		_contentsDirty = true;
 	}
 
 	return kMiniscriptInstructionOutcomeContinue;
@@ -1367,34 +1361,78 @@ MiniscriptInstructionOutcome MToonElement::scriptSetRange(MiniscriptThread *thre
 		return kMiniscriptInstructionOutcomeFailed;
 	}
 
-	IntRange intRange = value.getIntRange();
-	size_t numFrames = _metadata->frames.size();
+	return scriptSetRangeTyped(thread, value.getIntRange());
+}
+
+MiniscriptInstructionOutcome MToonElement::scriptSetRangeStart(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger)) {
+		thread->error("Invalid type for mToon range start");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	IntRange range = _playRange;
+	range.min = asInteger;
+	return scriptSetRangeTyped(thread, range);
+}
+
+MiniscriptInstructionOutcome MToonElement::scriptSetRangeEnd(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger)) {
+		thread->error("Invalid type for mToon range start");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	IntRange range = _playRange;
+	range.max = asInteger;
+	return scriptSetRangeTyped(thread, range);
+}
+
+MiniscriptInstructionOutcome MToonElement::scriptRangeWriteRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
+	if (attrib == "start") {
+		DynamicValueWriteFuncHelper<MToonElement, &MToonElement::scriptSetRangeStart>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "end") {
+		DynamicValueWriteFuncHelper<MToonElement, &MToonElement::scriptSetRangeEnd>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	return kMiniscriptInstructionOutcomeFailed;
+}
+
+MiniscriptInstructionOutcome MToonElement::scriptSetRangeTyped(MiniscriptThread *thread, const IntRange &intRangeRef) {
+	IntRange intRange = intRangeRef;
+
+	int32 maxFrame = _metadata->frames.size() - 1;
+
+	// Intentionally buggy sanitization, see notes.
+	const bool isInvertedRange = (intRange.min > intRange.max);
+
 	if (intRange.min < 1)
 		intRange.min = 1;
-	else if (intRange.min > numFrames)
-		intRange.min = numFrames;
+	if (intRange.max > maxFrame)
+		intRange.max = maxFrame;
 
-	if (intRange.max > numFrames)
-		intRange.max = numFrames;
-
-	if (intRange.max < intRange.min)
-		intRange.min = intRange.max;
+	if (isInvertedRange) {
+		_playRange = IntRange::create(intRange.max, intRange.min);
+		if (_rateTimes100000 > 0)
+			_rateTimes100000 = -_rateTimes100000;
+	} else {
+		_playRange = intRange;
+		if (_rateTimes100000 < 0)
+			_rateTimes100000 = -_rateTimes100000;
+	}
 
-	_playRange = intRange;
+	int32 newCel = _cel;
+	if (newCel < intRange.min || newCel > intRange.max)
+		newCel = intRange.min;
 
-	uint32 targetFrame = _frame;
-	uint32 minFrame = intRange.min - 1;
-	uint32 maxFrame = intRange.max - 1;
-	if (targetFrame < minFrame)
-		targetFrame = minFrame;
-	else if (targetFrame > maxFrame)
-		targetFrame = maxFrame;
+	if (newCel < 1 || newCel > maxFrame)
+		newCel = maxFrame;
 
-	if (targetFrame != _frame) {
-		ChangeFrameTaskData *taskData = thread->getRuntime()->getVThread().pushTask("MToonElement::changeFrameTask", this, &MToonElement::changeFrameTask);
-		taskData->frame = targetFrame;
-		taskData->runtime = _runtime;
-		return kMiniscriptInstructionOutcomeYieldToVThreadNoRetry;
+	if (newCel != _cel) {
+		_cel = newCel;
+		_contentsDirty = true;
 	}
 
 	return kMiniscriptInstructionOutcomeContinue;
@@ -1834,7 +1872,7 @@ VThreadState SoundElement::consumeCommand(Runtime *runtime, const Common::Shared
 		return kVThreadReturn;
 	}
 
-	return Structural::consumeCommand(runtime, msg);
+	return NonVisualElement::consumeCommand(runtime, msg);
 }
 
 void SoundElement::activate() {
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index bee0b2c5294..5820f8dfb97 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -213,12 +213,16 @@ private:
 	};
 
 	VThreadState startPlayingTask(const StartPlayingTaskData &taskData);
-	VThreadState changeFrameTask(const ChangeFrameTaskData &taskData);
 
 	void playMedia(Runtime *runtime, Project *project) override;
 	MiniscriptInstructionOutcome scriptSetRate(MiniscriptThread *thread, const DynamicValue &value);
 	MiniscriptInstructionOutcome scriptSetCel(MiniscriptThread *thread, const DynamicValue &value);
 	MiniscriptInstructionOutcome scriptSetRange(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetRangeStart(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetRangeEnd(MiniscriptThread *thread, const DynamicValue &value);
+
+	MiniscriptInstructionOutcome scriptRangeWriteRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib);
+	MiniscriptInstructionOutcome scriptSetRangeTyped(MiniscriptThread *thread, const IntRange &value);
 
 	void onPauseStateChanged();
 
@@ -229,7 +233,6 @@ private:
 
 	uint32 _assetID;
 	int32 _rateTimes100000;
-	uint32 _frame;
 	int32 _flushPriority;
 	uint32 _celStartTimeMSec;
 	bool _isPlaying;	// Is actually rolling media, this is only set by playMedia because it needs to start after scene transition
@@ -242,7 +245,9 @@ private:
 	Common::SharedPtr<CachedMToon> _cachedMToon;
 	Common::SharedPtr<PlayMediaSignaller> _playMediaSignaller;
 
+	// NOTE: To produce proper behavior, these are not sanitized until playMedia.  render must tolerate invalid values without changing them.
 	IntRange _playRange;
+	int32 _cel;
 };
 
 class TextLabelElement : public VisualElement {
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index ca9b4a981c1..950c9c11269 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -296,6 +296,7 @@ public:
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	const char *debugGetTypeName() const override { return "Drag Motion Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
 #endif
 
 private:
diff --git a/engines/mtropolis/notes/notes.txt b/engines/mtropolis/notes/notes.txt
index 5173fd46299..fab615102f2 100644
--- a/engines/mtropolis/notes/notes.txt
+++ b/engines/mtropolis/notes/notes.txt
@@ -127,4 +127,44 @@ messenger modifiers in the main scene, but moving a main scene object will not
 collide with the same object?  Needs more research.
 
 Collisions only occur with visible objects.  It seems they are also not capable
-of colliding with scenes.
\ No newline at end of file
+of colliding with scenes.
+
+
+
+mToon event and cel behavior:
+
+Setting individual values of mToons has extremely quirky behavior with a lot of
+unexpected or broken behavior in edge cases.  In theory, you can reverse an mToon
+by setting its range, but what actually happens is that whenever you set a range,
+the range is sanitized (incorrectly) and the rate is set negative if the
+unsanitized range was backwards.
+
+Setting the range to a forward or backward range causes the rate to be set
+negative or positive.  In theory this is supposed to ensure that the range is
+forward and the rate controls play direction, but it actually has some broken
+cases.
+
+Some sample situations, assuming an mToon with 7 valid frames:
+set mtoon.range to (20 thru 10)
+... will result in a negative rate and range of 10 thru 7.
+
+set mtoon.range.start to 2
+set mtoon.range.end to 4
+set mtoon.range.start to 5
+set mtoon.range.end to 6
+
+... will result in a positive rate and a range of 4 thru 6, because the range
+becomes "5 thru 4" on the third line, which inverts to 5 thru 4.
+
+The cel is always set to a valid value, but this is subject to some quirks
+as well: The cel can be set to an out-of-range value and will go to that cel.
+(The Obsidian booth hint room depends on this behavior.)
+
+Cel changes from scripts and clamping do not fire events.  "At first cel" is
+only fired from play control looping or starting the animation.  "At last cel"
+is only fired from play control.  If the animation is 1 frame, only "at first
+cel" is fired.
+
+"At first cel" is also fired on auto-play even if the animation is paused.
+
+The inter-frame timer is not changed by changing the cel from scripts.
\ No newline at end of file
diff --git a/engines/mtropolis/notes/obsidian.txt b/engines/mtropolis/notes/obsidian.txt
index 07404858b03..3b4b22aa8c2 100644
--- a/engines/mtropolis/notes/obsidian.txt
+++ b/engines/mtropolis/notes/obsidian.txt
@@ -6,3 +6,26 @@ Uses a special MIDI file "ANO&Vol.mid" which triggers AllNoteOff on all channels
 Navigation mapping:
 	current  fwright right       rear       left fwleft
 	+0       +1      +2    +3    +4   +5    +6   +7
+
+
+Known script errors:
+
+Forest:
+Miniscript error in (59db9 'set iVol on GEN_SND_Set_iVol'): Failed to assign value to proxy
+
+Triggers on startup.  Caused by kiCrixVolNormal and kiCrixVolLow variables being absent in
+"Forest Intro" section.  The GUIDs are valid for the "Forest" section though, which is always
+loaded.  Harmless?
+
+
+Miniscript error in (11ace '<set cel> on F199_SetState'): Failed to get a writeable reference to attribute 'cel'
+
+Triggers on viewing the drawing pages in the "Journal" section.  Caused by F199_SetState being posted to an
+image elmeent.  Harmless?  Is "cel" a valid attrib?
+
+
+
+Bureau:
+Miniscript error in (5d6349 '<init> on PE'): Failed to get a writeable reference to attribute 'cel'
+
+Triggers on looking down at the chapter start.  Caused by script setting "cel" on a sound.
\ No newline at end of file
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 44c7f3e5def..0e94d71f6ba 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -2905,7 +2905,10 @@ void Structural::materializeDescendents(Runtime *runtime, ObjectLinkingScope *ou
 
 VThreadState Structural::consumeCommand(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
 	if (Event::create(EventIDs::kUnpause, 0).respondsTo(msg->getEvent())) {
-		_paused = false;
+		if (_paused) {
+			_paused = false;
+			onPauseStateChanged();
+		}
 
 		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kUnpause, 0), DynamicValue(), getSelfReference()));
 		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
@@ -2913,6 +2916,18 @@ VThreadState Structural::consumeCommand(Runtime *runtime, const Common::SharedPt
 
 		return kVThreadReturn;
 	}
+	if (Event::create(EventIDs::kPause, 0).respondsTo(msg->getEvent())) {
+		if (_paused) {
+			_paused = true;
+			onPauseStateChanged();
+		}
+
+		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kPause, 0), DynamicValue(), getSelfReference()));
+		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));
+		runtime->sendMessageOnVThread(dispatch);
+
+		return kVThreadReturn;
+	}
 
 	warning("Command type %i was ignored", msg->getEvent().eventType);
 	return kVThreadReturn;
@@ -2983,6 +2998,9 @@ MiniscriptInstructionOutcome Structural::scriptSetPaused(MiniscriptThread *threa
 	// This is necessary in Obsidian to prevent the rotator lever from triggering when leaving the menu
 	// while at the Bureau light carousel, since the lever isn't flagged as paused but is set paused
 	// via an init script, and the lever trigger is detected via the pause event.
+	//
+	// (It's possible that this is actually yet another case of the event simply not being sent when the
+	// property is set from script... need to verify and update this comment.)
 	if (!thread->getRuntime()->isAwaitingSceneTransition()) {
 		Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(targetValue ? EventIDs::kPause : EventIDs::kUnpause, 0), DynamicValue(), getSelfReference()));
 		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(msgProps, this, false, true, false));


Commit: d7a8f4e674ca42fd6c6c19fa3f59cd554ec9335a
    https://github.com/scummvm/scummvm/commit/d7a8f4e674ca42fd6c6c19fa3f59cd554ec9335a
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix pause event regression

Changed paths:
    engines/mtropolis/runtime.cpp


diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 0e94d71f6ba..62ac240a99a 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -2917,7 +2917,7 @@ VThreadState Structural::consumeCommand(Runtime *runtime, const Common::SharedPt
 		return kVThreadReturn;
 	}
 	if (Event::create(EventIDs::kPause, 0).respondsTo(msg->getEvent())) {
-		if (_paused) {
+		if (!_paused) {
 			_paused = true;
 			onPauseStateChanged();
 		}


Commit: 6f0ada67d7d61a1a24ac4e6e622bc10f0ba22b70
    https://github.com/scummvm/scummvm/commit/6f0ada67d7d61a1a24ac4e6e622bc10f0ba22b70
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix off-by-one clipping last frame of mToon animations

Changed paths:
    engines/mtropolis/elements.cpp


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 9c7feb847a9..646b035f483 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -1403,7 +1403,7 @@ MiniscriptInstructionOutcome MToonElement::scriptRangeWriteRefAttribute(Miniscri
 MiniscriptInstructionOutcome MToonElement::scriptSetRangeTyped(MiniscriptThread *thread, const IntRange &intRangeRef) {
 	IntRange intRange = intRangeRef;
 
-	int32 maxFrame = _metadata->frames.size() - 1;
+	int32 maxFrame = _metadata->frames.size();
 
 	// Intentionally buggy sanitization, see notes.
 	const bool isInvertedRange = (intRange.min > intRange.max);


Commit: 227bd68cc6707b0bc91e739421c432bf3c038145
    https://github.com/scummvm/scummvm/commit/227bd68cc6707b0bc91e739421c432bf3c038145
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Prioritize direct-to-screen videos over layer order for mouse collision

Changed paths:
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 62ac240a99a..5e7058d783e 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -4247,7 +4247,7 @@ bool Runtime::isModifierMouseInteractive(Modifier *modifier, MouseInteractivityT
 	return false;
 }
 
-void Runtime::recursiveFindMouseCollision(Structural *&bestResult, int &bestLayer, int &bestStackHeight, Structural *candidate, int stackHeight, int32 relativeX, int32 relativeY, MouseInteractivityTestType testType) {
+void Runtime::recursiveFindMouseCollision(Structural *&bestResult, int &bestLayer, int &bestStackHeight, bool &bestDirect, Structural *candidate, int stackHeight, int32 relativeX, int32 relativeY, MouseInteractivityTestType testType) {
 	int32 childRelativeX = relativeX;
 	int32 childRelativeY = relativeY;
 	if (candidate->isElement()) {
@@ -4255,10 +4255,20 @@ void Runtime::recursiveFindMouseCollision(Structural *&bestResult, int &bestLaye
 		if (element->isVisual()) {
 			VisualElement *visual = static_cast<VisualElement *>(candidate);
 			int layer = visual->getLayer();
-
-			// Weird layering behavior:
-			// Objects in a higher layer in lower scenes still have higher render order, so they're on top
-			const bool isInFront = (layer > bestLayer) || (layer == bestLayer && stackHeight > bestStackHeight);
+			bool isDirect = visual->isDirectToScreen();
+
+			// Layering priority:
+			bool isInFront = false;
+			if (isDirect && !bestDirect)
+				isInFront = true;
+			else if (isDirect == bestDirect) {
+				if (layer > bestLayer)
+					isInFront = true;
+				else if (layer == bestLayer) {
+					if (stackHeight > bestStackHeight)
+						isInFront = true;
+				}
+			}
 
 			if (isInFront && visual->isMouseInsideBox(relativeX, relativeY) && isStructuralMouseInteractive(visual, testType) && visual->isMouseCollisionAtPoint(relativeX, relativeY)) {
 				bestResult = candidate;
@@ -4273,7 +4283,7 @@ void Runtime::recursiveFindMouseCollision(Structural *&bestResult, int &bestLaye
 
 
 	for (const Common::SharedPtr<Structural> &child : candidate->getChildren()) {
-		recursiveFindMouseCollision(bestResult, bestLayer, bestStackHeight, child.get(), stackHeight, childRelativeX, childRelativeY, testType);
+		recursiveFindMouseCollision(bestResult, bestLayer, bestStackHeight, bestDirect, child.get(), stackHeight, childRelativeX, childRelativeY, testType);
 	}
 }
 
@@ -4641,10 +4651,11 @@ VThreadState Runtime::updateMouseStateTask(const UpdateMouseStateTaskData &data)
 		Structural *tracked = nullptr;
 		int bestSceneStack = INT_MIN;
 		int bestLayer = INT_MIN;
+		bool bestDirect = false;
 
 		for (size_t ri = 0; ri < _sceneStack.size(); ri++) {
 			const SceneStackEntry &sceneStackEntry = _sceneStack[_sceneStack.size() - 1 - ri];
-			recursiveFindMouseCollision(tracked, bestSceneStack, bestLayer, sceneStackEntry.scene.get(), _sceneStack.size() - 1 - ri, _cachedMousePosition.x, _cachedMousePosition.y, kMouseInteractivityTestMouseClick);
+			recursiveFindMouseCollision(tracked, bestSceneStack, bestLayer, bestDirect, sceneStackEntry.scene.get(), _sceneStack.size() - 1 - ri, _cachedMousePosition.x, _cachedMousePosition.y, kMouseInteractivityTestMouseClick);
 		}
 
 		if (tracked) {
@@ -4715,10 +4726,11 @@ VThreadState Runtime::updateMousePositionTask(const UpdateMousePositionTaskData
 	Structural *collisionItem = nullptr;
 	int bestSceneStack = INT_MIN;
 	int bestLayer = INT_MIN;
+	bool bestDirect = false;
 
 	for (size_t ri = 0; ri < _sceneStack.size(); ri++) {
 		const SceneStackEntry &sceneStackEntry = _sceneStack[_sceneStack.size() - 1 - ri];
-		recursiveFindMouseCollision(collisionItem, bestSceneStack, bestLayer, sceneStackEntry.scene.get(), _sceneStack.size() - 1 - ri, data.x, data.y, kMouseInteractivityTestAnything);
+		recursiveFindMouseCollision(collisionItem, bestSceneStack, bestLayer, bestDirect, sceneStackEntry.scene.get(), _sceneStack.size() - 1 - ri, data.x, data.y, kMouseInteractivityTestAnything);
 	}
 
 	Common::SharedPtr<Structural> newMouseOver;
@@ -6837,6 +6849,15 @@ void VisualElement::finalizeRender() {
 	_contentsDirty = false;
 }
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+void VisualElement::debugInspect(IDebugInspectionReport *report) const {
+	report->declareDynamic("layer", Common::String::format("%i", static_cast<int>(_layer)));
+	report->declareDynamic("relRect", Common::String::format("(%i,%i)-(%i,%i)", static_cast<int>(_rect.left), static_cast<int>(_rect.top), static_cast<int>(_rect.right), static_cast<int>(_rect.bottom)));
+
+	Element::debugInspect(report);
+}
+#endif
+
 MiniscriptInstructionOutcome VisualElement::scriptSetVisibility(MiniscriptThread *thread, const DynamicValue &result) {
 	// FIXME: Need to make this fire Show/Hide events!
 	if (result.getType() == DynamicValueTypes::kBoolean) {
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 1537847c0a8..4af476f372a 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -1694,7 +1694,7 @@ private:
 
 	static bool isStructuralMouseInteractive(Structural *structural, MouseInteractivityTestType testType);
 	static bool isModifierMouseInteractive(Modifier *modifier, MouseInteractivityTestType testType);
-	static void recursiveFindMouseCollision(Structural *&bestResult, int &bestLayer, int &bestStackHeight, Structural *candidate, int stackHeight, int32 relativeX, int32 relativeY, MouseInteractivityTestType testType);
+	static void recursiveFindMouseCollision(Structural *&bestResult, int &bestLayer, int &bestStackHeight, bool &bestDirect, Structural *candidate, int stackHeight, int32 relativeX, int32 relativeY, MouseInteractivityTestType testType);
 
 	void queueEventAsLowLevelSceneStateTransitionAction(const Event &evt, Structural *root, bool cascade, bool relay);
 
@@ -2487,6 +2487,10 @@ public:
 	virtual void render(Window *window) = 0;
 	void finalizeRender();
 
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	void debugInspect(IDebugInspectionReport *report) const override;
+#endif
+
 protected:
 	bool loadCommon(const Common::String &name, uint32 guid, const Data::Rect &rect, uint32 elementFlags, uint16 layer, uint32 streamLocator, uint16 sectionID);
 


Commit: 6fd87b94cddbef399606ec4beb63d5cfdafa3805
    https://github.com/scummvm/scummvm/commit/6fd87b94cddbef399606ec4beb63d5cfdafa3805
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix direct-to-screen prioritization not working

Changed paths:
    engines/mtropolis/runtime.cpp


diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 5e7058d783e..81e6b6ec9ae 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -4274,6 +4274,7 @@ void Runtime::recursiveFindMouseCollision(Structural *&bestResult, int &bestLaye
 				bestResult = candidate;
 				bestLayer = layer;
 				bestStackHeight = stackHeight;
+				bestDirect = isDirect;
 			}
 
 			childRelativeX -= visual->getRelativeRect().left;


Commit: 0497f6c9ec9604999eff724522bb0749953b07ea
    https://github.com/scummvm/scummvm/commit/0497f6c9ec9604999eff724522bb0749953b07ea
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix missing "cache" attribute and fix mToon "at last cel" triggering multiple times

Changed paths:
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 646b035f483..3b2536d7fb7 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -176,6 +176,25 @@ bool GraphicElement::load(ElementLoaderContext &context, const Data::GraphicElem
 	return true;
 }
 
+bool GraphicElement::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "cache") {
+		result.setBool(_cacheBitmap);
+		return true;
+	}
+
+	return VisualElement::readAttribute(thread, result, attrib);
+}
+
+MiniscriptInstructionOutcome GraphicElement::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
+	if (attrib == "cache") {
+		DynamicValueWriteBoolHelper::create(&_cacheBitmap, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	return VisualElement::writeRefAttribute(thread, result, attrib);
+}
+
+
 void GraphicElement::render(Window *window) {
 	if (_renderProps.getInkMode() == VisualElementRenderProperties::kInkModeDefault || _renderProps.getInkMode() == VisualElementRenderProperties::kInkModeInvisible || !_rect.isValid()) {
 		// Not rendered at all
@@ -1277,9 +1296,15 @@ void MToonElement::playMedia(Runtime *runtime, Project *project) {
 		// This needs to be handled correctly: Reaching the last frame triggers At Last Cel or At First Cel,
 		// but going PAST the end frame triggers automatic stop and pause. The Obsidian bureau filing cabinets
 		// depend on this, since they reset the cel when reaching the last cel but do not unpause.
+
+		// There's actually some weird stuff we don't handle here where the play control range is invalid, in
+		// which case the timing of "at last cel"/"at first cel" triggers based on where the timer would be
+		// in the invalid range, so mTropolis Player apparently keeps a play cel independent of the actual
+		// cel?
 		bool ranPastEnd = false;
 
 		size_t framesRemainingToOnePastEnd = isReversed ? (_cel - minCel + 1) : (maxCel + 1 - _cel);
+		bool alreadyAtLastCel = (framesRemainingToOnePastEnd == 1);
 		if (framesRemainingToOnePastEnd <= framesAdvanced) {
 			ranPastEnd = true;
 			if (_loop)
@@ -1289,6 +1314,7 @@ void MToonElement::playMedia(Runtime *runtime, Project *project) {
 		} else
 			targetCel = isReversed ? (_cel - framesAdvanced) : (_cel + framesAdvanced);
 
+		int32 playControlTargetCel = targetCel;
 		if (targetCel < 1)
 			targetCel = 1;
 		if (targetCel > sanitizeMaxCel)
@@ -1300,8 +1326,8 @@ void MToonElement::playMedia(Runtime *runtime, Project *project) {
 		}
 
 		// Events play control events even if no cel advance occurs
-		const bool atFirstCel = (_cel == (isReversed ? maxCel : minCel));
-		const bool atLastCel = (_cel == (isReversed ? minCel : maxCel));
+		bool atFirstCel = (targetCel == (isReversed ? maxCel : minCel));
+		bool atLastCel = (targetCel == (isReversed ? minCel : maxCel)) && !(ranPastEnd && alreadyAtLastCel);
 
 		if (atFirstCel) {
 			Common::SharedPtr<MessageProperties> msgProps(new MessageProperties(Event::create(EventIDs::kAtFirstCel, 0), DynamicValue(), getSelfReference()));
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index 5820f8dfb97..70b1d3456c2 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -57,6 +57,9 @@ public:
 
 	bool load(ElementLoaderContext &context, const Data::GraphicElement &data);
 
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) override;
+
 	void render(Window *window) override;
 
 #ifdef MTROPOLIS_DEBUG_ENABLE


Commit: c14ece6d9428a0371557888f7388fcc3b3c64519
    https://github.com/scummvm/scummvm/commit/c14ece6d9428a0371557888f7388fcc3b3c64519
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Off by default

Changed paths:
    engines/mtropolis/configure.engine


diff --git a/engines/mtropolis/configure.engine b/engines/mtropolis/configure.engine
index cb109add02a..df678b51cdb 100644
--- a/engines/mtropolis/configure.engine
+++ b/engines/mtropolis/configure.engine
@@ -1,3 +1,3 @@
 # This file is included from the main "configure" script
 # add_engine [name] [desc] [build-by-default] [subengines] [base games] [deps]
-add_engine mtropolis "mTropolis" yes
+add_engine mtropolis "mTropolis" no


Commit: da8d2517132e43ec0691e22ee2ff5ec5431b64e5
    https://github.com/scummvm/scummvm/commit/da8d2517132e43ec0691e22ee2ff5ec5431b64e5
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Clean up some things from initial review

Changed paths:
  A engines/mtropolis/POTFILES
    engines/mtropolis/detection.cpp
    engines/mtropolis/detection_tables.h
    engines/mtropolis/metaengine.cpp
    engines/mtropolis/module.mk
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/render.cpp
    engines/mtropolis/runtime.cpp


diff --git a/engines/mtropolis/POTFILES b/engines/mtropolis/POTFILES
new file mode 100644
index 00000000000..2162ac57fd0
--- /dev/null
+++ b/engines/mtropolis/POTFILES
@@ -0,0 +1,2 @@
+engines/mtropolis/detection.cpp
+engines/mtropolis/mtropolis.cpp
diff --git a/engines/mtropolis/detection.cpp b/engines/mtropolis/detection.cpp
index 46d91eee4e9..fccbdb4f637 100644
--- a/engines/mtropolis/detection.cpp
+++ b/engines/mtropolis/detection.cpp
@@ -61,8 +61,6 @@ public:
 	}
 
 	const ExtraGuiOptions getExtraGuiOptions(const Common::String &target) const override;
-
-	ADDetectedGame fallbackDetect(const FileMap &allFiles, const Common::FSList &fslist, ADDetectedGameExtraInfo **extra) const override;
 };
 
 const ExtraGuiOptions MTropolisMetaEngineDetection::getExtraGuiOptions(const Common::String &target) const {
@@ -87,18 +85,4 @@ const ExtraGuiOptions MTropolisMetaEngineDetection::getExtraGuiOptions(const Com
 	return options;
 }
 
-ADDetectedGame MTropolisMetaEngineDetection::fallbackDetect(const FileMap &allFiles, const Common::FSList &fslist, ADDetectedGameExtraInfo **extra) const {
-	// Set the default values for the fallback descriptor's ADGameDescription part.
-	MTropolis::g_fallbackDesc.desc.language = Common::UNK_LANG;
-	MTropolis::g_fallbackDesc.desc.platform = Common::kPlatformDOS;
-	MTropolis::g_fallbackDesc.desc.flags = ADGF_NO_FLAGS;
-
-	// Set default values for the fallback descriptor's MTropolisGameDescription part.
-	MTropolis::g_fallbackDesc.gameID = 0;
-	MTropolis::g_fallbackDesc.version = 0;
-
-	//return (const ADGameDescription *)&MTropolis::g_fallbackDesc;
-	return ADDetectedGame();
-}
-
 REGISTER_PLUGIN_STATIC(MTROPOLIS_DETECTION, PLUGIN_TYPE_ENGINE_DETECTION, MTropolisMetaEngineDetection);
diff --git a/engines/mtropolis/detection_tables.h b/engines/mtropolis/detection_tables.h
index 61b01e78421..d41f9810ffc 100644
--- a/engines/mtropolis/detection_tables.h
+++ b/engines/mtropolis/detection_tables.h
@@ -33,12 +33,12 @@ static const MTropolisGameDescription gameDescriptions[] = {
 			"obsidian",
 			"V1.0, 1/13/97, installed, CD",
 			{
-				{ "Obsidian Installer", 0, "1c272c23dc50b771970cabe8410c9349", -1 },
-				{ "Obsidian Data 2", 0, "1e590e3154c1af09efb951a07abc48b8", -1 },
-				{ "Obsidian Data 3", 0, "48e514a594b7a7ad190351d6d32d5d33", -1 },
-				{ "Obsidian Data 4", 0, "8dfa726c675aae3778951ddd18e4484c", -1 },
-				{ "Obsidian Data 5", 0, "6f085578b13b3db99543b969c9009b17", -1 },
-				{ "Obsidian Data 6", 0, "120ddcb1780be0f6380d708041733406", -1 },
+				{ "Obsidian Installer", 0, "1c272c23dc50b771970cabe8410c9349", 9250304 },
+				{ "Obsidian Data 2", 0, "1e590e3154c1af09efb951a07abc48b8", 563287808 },
+				{ "Obsidian Data 3", 0, "48e514a594b7a7ad190351d6d32d5d33", 617413632 },
+				{ "Obsidian Data 4", 0, "8dfa726c675aae3778951ddd18e4484c", 599297536 },
+				{ "Obsidian Data 5", 0, "6f085578b13b3db99543b969c9009b17", 583581056 },
+				{ "Obsidian Data 6", 0, "120ddcb1780be0f6380d708041733406", 558315648 },
 				AD_LISTEND
 			},
 			Common::EN_ANY,
@@ -56,17 +56,17 @@ static const MTropolisGameDescription gameDescriptions[] = {
 			"obsidian",
 			"V1.0, 1/13/97, installed, CD",
 			{
-				{ "Obsidian.exe", 0, "0b50a779136ae6c9cc8bcfa3148c1127", -1 },
-				{ "Obsidian.c95", 0, "fea68ff30ff319cdab30b79d2850a480", -1 },
-				{ "RSGKit.r95", 0, "071dc9098f9610fcec45c96342b1b69a", -1 },
-				{ "MCURSORS.C95", 0, "dcbe480913eebf233d0cdc33809bf048", -1 },
+				{ "Obsidian.exe", 0, "0b50a779136ae6c9cc8bcfa3148c1127", 762368 },
+				{ "Obsidian.c95", 0, "fea68ff30ff319cdab30b79d2850a480", 145920 },
+				{ "RSGKit.r95", 0, "071dc9098f9610fcec45c96342b1b69a", 625152 },
+				{ "MCURSORS.C95", 0, "dcbe480913eebf233d0cdc33809bf048", 87040 },
 				//{ "Start Obsidian", 0, "51a4980089bb35da0f7f6381382d2889", -1 },
-				{ "Obsidian Data 1.MPL", 0, "9531162c32272c33837074be4646422a", -1 },
-				{ "Obsidian Data 2.MPX", 0, "c13c9be0ab0482a952532fa647a67a7a", -1 },
-				{ "Obsidian Data 3.MPX", 0, "35d8332221a7236b122b43233428f5dc", -1 },
-				{ "Obsidian Data 4.MPX", 0, "263fe824a1dd6f91390bce447c01e54c", -1 },
-				{ "Obsidian Data 5.MPX", 0, "894e4712a7bfb1b3c54086d43e6f3bb7", -1 },
-				{ "Obsidian Data 6.MPX", 0, "f491955b858e1a41d25efbb060424833", -1 },
+				{ "Obsidian Data 1.MPL", 0, "9531162c32272c33837074be4646422a", 14755456 },
+				{ "Obsidian Data 2.MPX", 0, "c13c9be0ab0482a952532fa647a67a7a", 558175757 },
+				{ "Obsidian Data 3.MPX", 0, "35d8332221a7236b122b43233428f5dc", 614504412 },
+				{ "Obsidian Data 4.MPX", 0, "263fe824a1dd6f91390bce447c01e54c", 597911854 },
+				{ "Obsidian Data 5.MPX", 0, "894e4712a7bfb1b3c54086d43e6f3bb7", 576841795 },
+				{ "Obsidian Data 6.MPX", 0, "f491955b858e1a41d25efbb060424833", 554803689 },
 				AD_LISTEND
 			},
 			Common::EN_ANY,
@@ -82,25 +82,6 @@ static const MTropolisGameDescription gameDescriptions[] = {
 	{ AD_TABLE_END_MARKER, 0, 0, 0 }
 };
 
-/**
- * The fallback game descriptor used by the mTropolis engine's fallbackDetector.
- * Contents of this struct are to be overwritten by the fallbackDetector.
- */
-static MTropolisGameDescription g_fallbackDesc = {
-	{
-		"",
-		"",
-		AD_ENTRY1(0, 0), // This should always be AD_ENTRY1(0, 0) in the fallback descriptor
-		Common::UNK_LANG,
-		Common::kPlatformWindows,
-		ADGF_NO_FLAGS,
-		GUIO0()
-	},
-	0,
-	0,
-	0,
-};
-
 } // End of namespace MTropolisEngine
 
 #endif
diff --git a/engines/mtropolis/metaengine.cpp b/engines/mtropolis/metaengine.cpp
index 680c3444000..4ade910fea5 100644
--- a/engines/mtropolis/metaengine.cpp
+++ b/engines/mtropolis/metaengine.cpp
@@ -30,6 +30,8 @@
 #include "backends/keymapper/action.h"
 #include "backends/keymapper/keymap.h"
 
+#include "common/translation.h"
+
 namespace MTropolis {
 
 uint32 MTropolisEngine::getGameID() const {
@@ -93,7 +95,7 @@ Common::Array<Common::Keymap *> MTropolisMetaEngine::initKeymaps(const char *tar
 	Common::Keymap *keymap = new Common::Keymap(Common::Keymap::kKeymapTypeGame, "mtropolis", "mTropolis");
 
 	Common::Action *act;
-	act = new Common::Action("DEBUG_TOGGLE_OVERLAY", Common::convertUtf8ToUtf32("Toggle debug overlay"));
+	act = new Common::Action("DEBUG_TOGGLE_OVERLAY", _("Toggle debug overlay"));
 	act->setCustomEngineActionEvent(MTropolis::Actions::kDebugToggleOverlay);
 	act->addDefaultInputMapping("F10");
 	keymap->addAction(act);
diff --git a/engines/mtropolis/module.mk b/engines/mtropolis/module.mk
index 2a5892e4bbc..b5febdf1c2b 100644
--- a/engines/mtropolis/module.mk
+++ b/engines/mtropolis/module.mk
@@ -3,7 +3,6 @@ MODULE := engines/mtropolis
 MODULE_OBJS = \
 	asset_factory.o \
 	assets.o \
-	console.o \
 	data.o \
 	debug.o \
 	detection.o \
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index 491a50ab6c0..f92874d41ca 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -22,7 +22,6 @@
 #include "mtropolis/mtropolis.h"
 
 #include "mtropolis/actions.h"
-#include "mtropolis/console.h"
 #include "mtropolis/debug.h"
 #include "mtropolis/runtime.h"
 
@@ -547,7 +546,6 @@ Common::Error MTropolisEngine::run() {
 	_runtime->setDisplayResolution(preferredWidth, preferredHeight);
 
 	initGraphics(preferredWidth, preferredHeight, &modePixelFormats[selectedMode]);
-	setDebugger(new Console(this));
 
 #ifdef MTROPOLIS_DEBUG_ENABLE
 	if (ConfMan.getBool("mtropolis_debug_at_start")) {
@@ -573,6 +571,7 @@ Common::Error MTropolisEngine::run() {
 			break;
 
 		_runtime->drawFrame();
+		_system->delayMillis(10);
 	}
 
 	_runtime.release();
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 62c5cb7c0f7..e88c3d7ceaa 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -182,7 +182,7 @@ void MidiFilePlayerImpl::send(uint32 b) {
 }
 
 MultiMidiPlayer::MultiMidiPlayer() {
-	createDriver(MDT_MIDI | MDT_PREFER_GM);
+	createDriver(MDT_MIDI | MDT_ADLIB | MDT_PREFER_GM);
 
 	if (_driver->open() != 0) {
 		_driver->close();
@@ -1656,7 +1656,7 @@ const StandardPlugInHacks &StandardPlugIn::getHacks() const {
 	return _hacks;
 }
 
-StandardPlugInHacks& StandardPlugIn::getHacks() {
+StandardPlugInHacks &StandardPlugIn::getHacks() {
 	return _hacks;
 }
 
diff --git a/engines/mtropolis/render.cpp b/engines/mtropolis/render.cpp
index 51772ef2f0c..7e78aa764f4 100644
--- a/engines/mtropolis/render.cpp
+++ b/engines/mtropolis/render.cpp
@@ -276,15 +276,19 @@ void renderProject(Runtime *runtime, Window *mainWindow) {
 
 	if (!sceneChanged) {
 		for (Common::Array<RenderItem>::const_iterator it = normalBucket.begin(), itEnd = normalBucket.end(); it != itEnd; ++it) {
-			if (it->element->needsRender())
+			if (it->element->needsRender()) {
 				sceneChanged = true;
+				break;
+			}
 		}
 	}
 
 	if (!sceneChanged) {
 		for (Common::Array<RenderItem>::const_iterator it = directBucket.begin(), itEnd = directBucket.end(); it != itEnd; ++it) {
-			if (it->element->needsRender())
+			if (it->element->needsRender()) {
 				sceneChanged = true;
+				break;
+			}
 		}
 	}
 
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 81e6b6ec9ae..331781a2742 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -3776,8 +3776,7 @@ bool Runtime::runFrame() {
 	return true;
 }
 
-struct WindowSortingBucket
-{
+struct WindowSortingBucket {
 	size_t originalIndex;
 	Window *window;
 
@@ -3932,8 +3931,7 @@ void Runtime::executeTeardown(const Teardown &teardown) {
 }
 
 void Runtime::executeLowLevelSceneStateTransition(const LowLevelSceneStateTransitionAction &action) {
-	switch (action.getActionType())
-	{
+	switch (action.getActionType()) {
 	case LowLevelSceneStateTransitionAction::kSendMessage:
 		sendMessageOnVThread(action.getMessage());
 		break;


Commit: af2594166978e32b77731ab17431d04a441bc9d5
    https://github.com/scummvm/scummvm/commit/af2594166978e32b77731ab17431d04a441bc9d5
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Replace Rect16 with Common::Rect

Changed paths:
    engines/mtropolis/assets.cpp
    engines/mtropolis/assets.h
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/elements.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/plugin/obsidian.cpp
    engines/mtropolis/plugin/obsidian.h
    engines/mtropolis/render.cpp
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index 1ac53030cc9..7d5755f7984 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -124,8 +124,8 @@ bool CachedMToon::loadFromStream(const Common::SharedPtr<MToonMetadata> &metadat
 	if (metadata->codecID == kMToonRLECodecID) {
 		loadRLEFrames(data);
 
-		uint16 fullWidth = metadata->rect.getWidth();
-		uint16 fullHeight = metadata->rect.getHeight();
+		uint16 fullWidth = metadata->rect.width();
+		uint16 fullHeight = metadata->rect.height();
 
 		uint16 firstFrameWidth = 0;
 		uint16 firstFrameHeight = 0;
@@ -387,8 +387,8 @@ void CachedMToon::loadUncompressedFrame(const Common::Array<uint8> &data, size_t
 	else
 		error("Unknown mToon encoding");
 
-	size_t w = frameDef.rect.getWidth();
-	size_t h = frameDef.rect.getHeight();
+	size_t w = frameDef.rect.width();
+	size_t h = frameDef.rect.height();
 
 	surface->create(w, h, pixFmt);
 
@@ -568,7 +568,7 @@ void CachedMToon::getOrRenderFrame(uint32 prevFrame, uint32 targetFrame, Common:
 
 		if (!surface || surface->format != _rleOptimizedFormat) {
 			surface.reset(new Graphics::Surface());
-			surface->create(_metadata->rect.getWidth(), _metadata->rect.getHeight(), _rleOptimizedFormat);
+			surface->create(_metadata->rect.width(), _metadata->rect.height(), _rleOptimizedFormat);
 		}
 
 		bool isBottomUp = (_metadata->imageFormat == MToonMetadata::kImageFormatWindows);
@@ -757,7 +757,7 @@ ImageAsset::~ImageAsset() {
 
 bool ImageAsset::load(AssetLoaderContext &context, const Data::ImageAsset &data) {
 	_assetID = data.assetID;
-	if (!_rect.load(data.rect1))
+	if (!data.rect1.toScummVMRect(_rect))
 		return false;
 	_filePosition = data.filePosition;
 	_size = data.size;
@@ -800,7 +800,7 @@ AssetType ImageAsset::getAssetType() const {
 	return kAssetTypeImage;
 }
 
-const Rect16& ImageAsset::getRect() const {
+const Common::Rect &ImageAsset::getRect() const {
 	return _rect;
 }
 
@@ -842,7 +842,7 @@ const Common::SharedPtr<CachedImage> &ImageAsset::loadAndCacheImage(Runtime *run
 
 	size_t bytesPerRow = 0;
 
-	Rect16 imageRect = getRect();
+	Common::Rect imageRect = getRect();
 	int width = imageRect.right - imageRect.left;
 	int height = imageRect.bottom - imageRect.top;
 
@@ -992,7 +992,7 @@ bool MToonAsset::load(AssetLoaderContext &context, const Data::MToonAsset &data)
 	_frameDataPosition = data.frameDataPosition;
 	_sizeOfFrameData = data.sizeOfFrameData;
 
-	if (!_metadata->rect.load(data.rect))
+	if (!data.rect.toScummVMRect(_metadata->rect))
 		return false;
 
 	_metadata->bitsPerPixel = data.bitsPerPixel;
@@ -1052,7 +1052,7 @@ bool MToonMetadata::FrameDef::load(AssetLoaderContext &context, const Data::MToo
 	decompressedBytesPerRow = data.decompressedBytesPerRow;
 	decompressedSize = data.decompressedSize;
 	isKeyFrame = (data.keyframeFlag != 0);
-	if (!rect.load(data.rect1))
+	if (!data.rect1.toScummVMRect(rect))
 		return false;
 
 	return true;
@@ -1086,13 +1086,13 @@ bool TextAsset::load(AssetLoaderContext &context, const Data::TextAsset &data) {
 	_isBitmap = ((data.isBitmap & 1) != 0);
 
 	if (_isBitmap) {
-		if (!_bitmapRect.load(data.bitmapRect))
+		if (!data.bitmapRect.toScummVMRect(_bitmapRect))
 			return false;
 
 		_bitmapData.reset(new Graphics::ManagedSurface());
 
-		uint16 width = _bitmapRect.getWidth();
-		uint16 height = _bitmapRect.getHeight();
+		uint16 width = _bitmapRect.width();
+		uint16 height = _bitmapRect.height();
 
 		uint16 pitch = (data.pitchBigEndian[0] << 8) + data.pitchBigEndian[1];
 
@@ -1118,7 +1118,7 @@ bool TextAsset::load(AssetLoaderContext &context, const Data::TextAsset &data) {
 			}
 		}
 	} else {
-		_bitmapRect = Rect16::create(0, 0, 0, 0);
+		_bitmapRect = Common::Rect(0, 0, 0, 0);
 
 		_stringData = data.text;
 
diff --git a/engines/mtropolis/assets.h b/engines/mtropolis/assets.h
index c1b11a51958..07285adb400 100644
--- a/engines/mtropolis/assets.h
+++ b/engines/mtropolis/assets.h
@@ -67,7 +67,7 @@ struct MToonMetadata {
 	};
 
 	struct FrameDef {
-		Rect16 rect;
+		Common::Rect rect;
 		uint32 dataOffset;
 		uint32 compressedSize;
 		uint32 decompressedSize;
@@ -88,7 +88,7 @@ struct MToonMetadata {
 
 	ImageFormat imageFormat;
 
-	Rect16 rect;
+	Common::Rect rect;
 	uint16 bitsPerPixel;
 	uint32 codecID;
 	uint32 encodingFlags;
@@ -240,7 +240,7 @@ public:
 		kImageFormatWindows,
 	};
 
-	const Rect16 &getRect() const;
+	const Common::Rect &getRect() const;
 	ColorDepthMode getColorDepth() const;
 	uint32 getFilePosition() const;
 	uint32 getSize() const;
@@ -250,7 +250,7 @@ public:
 	const Common::SharedPtr<CachedImage> &loadAndCacheImage(Runtime *runtime);
 
 private:
-	Rect16 _rect;
+	Common::Rect _rect;
 	ColorDepthMode _colorDepth;
 	uint32 _filePosition;
 	uint32 _size;
@@ -288,7 +288,7 @@ public:
 	const Common::Array<MacFormattingSpan> &getMacFormattingSpans() const;
 
 private:
-	Rect16 _bitmapRect;
+	Common::Rect _bitmapRect;
 	TextAlignment _alignment;
 	bool _isBitmap;
 
diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index 46d33548b33..e6dcd7f5229 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -301,6 +301,21 @@ bool Rect::load(DataReader &reader) {
 		return false;
 }
 
+bool Rect::toScummVMRect(Common::Rect &outRect) const {
+	if (left > right || top > bottom)
+		return false;
+	outRect = Common::Rect(left, top, right, bottom);
+	return true;
+}
+
+bool Rect::toScummVMRectUnchecked(Common::Rect &outRect) const {
+	outRect.top = top;
+	outRect.left = left;
+	outRect.bottom = bottom;
+	outRect.right = right;
+	return true;
+}
+
 bool Point::load(DataReader &reader) {
 	if (reader.getProjectFormat() == kProjectFormatMacintosh)
 		return reader.readS16(y) && reader.readS16(x);
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index 225c419501a..3e110b8607b 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -28,6 +28,7 @@
 #include "common/hash-str.h"
 #include "common/ptr.h"
 #include "common/stream.h"
+#include "common/rect.h"
 
 // This contains defs related to parsing of mTropolis stored data into structured data objects.
 // This is separated from asset construction for a number of reasons, mainly that data parsing has
@@ -211,6 +212,9 @@ private:
 struct Rect {
 	bool load(DataReader &reader);
 
+	bool toScummVMRect(Common::Rect &outRect) const;
+	bool toScummVMRectUnchecked(Common::Rect &outRect) const;
+
 	int16 top;
 	int16 left;
 	int16 bottom;
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 3b2536d7fb7..bcf5b9a7019 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -196,7 +196,7 @@ MiniscriptInstructionOutcome GraphicElement::writeRefAttribute(MiniscriptThread
 
 
 void GraphicElement::render(Window *window) {
-	if (_renderProps.getInkMode() == VisualElementRenderProperties::kInkModeDefault || _renderProps.getInkMode() == VisualElementRenderProperties::kInkModeInvisible || !_rect.isValid()) {
+	if (_renderProps.getInkMode() == VisualElementRenderProperties::kInkModeDefault || _renderProps.getInkMode() == VisualElementRenderProperties::kInkModeInvisible || _rect.isEmpty()) {
 		// Not rendered at all
 		_mask.reset();
 		return;
@@ -208,14 +208,14 @@ void GraphicElement::render(Window *window) {
 	const bool needsMask = (_renderProps.getShape() != VisualElementRenderProperties::kShapeRect);
 	bool needsMaskRedraw = _renderProps.isDirty();
 
-	uint16 width = _rect.getWidth();
-	uint16 height = _rect.getHeight();
+	uint16 width = _rect.width();
+	uint16 height = _rect.height();
 
 	if (needsMask) {
 		if (!_mask || _mask->w != width || _mask->h != height) {
 			_mask.reset();
 			_mask.reset(new Graphics::ManagedSurface());
-			_mask->create(_rect.getWidth(), _rect.getHeight(), Graphics::PixelFormat::createFormatCLUT8());
+			_mask->create(_rect.width(), _rect.height(), Graphics::PixelFormat::createFormatCLUT8());
 
 			needsMaskRedraw = true;
 		}
@@ -250,6 +250,19 @@ void GraphicElement::render(Window *window) {
 		// Notes for future:
 		// Rounded rect corner arc size is fixed at 13x13 unless the graphic is smaller.
 
+		// TODO: Overhaul this again to be more accurate, it was designed to work "OpenGL-style"
+		// where a point exactly on an edge would be excluded on a right/bottom edge to ensure
+		// that the canvas puzzle in Obsidian worked correctly, but it turns out that the canvas
+		// puzzle only uses polys for the mouse collision and uses a special shape ID for the
+		// triangles.  This most likely is supposed to work "QuickDraw style" where the pixel
+		// coordinates of the poly points are always included.
+		//
+		// Maybe it doesn't matter, but if we find a game in the future with polygons, and
+		// there are 1-pixel gaps or something.... that's why.  And that's what needs to be fixed.
+		//
+		// In fact, because of that, we should probably just use ScummVM's draw routines instead
+		// of this code here.  Sigh.
+
 		if (shape == VisualElementRenderProperties::kShapePolygon && polyPoints->size() >= 3) {
 			_mask->clear(0);
 
@@ -418,19 +431,20 @@ void GraphicElement::render(Window *window) {
 		}
 	}
 
-	Rect16 srcRect = Rect16::create(0, 0, _rect.getWidth(), _rect.getHeight());
-	Rect16 drawRect = srcRect.translate(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y);
+	Common::Rect srcRect = Common::Rect(0, 0, _rect.width(), _rect.height());
+	Common::Rect drawRect = srcRect;
+	drawRect.translate(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y);
 
-	Rect16 windowRect = Rect16::create(0, 0, window->getWidth(), window->getHeight());
-	Rect16 clippedDrawRect = drawRect.intersect(windowRect);
+	Common::Rect windowRect = Common::Rect(0, 0, window->getWidth(), window->getHeight());
+	Common::Rect clippedDrawRect = drawRect.findIntersectingRect(windowRect);
 
-	Rect16 clippedSrcRect = srcRect;
+	Common::Rect clippedSrcRect = srcRect;
 	clippedSrcRect.left += clippedDrawRect.left - drawRect.left;
 	clippedSrcRect.top += clippedDrawRect.top - drawRect.top;
 	clippedSrcRect.right += clippedDrawRect.right - drawRect.right;
 	clippedSrcRect.bottom += clippedDrawRect.bottom - drawRect.bottom;
 
-	if (!clippedSrcRect.isValid())
+	if (clippedSrcRect.isEmpty())
 		return;
 
 	int32 srcToDestX = clippedDrawRect.left - clippedSrcRect.left;
@@ -448,7 +462,7 @@ void GraphicElement::render(Window *window) {
 
 			for (int32 srcY = clippedSrcRect.top; srcY < clippedSrcRect.bottom; srcY++) {
 				int32 destY = srcY + srcToDestY;
-				int32 spanWidth = clippedDrawRect.getWidth();
+				int32 spanWidth = clippedDrawRect.width();
 				void *destPixels = window->getSurface()->getBasePtr(clippedDrawRect.left, srcY + srcToDestY);
 				if (_mask) {
 					const uint8 *maskBytes = static_cast<const uint8 *>(_mask->getBasePtr(clippedSrcRect.left, srcY));
@@ -491,7 +505,7 @@ void GraphicElement::render(Window *window) {
 
 			for (int32 srcY = clippedSrcRect.top; srcY < clippedSrcRect.bottom; srcY++) {
 				int32 destY = srcY + srcToDestY;
-				int32 spanWidth = clippedDrawRect.getWidth();
+				int32 spanWidth = clippedDrawRect.width();
 				void *destPixels = window->getSurface()->getBasePtr(clippedDrawRect.left, srcY + srcToDestY);
 				if (_mask) {
 					const uint8 *maskBytes = static_cast<const uint8 *>(_mask->getBasePtr(clippedSrcRect.left, srcY));
@@ -694,7 +708,7 @@ void MovieElement::render(Window *window) {
 	if (_displayFrame) {
 		Graphics::ManagedSurface *target = window->getSurface().get();
 		Common::Rect srcRect(0, 0, _displayFrame->w, _displayFrame->h);
-		Common::Rect destRect(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y, _cachedAbsoluteOrigin.x + _rect.getWidth(), _cachedAbsoluteOrigin.y + _rect.getHeight());
+		Common::Rect destRect(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y, _cachedAbsoluteOrigin.x + _rect.width(), _cachedAbsoluteOrigin.y + _rect.height());
 		target->blitFrom(*_displayFrame, srcRect, destRect);
 	}
 }
@@ -1053,7 +1067,7 @@ void ImageElement::render(Window *window) {
 	if (_cachedImage) {
 		Common::SharedPtr<Graphics::Surface> optimized = _cachedImage->optimize(_runtime);
 		Common::Rect srcRect(optimized->w, optimized->h);
-		Common::Rect destRect(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y, _cachedAbsoluteOrigin.x + _rect.getWidth(), _cachedAbsoluteOrigin.y + _rect.getHeight());
+		Common::Rect destRect(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y, _cachedAbsoluteOrigin.x + _rect.width(), _cachedAbsoluteOrigin.y + _rect.height());
 
 		VisualElementRenderProperties::InkMode inkMode = _renderProps.getInkMode();
 
@@ -1210,15 +1224,15 @@ void MToonElement::render(Window *window) {
 
 		_renderedFrame = frame;
 
-		Rect16 frameRect = _metadata->frames[frame].rect;
+		Common::Rect frameRect = _metadata->frames[frame].rect;
 
 		if (_renderSurface) {
 			Common::Rect srcRect;
 			Common::Rect destRect;
 
-			if (frameRect.getWidth() == _renderSurface->w && frameRect.getHeight() == _renderSurface->h) {
+			if (frameRect.width() == _renderSurface->w && frameRect.height() == _renderSurface->h) {
 				// Frame rect is the size of the render surface, meaning the frame rect is an offset
-				srcRect = Common::Rect(0, 0, frameRect.getWidth(), frameRect.getHeight());
+				srcRect = Common::Rect(0, 0, frameRect.width(), frameRect.height());
 			} else {
 				// Frame rect is a sub-area of the rendered rect
 				srcRect = Common::Rect(frameRect.left, frameRect.top, frameRect.right, frameRect.bottom);
@@ -1597,8 +1611,8 @@ void TextLabelElement::render(Window *window) {
 	if (!_visible)
 		return;
 
-	int renderWidth = _rect.getWidth();
-	int renderHeight = _rect.getHeight();
+	int renderWidth = _rect.width();
+	int renderHeight = _rect.height();
 	if (_renderedText) {
 		if (renderWidth != _renderedText->w || renderHeight != _renderedText->h)
 			_needsRender = true;
@@ -1729,7 +1743,7 @@ void TextLabelElement::render(Window *window) {
 
 	Graphics::ManagedSurface *target = window->getSurface().get();
 	Common::Rect srcRect(0, 0, renderWidth, renderHeight);
-	Common::Rect destRect(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y, _cachedAbsoluteOrigin.x + _rect.getWidth(), _cachedAbsoluteOrigin.y + _rect.getHeight());
+	Common::Rect destRect(_cachedAbsoluteOrigin.x, _cachedAbsoluteOrigin.y, _cachedAbsoluteOrigin.x + _rect.width(), _cachedAbsoluteOrigin.y + _rect.height());
 
 	const uint32 opaqueColor = 0xff000000;
 	const uint32 drawPalette[2] = {0, opaqueColor};
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index b538dbfd1a2..d0a27b3e88e 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -644,7 +644,8 @@ bool DragMotionModifier::load(ModifierLoaderContext &context, const Data::DragMo
 
 	_dragProps.reset(new DragMotionProperties());
 
-	if (!_enableWhen.load(data.enableWhen) || !_disableWhen.load(data.disableWhen) || !_dragProps->constraintMargin.loadUnchecked(data.constraintMargin))
+	// constraint margin is unchecked here because it's a margin, not a real rectangle, but it's stored as if it's a rect
+	if (!_enableWhen.load(data.enableWhen) || !_disableWhen.load(data.disableWhen) || !data.constraintMargin.toScummVMRectUnchecked(_dragProps->constraintMargin))
 		return false;
 
 	bool constrainVertical = false;
diff --git a/engines/mtropolis/plugin/obsidian.cpp b/engines/mtropolis/plugin/obsidian.cpp
index a3a0824addf..48472fb318c 100644
--- a/engines/mtropolis/plugin/obsidian.cpp
+++ b/engines/mtropolis/plugin/obsidian.cpp
@@ -661,17 +661,18 @@ MiniscriptInstructionOutcome XorCheckModifier::scriptSetCheckNow(MiniscriptThrea
 	Common::Array<VisualElement *> xorElements;
 	recursiveFindXorElements(scene, xorElements);
 
-	Rect16 triRects[4];
+	Common::Rect triRects[4];
 	for (int i = 0; i < 4; i++)
-		triRects[i] = Rect16::create(0, 0, 0, 0);
+		triRects[i] = Common::Rect(0, 0, 0, 0);
 
-	Common::Array<Rect16> pendingRects;
+	Common::Array<Common::Rect> pendingRects;
 
 	for (VisualElement *element : xorElements) {
 		VisualElementRenderProperties::Shape shape = element->getRenderProperties().getShape();
-		Rect16 rect = element->getRelativeRect();
+		Common::Rect rect = element->getRelativeRect();
 		Point16 absOrigin = element->getCachedAbsoluteOrigin();
-		Rect16 absRect = rect.translate(absOrigin.x - rect.left, absOrigin.y - rect.top);
+		Common::Rect absRect = rect;
+		absRect.translate(absOrigin.x - rect.left, absOrigin.y - rect.top);
 
 		if (shape >= VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri1 && shape <= VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri4)
 			triRects[shape - VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri1] = absRect;
@@ -695,15 +696,15 @@ MiniscriptInstructionOutcome XorCheckModifier::scriptSetCheckNow(MiniscriptThrea
 		return kMiniscriptInstructionOutcomeContinue;
 	}
 
-	Common::Array<Rect16> maskedRects;
+	Common::Array<Common::Rect> maskedRects;
 	while (pendingRects.size() > 0) {
-		const Rect16 pendingRect = pendingRects.back();
+		const Common::Rect pendingRect = pendingRects.back();
 		pendingRects.pop_back();
 
 		bool hasIntersection = false;
 		size_t intersectionIndex = 0;
 		for (size_t j = 0; j < maskedRects.size(); j++) {
-			if (maskedRects[j].intersect(pendingRect).isValid()) {
+			if (maskedRects[j].intersects(pendingRect)) {
 				hasIntersection = true;
 				intersectionIndex = j;
 			}
@@ -720,7 +721,7 @@ MiniscriptInstructionOutcome XorCheckModifier::scriptSetCheckNow(MiniscriptThrea
 			continue;
 		}
 
-		const Rect16 intersectingRect = maskedRects[intersectionIndex];
+		const Common::Rect intersectingRect = maskedRects[intersectionIndex];
 
 		// Try to subdivide the intersecting rect using one of the axes of the incoming rect.
 		// If this succeeds, requeue the intersecting rect fragments and add the pending rect
@@ -768,10 +769,10 @@ void XorCheckModifier::recursiveFindXorElements(Structural *structural, Common::
 		elements.push_back(visual);
 }
 
-bool XorCheckModifier::sliceRectX(const Rect16 &rect, int32 x, Common::Array<Rect16> &outSlices) {
+bool XorCheckModifier::sliceRectX(const Common::Rect &rect, int32 x, Common::Array<Common::Rect> &outSlices) {
 	if (x > rect.left && x < rect.right) {
-		Rect16 leftSlice = Rect16::create(rect.left, rect.top, x, rect.bottom);
-		Rect16 rightSlice = Rect16::create(x, rect.top, rect.right, rect.bottom);
+		Common::Rect leftSlice = Common::Rect(rect.left, rect.top, x, rect.bottom);
+		Common::Rect rightSlice = Common::Rect(x, rect.top, rect.right, rect.bottom);
 		outSlices.push_back(leftSlice);
 		outSlices.push_back(rightSlice);
 		return true;
@@ -780,10 +781,10 @@ bool XorCheckModifier::sliceRectX(const Rect16 &rect, int32 x, Common::Array<Rec
 	return false;
 }
 
-bool XorCheckModifier::sliceRectY(const Rect16 &rect, int32 y, Common::Array<Rect16> &outSlices) {
+bool XorCheckModifier::sliceRectY(const Common::Rect &rect, int32 y, Common::Array<Common::Rect> &outSlices) {
 	if (y > rect.top && y < rect.bottom) {
-		Rect16 topSlice = Rect16::create(rect.left, rect.top, rect.right, y);
-		Rect16 bottomSlice = Rect16::create(rect.left, y, rect.right, rect.bottom);
+		Common::Rect topSlice = Common::Rect(rect.left, rect.top, rect.right, y);
+		Common::Rect bottomSlice = Common::Rect(rect.left, y, rect.right, rect.bottom);
 		outSlices.push_back(topSlice);
 		outSlices.push_back(bottomSlice);
 		return true;
diff --git a/engines/mtropolis/plugin/obsidian.h b/engines/mtropolis/plugin/obsidian.h
index 2682d5f1a97..dfd5e53a4c1 100644
--- a/engines/mtropolis/plugin/obsidian.h
+++ b/engines/mtropolis/plugin/obsidian.h
@@ -199,8 +199,8 @@ private:
 	MiniscriptInstructionOutcome scriptSetCheckNow(MiniscriptThread *thread, const DynamicValue &value);
 
 	static void recursiveFindXorElements(Structural *structural, Common::Array<VisualElement *> &elements);
-	static bool sliceRectX(const Rect16 &rect, int32 x, Common::Array<Rect16> &outSlices);
-	static bool sliceRectY(const Rect16 &rect, int32 y, Common::Array<Rect16> &outSlices);
+	static bool sliceRectX(const Common::Rect &rect, int32 x, Common::Array<Common::Rect> &outSlices);
+	static bool sliceRectY(const Common::Rect &rect, int32 y, Common::Array<Common::Rect> &outSlices);
 
 	bool _allClear;
 };
diff --git a/engines/mtropolis/render.cpp b/engines/mtropolis/render.cpp
index 7e78aa764f4..467b88f9b6a 100644
--- a/engines/mtropolis/render.cpp
+++ b/engines/mtropolis/render.cpp
@@ -212,7 +212,7 @@ static void recursiveCollectDrawElementsAndUpdateOrigins(const Point16 &parentOr
 		Element *element = static_cast<Element *>(structural);
 		if (element->isVisual()) {
 			VisualElement *visualElement = static_cast<VisualElement *>(element);
-			const Rect16 &elementRect = visualElement->getRelativeRect();
+			const Common::Rect &elementRect = visualElement->getRelativeRect();
 
 			elementOrigin.x += elementRect.left;
 			elementOrigin.y += elementRect.top;
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 331781a2742..5af372bf438 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -294,31 +294,6 @@ Common::String Point16::toString() const {
 	return Common::String::format("(%i,%i)", x, y);
 }
 
-bool Rect16::load(const Data::Rect &rect) {
-	top = rect.top;
-	left = rect.left;
-	bottom = rect.bottom;
-	right = rect.right;
-
-	if (bottom < top || right < left)
-		return false;
-
-	return true;
-}
-
-bool Rect16::loadUnchecked(const Data::Rect &rect) {
-	top = rect.top;
-	left = rect.left;
-	bottom = rect.bottom;
-	right = rect.right;
-
-	return true;
-}
-
-Common::Rect Rect16::toScummvmRect() const {
-	return Common::Rect(left, top, right, bottom);
-}
-
 bool IntRange::load(const Data::IntRange &range) {
 	max = range.max;
 	min = range.min;
@@ -4661,7 +4636,7 @@ VThreadState Runtime::updateMouseStateTask(const UpdateMouseStateTaskData &data)
 			_mouseTrackingObject = tracked->getSelfReference().staticCast<Structural>();
 			_mouseTrackingDragStart = _cachedMousePosition;
 			if (tracked->isElement() && static_cast<Element *>(tracked)->isVisual()) {
-				Rect16 initialRect = static_cast<VisualElement *>(tracked)->getRelativeRect();
+				Common::Rect initialRect = static_cast<VisualElement *>(tracked)->getRelativeRect();
 				_mouseTrackingObjectInitialOrigin = Point16::create(initialRect.left, initialRect.top);
 			} else
 				_mouseTrackingObjectInitialOrigin = Point16::create(0, 0);
@@ -5107,7 +5082,7 @@ void Runtime::checkCollisions() {
 		if (visual->isVisible()) {
 			bool foundSelf = false;
 			size_t selfIndex = 0;
-			Rect16 selfRect = Rect16::create(0, 0, 0, 0);
+			Common::Rect selfRect = Common::Rect(0, 0, 0, 0);
 
 			for (size_t i = 0; i < collisionObjects.size(); i++) {
 				if (collisionObjects[i].element == visual) {
@@ -5137,7 +5112,7 @@ void Runtime::checkCollisions() {
 					assert(collisionObjectElement != visual);
 
 					// Potential collision
-					if (!collisionObject.absRect.intersect(selfRect).isValid())
+					if (!collisionObject.absRect.intersects(selfRect))
 						continue;
 
 					if (excludeParents) {
@@ -5209,7 +5184,7 @@ void Runtime::recursiveFindColliders(Structural *structural, size_t sceneStackDe
 		Element *element = static_cast<Element *>(structural);
 		if (element->isVisual()) {
 			VisualElement *visual = static_cast<VisualElement *>(element);
-			const Rect16 &rect = visual->getRelativeRect();
+			const Common::Rect &rect = visual->getRelativeRect();
 
 			childOffsetX += rect.left;
 			childOffsetY += rect.top;
@@ -5217,7 +5192,8 @@ void Runtime::recursiveFindColliders(Structural *structural, size_t sceneStackDe
 			// isRoot = Is a scene, and colliding with scenes is not allowed
 			if (!isRoot && visual->isVisible()) {
 				ColliderInfo colliderInfo;
-				colliderInfo.absRect = rect.translate(parentOriginX, parentOriginY);
+				colliderInfo.absRect = rect;
+				colliderInfo.absRect.translate(parentOriginX, parentOriginY);
 				colliderInfo.element = visual;
 				colliderInfo.layer = visual->getLayer();
 				colliderInfo.sceneStackDepth = sceneStackDepth;
@@ -6622,7 +6598,7 @@ VisualElementRenderProperties &VisualElementRenderProperties::operator=(const Vi
 }
 
 VisualElement::VisualElement()
-	: _rect(Rect16::create(0, 0, 0, 0)), _cachedAbsoluteOrigin(Point16::create(0, 0))
+	: _rect(0, 0, 0, 0), _cachedAbsoluteOrigin(Point16::create(0, 0))
 	, _contentsDirty(true) {
 }
 
@@ -6741,7 +6717,7 @@ MiniscriptInstructionOutcome VisualElement::writeRefAttribute(MiniscriptThread *
 	return Element::writeRefAttribute(thread, writeProxy, attrib);
 }
 
-const Rect16 &VisualElement::getRelativeRect() const {
+const Common::Rect &VisualElement::getRelativeRect() const {
 	return _rect;
 }
 
@@ -6795,15 +6771,15 @@ void VisualElement::handleDragMotion(Runtime *runtime, const Point16 &initialPoi
 		targetPoint.x = initialPoint.x;
 
 	if (_dragProps->constrainToParent && _parent && _parent->isElement() && static_cast<Element *>(_parent)->isVisual()) {
-		Rect16 constrainInset = _dragProps->constraintMargin;
+		Common::Rect constrainInset = _dragProps->constraintMargin;
 
-		Rect16 parentRect = static_cast<VisualElement *>(_parent)->getRelativeRect();
+		Common::Rect parentRect = static_cast<VisualElement *>(_parent)->getRelativeRect();
 
 		// rect.width - inset.right
 		int32 minX = constrainInset.left;
 		int32 minY = constrainInset.top;
-		int32 maxX = parentRect.getWidth() - constrainInset.right - _rect.getWidth();
-		int32 maxY = parentRect.getHeight() - constrainInset.bottom - _rect.getHeight();
+		int32 maxX = parentRect.width() - constrainInset.right - _rect.width();
+		int32 maxY = parentRect.height() - constrainInset.bottom - _rect.height();
 
 		// TODO: Handle "squished" case where max < min, it does work but it's weird
 		if (targetPoint.x < minX)
@@ -6872,7 +6848,7 @@ MiniscriptInstructionOutcome VisualElement::scriptSetVisibility(MiniscriptThread
 }
 
 bool VisualElement::loadCommon(const Common::String &name, uint32 guid, const Data::Rect &rect, uint32 elementFlags, uint16 layer, uint32 streamLocator, uint16 sectionID) {
-	if (!_rect.load(rect))
+	if (!rect.toScummVMRect(_rect))
 		return false;
 
 	_name = name;
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 4af476f372a..ede6b1bebb5 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -310,65 +310,6 @@ struct Point16 {
 	Common::String toString() const;
 };
 
-struct Rect16 {
-	int16 top;
-	int16 left;
-	int16 bottom;
-	int16 right;
-
-	bool load(const Data::Rect &rect);
-	bool loadUnchecked(const Data::Rect &rect);
-
-	inline bool operator==(const Rect16 &other) const {
-		return top == other.top && left == other.left && bottom == other.bottom && right == other.right;
-	}
-
-	inline bool operator!=(const Rect16 &other) const {
-		return !((*this) == other);
-	}
-
-	inline uint16 getWidth() const { return static_cast<uint16>(right - left); }
-	inline uint16 getHeight() const { return static_cast<uint16>(bottom - top); }
-
-	inline static Rect16 create(int16 left, int16 top, int16 right, int16 bottom) {
-		Rect16 result;
-		result.left = left;
-		result.top = top;
-		result.right = right;
-		result.bottom = bottom;
-		return result;
-	}
-
-	inline bool isValid() const {
-		return right > left && bottom > top;
-	}
-
-	inline Rect16 intersect(const Rect16 &other) const {
-		Rect16 result = *this;
-		if (result.left < other.left)
-			result.left = other.left;
-		if (result.top < other.top)
-			result.top = other.top;
-		if (result.right > other.right)
-			result.right = other.right;
-		if (result.bottom > other.bottom)
-			result.bottom = other.bottom;
-
-		return result;
-	}
-
-	inline Rect16 translate(int32 dx, int32 dy) const {
-		Rect16 result = *this;
-		result.left += dx;
-		result.right += dx;
-		result.top += dy;
-		result.bottom += dy;
-		return result;
-	}
-
-	Common::Rect toScummvmRect() const;
-};
-
 struct IntRange {
 	int32 min;
 	int32 max;
@@ -1500,7 +1441,7 @@ private:
 
 struct DragMotionProperties {
 	ConstraintDirection constraintDirection;
-	Rect16 constraintMargin;
+	Common::Rect constraintMargin;
 	bool constrainToParent;
 };
 
@@ -1678,7 +1619,7 @@ private:
 		size_t sceneStackDepth;
 		uint16 layer;
 		VisualElement *element;
-		Rect16 absRect;
+		Common::Rect absRect;
 	};
 
 	static Common::SharedPtr<Structural> findDefaultSharedSceneForScene(Structural *scene);
@@ -2461,7 +2402,7 @@ public:
 
 	Point16 getParentOrigin() const;
 	Point16 getGlobalPosition() const;
-	const Rect16 &getRelativeRect() const;
+	const Common::Rect &getRelativeRect() const;
 
 	// The cached absolute origin is from the last time the element was rendered.
 	// Do not rely on it mid-frame.
@@ -2524,14 +2465,14 @@ protected:
 
 	bool _directToScreen;
 	bool _visible;
-	Rect16 _rect;
+	Common::Rect _rect;
 	Point16 _cachedAbsoluteOrigin;
 	uint16 _layer;
 
 	Common::SharedPtr<DragMotionProperties> _dragProps;
 
 	VisualElementRenderProperties _renderProps;
-	Rect16 _prevRect;
+	Common::Rect _prevRect;
 	bool _contentsDirty;
 };
 


Commit: 7ed8358691b93b47156d8b5ea49e77337ab00ceb
    https://github.com/scummvm/scummvm/commit/7ed8358691b93b47156d8b5ea49e77337ab00ceb
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Change Point16 to Common::Point

Changed paths:
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/elements.cpp
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/plugin/obsidian.cpp
    engines/mtropolis/plugin/obsidian.h
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/render.cpp
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h
    engines/mtropolis/vthread.h


diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index e6dcd7f5229..709c4f335c3 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -325,6 +325,11 @@ bool Point::load(DataReader &reader) {
 		return false;
 }
 
+bool Point::toScummVMPoint(Common::Point &outPoint) const {
+	outPoint = Common::Point(x, y);
+	return true;
+}
+
 bool Event::load(DataReader& reader) {
 	return reader.readU32(eventID) && reader.readU32(eventInfo);
 }
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index 3e110b8607b..353c9fb4ffb 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -223,6 +223,7 @@ struct Rect {
 
 struct Point {
 	bool load(DataReader &reader);
+	bool toScummVMPoint(Common::Point &outPoint) const;
 
 	int16 x;
 	int16 y;
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index bcf5b9a7019..7ae61b86911 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -224,22 +224,22 @@ void GraphicElement::render(Window *window) {
 	}
 
 	if (needsMaskRedraw) {
-		Common::Array<Point16> starPoints;
-		const Common::Array<Point16> *polyPoints = nullptr;
+		Common::Array<Common::Point> starPoints;
+		const Common::Array<Common::Point> *polyPoints = nullptr;
 
 		VisualElementRenderProperties::Shape shape = _renderProps.getShape();
 		if (shape == VisualElementRenderProperties::kShapeStar) {
 			starPoints.resize(10);
-			starPoints[0] = Point16::create(width / 2, 0);
-			starPoints[1] = Point16::create(width * 2 / 3, height / 3);
-			starPoints[2] = Point16::create(width, height / 3);
-			starPoints[3] = Point16::create(width * 3 / 4, height / 2);
-			starPoints[4] = Point16::create(width, height);
-			starPoints[5] = Point16::create(width / 2, height * 2 / 3);
-			starPoints[6] = Point16::create(0, height);
-			starPoints[7] = Point16::create(width / 4, height / 2);
-			starPoints[8] = Point16::create(0, height / 3);
-			starPoints[9] = Point16::create(width / 3, height / 3);
+			starPoints[0] = Common::Point(width / 2, 0);
+			starPoints[1] = Common::Point(width * 2 / 3, height / 3);
+			starPoints[2] = Common::Point(width, height / 3);
+			starPoints[3] = Common::Point(width * 3 / 4, height / 2);
+			starPoints[4] = Common::Point(width, height);
+			starPoints[5] = Common::Point(width / 2, height * 2 / 3);
+			starPoints[6] = Common::Point(0, height);
+			starPoints[7] = Common::Point(width / 4, height / 2);
+			starPoints[8] = Common::Point(0, height / 3);
+			starPoints[9] = Common::Point(width / 3, height / 3);
 			polyPoints = &starPoints;
 
 			shape = VisualElementRenderProperties::kShapePolygon;
@@ -266,25 +266,25 @@ void GraphicElement::render(Window *window) {
 		if (shape == VisualElementRenderProperties::kShapePolygon && polyPoints->size() >= 3) {
 			_mask->clear(0);
 
-			Point16 firstPoint = (*polyPoints)[0];
+			Common::Point firstPoint = (*polyPoints)[0];
 			for (uint polyStart = 1; polyStart < polyPoints->size() - 1; polyStart++) {
-				Point16 points[3];
+				Common::Point points[3];
 				points[0] = firstPoint;
 				points[1] = (*polyPoints)[polyStart];
 				points[2] = (*polyPoints)[polyStart + 1];
 
 				// Sort poly points into height ascending order
 				for (int sortStart = 0; sortStart < 2; sortStart++) {
-					Point16 *thisPoint = &points[sortStart];
-					Point16 *lowestY = thisPoint;
+					Common::Point *thisPoint = &points[sortStart];
+					Common::Point *lowestY = thisPoint;
 					for (int candidateIndex = sortStart + 1; candidateIndex < 3; candidateIndex++) {
-						Point16 *candidate = &points[candidateIndex];
+						Common::Point *candidate = &points[candidateIndex];
 						if (candidate->y < lowestY->y)
 							lowestY = candidate;
 					}
 
 					if (lowestY != thisPoint) {
-						Point16 temp = *thisPoint;
+						Common::Point temp = *thisPoint;
 						*thisPoint = *lowestY;
 						*lowestY = temp;
 					}
@@ -294,22 +294,22 @@ void GraphicElement::render(Window *window) {
 					continue; // Degenerate triangle
 
 				// Bin into 2 sets
-				Point16 *triPoints[2][3] = {{&points[0], &points[1], &points[2]},
+				Common::Point *triPoints[2][3] = {{&points[0], &points[1], &points[2]},
 											{&points[2], &points[1], &points[0]}};
 
 				int32 yRanges[2][2] = {{points[0].y, points[1].y},
 									   {points[1].y, points[2].y}};
 
 				for (int half = 0; half < 2; half++) {
-					Point16 *commonPoint = triPoints[half][0];
-					Point16 *leftVert = triPoints[half][1];
-					Point16 *rightVert = triPoints[half][2];
+					Common::Point *commonPoint = triPoints[half][0];
+					Common::Point *leftVert = triPoints[half][1];
+					Common::Point *rightVert = triPoints[half][2];
 
 					if (leftVert->x == rightVert->x || commonPoint->y == points[1].y)
 						continue; // Degenerate tri
 
 					if (leftVert->x > rightVert->x) {
-						Point16 *temp = leftVert;
+						Common::Point *temp = leftVert;
 						leftVert = rightVert;
 						rightVert = temp;
 					}
@@ -1328,7 +1328,6 @@ void MToonElement::playMedia(Runtime *runtime, Project *project) {
 		} else
 			targetCel = isReversed ? (_cel - framesAdvanced) : (_cel + framesAdvanced);
 
-		int32 playControlTargetCel = targetCel;
 		if (targetCel < 1)
 			targetCel = 1;
 		if (targetCel > sanitizeMaxCel)
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 6cfd9e84347..1d3703b4685 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -599,8 +599,8 @@ MiniscriptInstructionOutcome BinaryArithInstruction::execute(MiniscriptThread *t
 	DynamicValue &lsDest = thread->getStackValueFromTop(1).value;
 
 	if (lsDest.getType() == DynamicValueTypes::kPoint && rs.getType() == DynamicValueTypes::kPoint) {
-		Point16 lsPoint = lsDest.getPoint();
-		Point16 rsPoint = rs.getPoint();
+		Common::Point lsPoint = lsDest.getPoint().toScummVMPoint();
+		Common::Point rsPoint = rs.getPoint().toScummVMPoint();
 
 		double resultX = 0.0;
 		double resultY = 0.0;
@@ -612,7 +612,7 @@ MiniscriptInstructionOutcome BinaryArithInstruction::execute(MiniscriptThread *t
 		if (outcome != kMiniscriptInstructionOutcomeContinue)
 			return outcome;
 
-		lsDest.setPoint(Point16::create(static_cast<int16>(round(resultX)), static_cast<int16>(round(resultY))));
+		lsDest.setPoint(Common::Point(static_cast<int16>(round(resultX)), static_cast<int16>(round(resultY))));
 	} else {
 		double leftVal = 0.0;
 		switch (lsDest.getType()) {
@@ -1122,7 +1122,7 @@ MiniscriptInstructionOutcome BuiltinFunc::executeRectToPolar(MiniscriptThread *t
 		return kMiniscriptInstructionOutcomeFailed;
 	}
 
-	const Point16 &pt = inputDynamicValue.getPoint();
+	const Point16POD &pt = inputDynamicValue.getPoint();
 
 	double angle = atan2(pt.x, pt.y);
 	double magnitude = sqrt(pt.x * pt.x + pt.y * pt.y);
@@ -1145,7 +1145,7 @@ MiniscriptInstructionOutcome BuiltinFunc::executePolarToRect(MiniscriptThread *t
 	double x = cos(vec.angleDegrees * (M_PI / 180.0)) * vec.magnitude;
 	double y = sin(vec.angleDegrees * (M_PI / 180.0)) * vec.magnitude;
 
-	returnValue->setPoint(Point16::create(static_cast<int16>(round(x)), static_cast<int16>(round(y))));
+	returnValue->setPoint(Common::Point(static_cast<int16>(round(x)), static_cast<int16>(round(y))));
 
 	return kMiniscriptInstructionOutcomeContinue;
 }
@@ -1277,7 +1277,7 @@ MiniscriptInstructionOutcome PointCreate::execute(MiniscriptThread *thread) cons
 		}
 	}
 
-	xValDest.setPoint(Point16::create(coords[0], coords[1]));
+	xValDest.setPoint(Common::Point(coords[0], coords[1]));
 
 	thread->popValues(1);
 
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index d0a27b3e88e..418e3b24025 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -595,7 +595,7 @@ bool PathMotionModifierV2::load(ModifierLoaderContext &context, const Data::Path
 
 		outPoint.frame = inPoint.frame;
 		outPoint.useFrame = ((inPoint.frameFlags & Data::PathMotionModifierV2::PointDef::kFrameFlagPlaySequentially) != 0);
-		if (!outPoint.point.load(inPoint.point) || !outPoint.sendSpec.load(inPoint.send, inPoint.messageFlags, inPoint.with, inPoint.withSource, inPoint.withString, inPoint.destination))
+		if (!inPoint.point.toScummVMPoint(outPoint.point) || !outPoint.sendSpec.load(inPoint.send, inPoint.messageFlags, inPoint.with, inPoint.withSource, inPoint.withString, inPoint.destination))
 			return false;
 	}
 
@@ -1577,7 +1577,7 @@ bool GraphicModifier::load(ModifierLoaderContext &context, const Data::GraphicMo
 		return false;
 
 	// We need the poly points even if this isn't a poly shape since I think it's possible to change the shape type at runtime
-	Common::Array<Point16> &polyPoints = _renderProps.modifyPolyPoints();
+	Common::Array<Common::Point> &polyPoints = _renderProps.modifyPolyPoints();
 	polyPoints.resize(data.polyPoints.size());
 	for (size_t i = 0; i < data.polyPoints.size(); i++) {
 		polyPoints[i].x = data.polyPoints[i].x;
@@ -2106,7 +2106,7 @@ Common::SharedPtr<ModifierSaveLoad> PointVariableModifier::getSaveLoad() {
 
 bool PointVariableModifier::varSetValue(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kPoint)
-		_value = value.getPoint();
+		_value = value.getPoint().toScummVMPoint();
 	else
 		return false;
 
@@ -2147,7 +2147,7 @@ MiniscriptInstructionOutcome PointVariableModifier::writeRefAttribute(Miniscript
 void PointVariableModifier::debugInspect(IDebugInspectionReport *report) const {
 	VariableModifier::debugInspect(report);
 
-	report->declareDynamic("value", _value.toString());
+	report->declareDynamic("value", pointToString(_value));
 }
 #endif
 
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 950c9c11269..6182e0cf8b3 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -262,7 +262,7 @@ public:
 
 private:
 	struct PointDef {
-		Point16 point;
+		Common::Point point;
 		uint32 frame;
 		bool useFrame;
 
@@ -896,13 +896,13 @@ private:
 		bool loadInternal(Common::ReadStream *stream) override;
 
 		PointVariableModifier *_modifier;
-		Point16 _value;
+		Common::Point _value;
 	};
 
 	Common::SharedPtr<Modifier> shallowClone() const override;
 	const char *getDefaultName() const override;
 
-	Point16 _value;
+	Common::Point _value;
 };
 
 class FloatingPointVariableModifier : public VariableModifier {
diff --git a/engines/mtropolis/plugin/obsidian.cpp b/engines/mtropolis/plugin/obsidian.cpp
index 48472fb318c..c20a759eb4a 100644
--- a/engines/mtropolis/plugin/obsidian.cpp
+++ b/engines/mtropolis/plugin/obsidian.cpp
@@ -33,7 +33,7 @@ bool MovementModifier::load(const PlugInModifierLoaderContext &context, const Da
 	_rate = 0;
 	_frequency = 0;
 	_type = false;
-	_dest = Point16::create(0, 0);
+	_dest = Common::Point(0, 0);
 
 	return true;
 }
@@ -670,7 +670,7 @@ MiniscriptInstructionOutcome XorCheckModifier::scriptSetCheckNow(MiniscriptThrea
 	for (VisualElement *element : xorElements) {
 		VisualElementRenderProperties::Shape shape = element->getRenderProperties().getShape();
 		Common::Rect rect = element->getRelativeRect();
-		Point16 absOrigin = element->getCachedAbsoluteOrigin();
+		Common::Point absOrigin = element->getCachedAbsoluteOrigin();
 		Common::Rect absRect = rect;
 		absRect.translate(absOrigin.x - rect.left, absOrigin.y - rect.top);
 
diff --git a/engines/mtropolis/plugin/obsidian.h b/engines/mtropolis/plugin/obsidian.h
index dfd5e53a4c1..5b84e402680 100644
--- a/engines/mtropolis/plugin/obsidian.h
+++ b/engines/mtropolis/plugin/obsidian.h
@@ -48,7 +48,7 @@ private:
 	Common::SharedPtr<Modifier> shallowClone() const override;
 	const char *getDefaultName() const override;
 
-	Point16 _dest;
+	Common::Point _dest;
 	bool _type;
 	int32 _rate;
 	int32 _frequency;
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index e88c3d7ceaa..0f7c1ffc5ae 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -1347,7 +1347,7 @@ void ListVariableModifier::debugInspect(IDebugInspectionReport *report) const {
 			report->declareLoose(Common::String::format("[%i] = %g", cardinal, _list->getFloat()[i]));
 			break;
 		case DynamicValueTypes::kPoint:
-			report->declareLoose(Common::String::format("[%i] = ", cardinal) + _list->getPoint()[i].toString());
+			report->declareLoose(Common::String::format("[%i] = ", cardinal) + pointToString(_list->getPoint()[i]));
 			break;
 		case DynamicValueTypes::kIntegerRange:
 			report->declareLoose(Common::String::format("[%i] = ", cardinal) + _list->getIntRange()[i].toString());
@@ -1459,7 +1459,7 @@ void ListVariableModifier::SaveLoad::recursiveWriteList(DynamicList *list, Commo
 			stream->writeSint32BE(list->getInt()[i]);
 			break;
 		case DynamicValueTypes::kPoint: {
-				const Point16 &pt = list->getPoint()[i];
+				const Common::Point &pt = list->getPoint()[i];
 				stream->writeSint16BE(pt.x);
 				stream->writeSint16BE(pt.y);
 			}
@@ -1514,7 +1514,7 @@ Common::SharedPtr<DynamicList> ListVariableModifier::SaveLoad::recursiveReadList
 				val.setInt(i32);
 			} break;
 		case DynamicValueTypes::kPoint: {
-				Point16 pt;
+				Common::Point pt;
 				pt.x = stream->readSint16BE();
 				pt.y = stream->readSint16BE();
 				val.setPoint(pt);
@@ -1608,7 +1608,7 @@ bool SysInfoModifier::readAttribute(MiniscriptThread *thread, DynamicValue &resu
 	} else if (attrib == "screensize") {
 		uint16 width, height;
 		thread->getRuntime()->getDisplayResolution(width, height);
-		result.setPoint(Point16::create(width, height));
+		result.setPoint(Common::Point(width, height));
 		return true;
 	}
 
diff --git a/engines/mtropolis/render.cpp b/engines/mtropolis/render.cpp
index 467b88f9b6a..94d6de26aa5 100644
--- a/engines/mtropolis/render.cpp
+++ b/engines/mtropolis/render.cpp
@@ -206,8 +206,8 @@ uint32 resolveRGB(uint8 r, uint8 g, uint8 b, const Graphics::PixelFormat &fmt) {
 	return rPlaced | gPlaced | bPlaced | aPlaced;
 }
 
-static void recursiveCollectDrawElementsAndUpdateOrigins(const Point16 &parentOrigin, Structural *structural, size_t sceneStackDepth, Common::Array<RenderItem> &normalBucket, Common::Array<RenderItem> &directBucket) {
-	Point16 elementOrigin = parentOrigin;
+static void recursiveCollectDrawElementsAndUpdateOrigins(const Common::Point &parentOrigin, Structural *structural, size_t sceneStackDepth, Common::Array<RenderItem> &normalBucket, Common::Array<RenderItem> &directBucket) {
+	Common::Point elementOrigin = parentOrigin;
 	if (structural->isElement()) {
 		Element *element = static_cast<Element *>(structural);
 		if (element->isVisual()) {
@@ -217,7 +217,7 @@ static void recursiveCollectDrawElementsAndUpdateOrigins(const Point16 &parentOr
 			elementOrigin.x += elementRect.left;
 			elementOrigin.y += elementRect.top;
 
-			visualElement->setCachedAbsoluteOrigin(Point16::create(elementOrigin.x, elementOrigin.y));
+			visualElement->setCachedAbsoluteOrigin(Common::Point(elementOrigin.x, elementOrigin.y));
 
 			RenderItem item;
 			item.element = visualElement;
@@ -267,7 +267,7 @@ void renderProject(Runtime *runtime, Window *mainWindow) {
 
 	size_t sceneStackDepth = 0;
 	for (Common::Array<Structural *>::const_iterator it = scenes.begin(), itEnd = scenes.end(); it != itEnd; ++it) {
-		recursiveCollectDrawElementsAndUpdateOrigins(Point16::create(0, 0), *it, sceneStackDepth, normalBucket, directBucket);
+		recursiveCollectDrawElementsAndUpdateOrigins(Common::Point(0, 0), *it, sceneStackDepth, normalBucket, directBucket);
 		sceneStackDepth++;
 	}
 
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 5af372bf438..586399f7665 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -270,28 +270,21 @@ bool EventIDs::isCommand(EventID eventID) {
 }
 
 
-bool Point16::load(const Data::Point &point) {
-	x = point.x;
-	y = point.y;
-
-	return true;
-}
-
-MiniscriptInstructionOutcome Point16::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib) {
+MiniscriptInstructionOutcome pointWriteRefAttrib(Common::Point &point, MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib) {
 	if (attrib == "x") {
-		DynamicValueWriteIntegerHelper<int16>::create(&x, proxy);
+		DynamicValueWriteIntegerHelper<int16>::create(&point.x, proxy);
 		return kMiniscriptInstructionOutcomeContinue;
 	}
 	if (attrib == "y") {
-		DynamicValueWriteIntegerHelper<int16>::create(&y, proxy);
+		DynamicValueWriteIntegerHelper<int16>::create(&point.y, proxy);
 		return kMiniscriptInstructionOutcomeContinue;
 	}
 
 	return kMiniscriptInstructionOutcomeFailed;
 }
 
-Common::String Point16::toString() const {
-	return Common::String::format("(%i,%i)", x, y);
+Common::String pointToString(const Common::Point &point) {
+	return Common::String::format("(%i,%i)", point.x, point.y);
 }
 
 bool IntRange::load(const Data::IntRange &range) {
@@ -347,6 +340,10 @@ ColorRGB8 ColorRGB8::create(uint8 r, uint8 g, uint8 b) {
 MessageFlags::MessageFlags() : relay(true), cascade(true), immediate(true) {
 }
 
+Common::Point Point16POD::toScummVMPoint() const {
+	return Common::Point(x, y);
+}
+
 DynamicListContainerBase::~DynamicListContainerBase() {
 }
 
@@ -358,7 +355,7 @@ void DynamicListDefaultSetter::defaultSet(double &value) {
 	value = 0.0;
 }
 
-void DynamicListDefaultSetter::defaultSet(Point16 &value) {
+void DynamicListDefaultSetter::defaultSet(Common::Point &value) {
 	value.x = 0;
 	value.y = 0;
 }
@@ -396,6 +393,10 @@ void DynamicListDefaultSetter::defaultSet(Common::SharedPtr<DynamicList> &value)
 void DynamicListDefaultSetter::defaultSet(ObjectReference &value) {
 }
 
+Common::Point DynamicListValueConverter<Common::Point>::dereference(const Point16POD *source) {
+	return source->toScummVMPoint();
+}
+
 bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const int32 *&outPtr) {
 	if (dynValue.getType() != DynamicValueTypes::kInteger)
 		return false;
@@ -410,7 +411,7 @@ bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const d
 	return true;
 }
 
-bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const Point16 *&outPtr) {
+bool DynamicListValueImporter::importValue(const DynamicValue &dynValue, const Point16POD *&outPtr) {
 	if (dynValue.getType() != DynamicValueTypes::kPoint)
 		return false;
 	outPtr = &dynValue.getPoint();
@@ -482,7 +483,7 @@ void DynamicListValueExporter::exportValue(DynamicValue &dynValue, const double
 	dynValue.setFloat(value);
 }
 
-void DynamicListValueExporter::exportValue(DynamicValue &dynValue, const Point16 &value) {
+void DynamicListValueExporter::exportValue(DynamicValue &dynValue, const Common::Point &value) {
 	dynValue.setPoint(value);
 }
 
@@ -682,9 +683,9 @@ const Common::Array<double> &DynamicList::getFloat() const {
 	return *static_cast<const Common::Array<double> *>(_container->getConstArrayPtr());
 }
 
-const Common::Array<Point16> &DynamicList::getPoint() const {
+const Common::Array<Common::Point> &DynamicList::getPoint() const {
 	assert(_type == DynamicValueTypes::kPoint);
-	return *static_cast<const Common::Array<Point16> *>(_container->getConstArrayPtr());
+	return *static_cast<const Common::Array<Common::Point> *>(_container->getConstArrayPtr());
 }
 
 const Common::Array<IntRange> &DynamicList::getIntRange() const {
@@ -742,9 +743,9 @@ Common::Array<double> &DynamicList::getFloat() {
 	return *static_cast<Common::Array<double> *>(_container->getArrayPtr());
 }
 
-Common::Array<Point16> &DynamicList::getPoint() {
+Common::Array<Common::Point> &DynamicList::getPoint() {
 	assert(_type == DynamicValueTypes::kPoint);
-	return *static_cast<Common::Array<Point16> *>(_container->getArrayPtr());
+	return *static_cast<Common::Array<Common::Point> *>(_container->getArrayPtr());
 }
 
 Common::Array<IntRange> &DynamicList::getIntRange() {
@@ -917,7 +918,7 @@ bool DynamicList::changeToType(DynamicValueTypes::DynamicValueType type) {
 		_container = new DynamicListContainer<double>();
 		break;
 	case DynamicValueTypes::kPoint:
-		_container = new DynamicListContainer<Point16>();
+		_container = new DynamicListContainer<Common::Point>();
 		break;
 	case DynamicValueTypes::kIntegerRange:
 		_container = new DynamicListContainer<IntRange>();
@@ -985,7 +986,7 @@ MiniscriptInstructionOutcome DynamicList::WriteProxyInterface::refAttrib(Miniscr
 	switch (list->getType()) {
 	case DynamicValueTypes::kPoint:
 		list->expandToMinimumSize(ptrOrOffset + 1);
-		return list->getPoint()[ptrOrOffset].refAttrib(thread, proxy, attrib);
+		return pointWriteRefAttrib(list->getPoint()[ptrOrOffset], thread, proxy, attrib);
 	case DynamicValueTypes::kIntegerRange:
 		list->expandToMinimumSize(ptrOrOffset + 1);
 		return list->getIntRange()[ptrOrOffset].refAttrib(thread, proxy, attrib);
@@ -1078,8 +1079,8 @@ bool DynamicValue::load(const Data::InternalTypeTaggedValue &data, const Common:
 		break;
 	case Data::InternalTypeTaggedValue::kPoint:
 		_type = DynamicValueTypes::kPoint;
-		if (!_value.asPoint.load(data.value.asPoint))
-			return false;
+		_value.asPoint.x = data.value.asPoint.x;
+		_value.asPoint.y = data.value.asPoint.y;
 		break;
 	case Data::InternalTypeTaggedValue::kIntegerRange:
 		_type = DynamicValueTypes::kIntegerRange;
@@ -1160,8 +1161,8 @@ bool DynamicValue::load(const Data::PlugInTypeTaggedValue &data) {
 		break;
 	case Data::PlugInTypeTaggedValue::kPoint:
 		_type = DynamicValueTypes::kPoint;
-		if (!_value.asPoint.load(data.value.asPoint))
-			return false;
+		_value.asPoint.x = data.value.asPoint.x;
+		_value.asPoint.y = data.value.asPoint.y;
 		break;
 	default:
 		assert(false);
@@ -1185,7 +1186,7 @@ const double &DynamicValue::getFloat() const {
 	return _value.asFloat;
 }
 
-const Point16 &DynamicValue::getPoint() const {
+const Point16POD &DynamicValue::getPoint() const {
 	assert(_type == DynamicValueTypes::kPoint);
 	return _value.asPoint;
 }
@@ -1288,11 +1289,12 @@ void DynamicValue::setFloat(double value) {
 	_value.asFloat = value;
 }
 
-void DynamicValue::setPoint(const Point16 &value) {
+void DynamicValue::setPoint(const Common::Point &value) {
 	if (_type != DynamicValueTypes::kPoint)
 		clear();
 	_type = DynamicValueTypes::kPoint;
-	_value.asPoint = value;
+	_value.asPoint.x = value.x;
+	_value.asPoint.y = value.y;
 }
 
 void DynamicValue::setIntRange(const IntRange &value) {
@@ -1449,7 +1451,7 @@ bool DynamicValue::operator==(const DynamicValue &other) const {
 	case DynamicValueTypes::kFloat:
 		return _value.asFloat == other._value.asFloat;
 	case DynamicValueTypes::kPoint:
-		return _value.asPoint == other._value.asPoint;
+		return _value.asPoint.x == other._value.asPoint.x && _value.asPoint.y == other._value.asPoint.y;
 	case DynamicValueTypes::kIntegerRange:
 		return _value.asIntRange == other._value.asIntRange;
 	case DynamicValueTypes::kVector:
@@ -1628,18 +1630,18 @@ MiniscriptInstructionOutcome DynamicValueWritePointHelper::write(MiniscriptThrea
 		return kMiniscriptInstructionOutcomeFailed;
 	}
 
-	*static_cast<Point16 *>(objectRef) = value.getPoint();
+	*static_cast<Common::Point *>(objectRef) = value.getPoint().toScummVMPoint();
 
 	return kMiniscriptInstructionOutcomeContinue;
 }
 
 MiniscriptInstructionOutcome DynamicValueWritePointHelper::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const {
 	if (attrib == "x") {
-		DynamicValueWriteIntegerHelper<int16>::create(&static_cast<Point16 *>(objectRef)->x, proxy);
+		DynamicValueWriteIntegerHelper<int16>::create(&static_cast<Common::Point *>(objectRef)->x, proxy);
 		return kMiniscriptInstructionOutcomeContinue;
 	}
 	if (attrib == "y") {
-		DynamicValueWriteIntegerHelper<int16>::create(&static_cast<Point16 *>(objectRef)->y, proxy);
+		DynamicValueWriteIntegerHelper<int16>::create(&static_cast<Common::Point *>(objectRef)->y, proxy);
 		return kMiniscriptInstructionOutcomeContinue;
 	}
 
@@ -1651,7 +1653,7 @@ MiniscriptInstructionOutcome DynamicValueWritePointHelper::refAttribIndexed(Mini
 	return kMiniscriptInstructionOutcomeFailed;
 }
 
-void DynamicValueWritePointHelper::create(Point16 *pointValue, DynamicValueWriteProxy &proxy) {
+void DynamicValueWritePointHelper::create(Common::Point *pointValue, DynamicValueWriteProxy &proxy) {
 	proxy.pod.ptrOrOffset = 0;
 	proxy.pod.objectRef = pointValue;
 	proxy.pod.ifc = &_instance;
@@ -3512,7 +3514,7 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, ISaveUIProvider *saveProv
 	_nextRuntimeGUID(1), _realDisplayMode(kColorDepthModeInvalid), _fakeDisplayMode(kColorDepthModeInvalid),
 	_displayWidth(1024), _displayHeight(768), _realTimeBase(0), _playTimeBase(0), _sceneTransitionState(kSceneTransitionStateNotTransitioning),
 	_lastFrameCursor(nullptr), _defaultCursor(new DefaultCursorGraphic()), _platform(kProjectPlatformUnknown),
-	_cachedMousePosition(Point16::create(0, 0)), _realMousePosition(Point16::create(0, 0)), _trackedMouseOutside(false),
+	_cachedMousePosition(Common::Point(0, 0)), _realMousePosition(Common::Point(0, 0)), _trackedMouseOutside(false),
 	_forceCursorRefreshOnce(true), _haveModifierOverrideCursor(false), _sceneGraphChanged(false), _isQuitting(false), _collisionCheckTime(0) {
 	_random.reset(new Common::RandomSource("mtropolis"));
 
@@ -4637,9 +4639,9 @@ VThreadState Runtime::updateMouseStateTask(const UpdateMouseStateTaskData &data)
 			_mouseTrackingDragStart = _cachedMousePosition;
 			if (tracked->isElement() && static_cast<Element *>(tracked)->isVisual()) {
 				Common::Rect initialRect = static_cast<VisualElement *>(tracked)->getRelativeRect();
-				_mouseTrackingObjectInitialOrigin = Point16::create(initialRect.left, initialRect.top);
+				_mouseTrackingObjectInitialOrigin = Common::Point(initialRect.left, initialRect.top);
 			} else
-				_mouseTrackingObjectInitialOrigin = Point16::create(0, 0);
+				_mouseTrackingObjectInitialOrigin = Common::Point(0, 0);
 			_trackedMouseOutside = false;
 
 			MessageToSend msg;
@@ -4671,7 +4673,7 @@ VThreadState Runtime::updateMouseStateTask(const UpdateMouseStateTaskData &data)
 	}
 
 	DynamicValue mousePtValue;
-	mousePtValue.setPoint(Point16::create(_cachedMousePosition.x, _cachedMousePosition.y));
+	mousePtValue.setPoint(Common::Point(_cachedMousePosition.x, _cachedMousePosition.y));
 
 	for (size_t ri = 0; ri < messagesToSend.size(); ri++) {
 		const MessageToSend &msg = messagesToSend[messagesToSend.size() - 1 - ri];
@@ -4751,7 +4753,7 @@ VThreadState Runtime::updateMousePositionTask(const UpdateMousePositionTaskData
 		Element *element = static_cast<Element *>(tracked.get());
 		assert(element->isVisual());
 		VisualElement *visual = static_cast<VisualElement *>(element);
-		Point16 parentOrigin = visual->getParentOrigin();
+		Common::Point parentOrigin = visual->getParentOrigin();
 		int32 relativeX = data.x - parentOrigin.x;
 		int32 relativeY = data.y - parentOrigin.y;
 		bool mouseOutside = !visual->isMouseInsideBox(relativeX, relativeY) || !visual->isMouseCollisionAtPoint(relativeX, relativeY);
@@ -4774,13 +4776,13 @@ VThreadState Runtime::updateMousePositionTask(const UpdateMousePositionTaskData
 
 		// TODO: Figure out the right location for this
 		if (element->isVisual()) {
-			Point16 targetPoint = Point16::create(data.x - _mouseTrackingDragStart.x + _mouseTrackingObjectInitialOrigin.x, data.y - _mouseTrackingDragStart.y + _mouseTrackingObjectInitialOrigin.y);
+			Common::Point targetPoint = Common::Point(data.x - _mouseTrackingDragStart.x + _mouseTrackingObjectInitialOrigin.x, data.y - _mouseTrackingDragStart.y + _mouseTrackingObjectInitialOrigin.y);
 			static_cast<VisualElement *>(element)->handleDragMotion(this, _mouseTrackingObjectInitialOrigin, targetPoint);
 		}
 	}
 
 	DynamicValue mousePtValue;
-	mousePtValue.setPoint(Point16::create(data.x, data.y));
+	mousePtValue.setPoint(Common::Point(data.x, data.y));
 
 	for (size_t ri = 0; ri < messagesToSend.size(); ri++) {
 		const MessageToSend &msg = messagesToSend[messagesToSend.size() - 1 - ri];
@@ -4953,7 +4955,7 @@ void Runtime::onKeyboardEvent(const Common::EventType evtType, bool repeat, cons
 		focusWindow->onKeyboardEvent(evtType, repeat, keyEvt);
 }
 
-const Point16 &Runtime::getCachedMousePosition() const {
+const Common::Point &Runtime::getCachedMousePosition() const {
 	return _cachedMousePosition;
 }
 
@@ -6563,11 +6565,11 @@ void VisualElementRenderProperties::setShadowSize(uint16 size) {
 	_shadowSize = size;
 }
 
-const Common::Array<Point16> &VisualElementRenderProperties::getPolyPoints() const {
+const Common::Array<Common::Point> &VisualElementRenderProperties::getPolyPoints() const {
 	return _polyPoints;
 }
 
-Common::Array<Point16> &VisualElementRenderProperties::modifyPolyPoints() {
+Common::Array<Common::Point> &VisualElementRenderProperties::modifyPolyPoints() {
 	_isDirty = true;
 	return _polyPoints;
 }
@@ -6598,7 +6600,7 @@ VisualElementRenderProperties &VisualElementRenderProperties::operator=(const Vi
 }
 
 VisualElement::VisualElement()
-	: _rect(0, 0, 0, 0), _cachedAbsoluteOrigin(Point16::create(0, 0))
+	: _rect(0, 0, 0, 0), _cachedAbsoluteOrigin(Common::Point(0, 0))
 	, _contentsDirty(true) {
 }
 
@@ -6668,7 +6670,7 @@ bool VisualElement::readAttribute(MiniscriptThread *thread, DynamicValue &result
 		result.setBool(_directToScreen);
 		return true;
 	} else if (attrib == "position") {
-		result.setPoint(Point16::create(_rect.left, _rect.top));
+		result.setPoint(Common::Point(_rect.left, _rect.top));
 		return true;
 	} else if (attrib == "centerposition") {
 		result.setPoint(getCenterPosition());
@@ -6721,8 +6723,8 @@ const Common::Rect &VisualElement::getRelativeRect() const {
 	return _rect;
 }
 
-Point16 VisualElement::getParentOrigin() const {
-	Point16 pos = Point16::create(0, 0);
+Common::Point VisualElement::getParentOrigin() const {
+	Common::Point pos = Common::Point(0, 0);
 	if (_parent && _parent->isElement()) {
 		Element *element = static_cast<Element *>(_parent);
 		if (element->isVisual()) {
@@ -6733,8 +6735,8 @@ Point16 VisualElement::getParentOrigin() const {
 	return pos;
 }
 
-Point16 VisualElement::getGlobalPosition() const {
-	Point16 pos = getParentOrigin();
+Common::Point VisualElement::getGlobalPosition() const {
+	Common::Point pos = getParentOrigin();
 
 	pos.x += _rect.left;
 	pos.y += _rect.top;
@@ -6742,11 +6744,11 @@ Point16 VisualElement::getGlobalPosition() const {
 	return pos;
 }
 
-const Point16 &VisualElement::getCachedAbsoluteOrigin() const {
+const Common::Point &VisualElement::getCachedAbsoluteOrigin() const {
 	return _cachedAbsoluteOrigin;
 }
 
-void VisualElement::setCachedAbsoluteOrigin(const Point16 &absOrigin) {
+void VisualElement::setCachedAbsoluteOrigin(const Common::Point &absOrigin) {
 	_cachedAbsoluteOrigin = absOrigin;
 }
 
@@ -6758,11 +6760,11 @@ const Common::SharedPtr<DragMotionProperties> &VisualElement::getDragMotionPrope
 	return _dragProps;
 }
 
-void VisualElement::handleDragMotion(Runtime *runtime, const Point16 &initialPoint, const Point16 &targetPointRef) {
+void VisualElement::handleDragMotion(Runtime *runtime, const Common::Point &initialPoint, const Common::Point &targetPointRef) {
 	if (!_dragProps)
 		return;
 
-	Point16 targetPoint = targetPointRef;
+	Common::Point targetPoint = targetPointRef;
 
 	// NOTE: Constraints do not override insets if the object is out of bounds
 	if (_dragProps->constraintDirection == kConstraintDirectionHorizontal)
@@ -6872,7 +6874,7 @@ MiniscriptInstructionOutcome VisualElement::scriptSetDirect(MiniscriptThread *th
 
 MiniscriptInstructionOutcome VisualElement::scriptSetPosition(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kPoint) {
-		const Point16 &destPoint = value.getPoint();
+		Common::Point destPoint = value.getPoint().toScummVMPoint();
 		int32 xDelta = destPoint.x - _rect.left;
 		int32 yDelta = destPoint.y - _rect.top;
 
@@ -6915,8 +6917,8 @@ MiniscriptInstructionOutcome VisualElement::scriptSetPositionY(MiniscriptThread
 
 MiniscriptInstructionOutcome VisualElement::scriptSetCenterPosition(MiniscriptThread *thread, const DynamicValue &value) {
 	if (value.getType() == DynamicValueTypes::kPoint) {
-		const Point16 &destPoint = value.getPoint();
-		const Point16 &srcPoint = getCenterPosition();
+		const Common::Point destPoint = value.getPoint().toScummVMPoint();
+		const Common::Point &srcPoint = getCenterPosition();
 		int32 xDelta = destPoint.x - srcPoint.x;
 		int32 yDelta = destPoint.y - srcPoint.y;
 
@@ -7040,8 +7042,8 @@ void VisualElement::offsetTranslate(int32 xDelta, int32 yDelta, bool cachedOrigi
 	}
 }
 
-Point16 VisualElement::getCenterPosition() const {
-	return Point16::create((_rect.left + _rect.right) / 2, (_rect.top + _rect.bottom) / 2);
+Common::Point VisualElement::getCenterPosition() const {
+	return Common::Point((_rect.left + _rect.right) / 2, (_rect.top + _rect.bottom) / 2);
 }
 
 VThreadState VisualElement::changeVisibilityTask(const ChangeFlagTaskData &taskData) {
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index ede6b1bebb5..87883e1141e 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -285,30 +285,8 @@ bool isCommand(EventID eventID);
 
 } // End of namespace EventIDs
 
-struct Point16 {
-	int16 x;
-	int16 y;
-
-	bool load(const Data::Point &point);
-
-	inline bool operator==(const Point16 &other) const {
-		return x == other.x && y == other.y;
-	}
-
-	inline bool operator!=(const Point16 &other) const {
-		return !((*this) == other);
-	}
-
-	inline static Point16 create(int16 x, int16 y) {
-		Point16 result;
-		result.x = x;
-		result.y = y;
-		return result;
-	}
-
-	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib);
-	Common::String toString() const;
-};
+MiniscriptInstructionOutcome pointWriteRefAttrib(Common::Point &point, MiniscriptThread *thread, DynamicValueWriteProxy &proxy, const Common::String &attrib);
+Common::String pointToString(const Common::Point &point);
 
 struct IntRange {
 	int32 min;
@@ -506,6 +484,13 @@ struct DynamicValueWriteProxy {
 	Common::SharedPtr<DynamicList> containerList;
 };
 
+struct Point16POD {
+	int16 x;
+	int16 y;
+
+	Common::Point toScummVMPoint() const;
+};
+
 
 class DynamicListContainerBase {
 public:
@@ -525,7 +510,7 @@ public:
 struct DynamicListDefaultSetter {
 	static void defaultSet(int32 &value);
 	static void defaultSet(double &value);
-	static void defaultSet(Point16 &value);
+	static void defaultSet(Common::Point &value);
 	static void defaultSet(IntRange &value);
 	static void defaultSet(bool &value);
 	static void defaultSet(AngleMagVector &value);
@@ -536,10 +521,24 @@ struct DynamicListDefaultSetter {
 	static void defaultSet(ObjectReference &value);
 };
 
+template<class T>
+struct DynamicListValueConverter {
+	typedef T DynamicValuePODType_t;
+
+	static const T &dereference(const T *source) { return *source; }
+};
+
+template<>
+struct DynamicListValueConverter<Common::Point> {
+	typedef Point16POD DynamicValuePODType_t;
+
+	static Common::Point dereference(const Point16POD *source);
+};
+
 struct DynamicListValueImporter {
 	static bool importValue(const DynamicValue &dynValue, const int32 *&outPtr);
 	static bool importValue(const DynamicValue &dynValue, const double *&outPtr);
-	static bool importValue(const DynamicValue &dynValue, const Point16 *&outPtr);
+	static bool importValue(const DynamicValue &dynValue, const Point16POD *&outPtr);
 	static bool importValue(const DynamicValue &dynValue, const IntRange *&outPtr);
 	static bool importValue(const DynamicValue &dynValue, const bool *&outPtr);
 	static bool importValue(const DynamicValue &dynValue, const AngleMagVector *&outPtr);
@@ -553,7 +552,7 @@ struct DynamicListValueImporter {
 struct DynamicListValueExporter {
 	static void exportValue(DynamicValue &dynValue, const int32 &value);
 	static void exportValue(DynamicValue &dynValue, const double &value);
-	static void exportValue(DynamicValue &dynValue, const Point16 &value);
+	static void exportValue(DynamicValue &dynValue, const Common::Point &value);
 	static void exportValue(DynamicValue &dynValue, const IntRange &value);
 	static void exportValue(DynamicValue &dynValue, const bool &value);
 	static void exportValue(DynamicValue &dynValue, const AngleMagVector &value);
@@ -625,7 +624,7 @@ private:
 
 template<class T>
 bool DynamicListContainer<T>::setAtIndex(size_t index, const DynamicValue &dynValue) {
-	const T *valuePtr = nullptr;
+	const typename DynamicListValueConverter<T>::DynamicValuePODType_t *valuePtr = nullptr;
 	if (!DynamicListValueImporter::importValue(dynValue, valuePtr))
 		return false;
 
@@ -638,9 +637,9 @@ bool DynamicListContainer<T>::setAtIndex(size_t index, const DynamicValue &dynVa
 				_array.push_back(defaultValue);
 			}
 		}
-		_array.push_back(*valuePtr);
+		_array.push_back(DynamicListValueConverter<T>::dereference(valuePtr));
 	} else {
-		_array[index] = *valuePtr;
+		_array[index] = DynamicListValueConverter<T>::dereference(valuePtr);
 	}
 
 	return true;
@@ -715,7 +714,7 @@ struct DynamicList {
 
 	const Common::Array<int32> &getInt() const;
 	const Common::Array<double> &getFloat() const;
-	const Common::Array<Point16> &getPoint() const;
+	const Common::Array<Common::Point> &getPoint() const;
 	const Common::Array<IntRange> &getIntRange() const;
 	const Common::Array<AngleMagVector> &getVector() const;
 	const Common::Array<Label> &getLabel() const;
@@ -728,7 +727,7 @@ struct DynamicList {
 
 	Common::Array<int32> &getInt();
 	Common::Array<double> &getFloat();
-	Common::Array<Point16> &getPoint();
+	Common::Array<Common::Point> &getPoint();
 	Common::Array<IntRange> &getIntRange();
 	Common::Array<AngleMagVector> &getVector();
 	Common::Array<Label> &getLabel();
@@ -791,7 +790,7 @@ struct DynamicValue {
 
 	const int32 &getInt() const;
 	const double &getFloat() const;
-	const Point16 &getPoint() const;
+	const Point16POD &getPoint() const;
 	const IntRange &getIntRange() const;
 	const AngleMagVector &getVector() const;
 	const Label &getLabel() const;
@@ -812,7 +811,7 @@ struct DynamicValue {
 
 	void setInt(int32 value);
 	void setFloat(double value);
-	void setPoint(const Point16 &value);
+	void setPoint(const Common::Point &value);
 	void setIntRange(const IntRange &value);
 	void setVector(const AngleMagVector &value);
 	void setLabel(const Label &value);
@@ -848,7 +847,7 @@ private:
 		Label asLabel;
 		VarReference asVarReference;
 		Event asEvent;
-		Point16 asPoint;
+		Point16POD asPoint;
 		bool asBool;
 		DynamicValueReadProxyPOD asReadProxy;
 		DynamicValueWriteProxyPOD asWriteProxy;
@@ -949,7 +948,7 @@ struct DynamicValueWritePointHelper : public IDynamicValueWriteInterface {
 	MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const override;
 	MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const override;
 
-	static void create(Point16 *pointValue, DynamicValueWriteProxy &proxy);
+	static void create(Common::Point *pointValue, DynamicValueWriteProxy &proxy);
 
 private:
 	static DynamicValueWritePointHelper _instance;
@@ -1523,7 +1522,7 @@ public:
 	void onMouseUp(int32 x, int32 y, Actions::MouseButton mButton);
 	void onKeyboardEvent(const Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt);
 
-	const Point16 &getCachedMousePosition() const;
+	const Common::Point &getCachedMousePosition() const;
 	void setModifierCursorOverride(uint32 cursorID);
 	void clearModifierCursorOverride();
 	void forceCursorRefreshOnce();
@@ -1721,10 +1720,10 @@ private:
 	Common::SharedPtr<AssetManagerInterface> _assetManagerInterface;
 
 	// The cached mouse position is updated at frame end
-	Point16 _cachedMousePosition;
+	Common::Point _cachedMousePosition;
 
 	// The real mouse position is updated all the time (even when suspended)
-	Point16 _realMousePosition;
+	Common::Point _realMousePosition;
 
 	// Mouse control is tracked in two ways: Mouse over is detected with mouse movement AND when
 	// "refreshCursor" is set on the world manager, it indicates the frontmost object that
@@ -1734,8 +1733,8 @@ private:
 	// Note that mouseOverObject is also NOT necessarily what will receive mouse down events.
 	Common::WeakPtr<Structural> _mouseOverObject;
 	Common::WeakPtr<Structural> _mouseTrackingObject;
-	Point16 _mouseTrackingDragStart;
-	Point16 _mouseTrackingObjectInitialOrigin;
+	Common::Point _mouseTrackingDragStart;
+	Common::Point _mouseTrackingObjectInitialOrigin;
 	bool _trackedMouseOutside;
 	bool _forceCursorRefreshOnce;
 
@@ -2356,8 +2355,8 @@ public:
 	uint16 getShadowSize() const;
 	void setShadowSize(uint16 size);
 
-	const Common::Array<Point16> &getPolyPoints() const;
-	Common::Array<Point16> &modifyPolyPoints();
+	const Common::Array<Common::Point> &getPolyPoints() const;
+	Common::Array<Common::Point> &modifyPolyPoints();
 
 	bool isDirty() const;
 	void clearDirty();
@@ -2374,7 +2373,7 @@ private:
 	uint16 _shadowSize;
 	ColorRGB8 _shadowColor;
 
-	Common::Array<Point16> _polyPoints;
+	Common::Array<Common::Point> _polyPoints;
 
 	bool _isDirty;
 };
@@ -2400,19 +2399,19 @@ public:
 	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
 	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &writeProxy, const Common::String &attrib) override;
 
-	Point16 getParentOrigin() const;
-	Point16 getGlobalPosition() const;
+	Common::Point getParentOrigin() const;
+	Common::Point getGlobalPosition() const;
 	const Common::Rect &getRelativeRect() const;
 
 	// The cached absolute origin is from the last time the element was rendered.
 	// Do not rely on it mid-frame.
-	const Point16 &getCachedAbsoluteOrigin() const;
-	void setCachedAbsoluteOrigin(const Point16 &absOrigin);
+	const Common::Point &getCachedAbsoluteOrigin() const;
+	void setCachedAbsoluteOrigin(const Common::Point &absOrigin);
 
 	void setDragMotionProperties(const Common::SharedPtr<DragMotionProperties> &dragProps);
 	const Common::SharedPtr<DragMotionProperties> &getDragMotionProperties() const;
 
-	void handleDragMotion(Runtime *runtime, const Point16 &initialOrigin, const Point16 &targetOrigin);
+	void handleDragMotion(Runtime *runtime, const Common::Point &initialOrigin, const Common::Point &targetOrigin);
 
 	struct OffsetTranslateTaskData {
 		int32 dx;
@@ -2452,7 +2451,7 @@ protected:
 
 	void offsetTranslate(int32 xDelta, int32 yDelta, bool cachedOriginOnly);
 
-	Point16 getCenterPosition() const;
+	Common::Point getCenterPosition() const;
 
 	struct ChangeFlagTaskData {
 		bool desiredFlag;
@@ -2466,7 +2465,7 @@ protected:
 	bool _directToScreen;
 	bool _visible;
 	Common::Rect _rect;
-	Point16 _cachedAbsoluteOrigin;
+	Common::Point _cachedAbsoluteOrigin;
 	uint16 _layer;
 
 	Common::SharedPtr<DragMotionProperties> _dragProps;
diff --git a/engines/mtropolis/vthread.h b/engines/mtropolis/vthread.h
index d1aa686518e..99c88aef040 100644
--- a/engines/mtropolis/vthread.h
+++ b/engines/mtropolis/vthread.h
@@ -19,8 +19,8 @@
  *
  */
 
-#ifndef MTROPOLIS_TASKSTACK_H
-#define MTROPOLIS_TASKSTACK_H
+#ifndef MTROPOLIS_VTHREAD_H
+#define MTROPOLIS_VTHREAD_H
 
 #include "mtropolis/debug.h"
 


Commit: 09a885e49da06e6f9dd12fe2e314dcb805d4a254
    https://github.com/scummvm/scummvm/commit/09a885e49da06e6f9dd12fe2e314dcb805d4a254
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Switch to using common XPFloat

Changed paths:
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/runtime.cpp


diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index 709c4f335c3..144f11fb106 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -203,6 +203,21 @@ bool DataReader::readF64(double &value) {
 	return checkErrorAndReset();
 }
 
+bool DataReader::readPlatformFloat(Common::XPFloat &value) {
+	if (_projectFormat == kProjectFormatMacintosh) {
+		return readU16(value.signAndExponent) && readU64(value.mantissa);
+	} else if (_projectFormat == kProjectFormatWindows) {
+		uint64 bits;
+		if (!readU64(bits))
+			return false;
+		value = Common::XPFloat::fromDoubleBits(bits);
+
+		return true;
+	}
+
+	return false;
+}
+
 bool DataReader::read(void *dest, size_t size) {
 	while (size > 0) {
 		uint32 thisChunkSize = 0xffffffffu;
@@ -358,78 +373,24 @@ bool IntRange::load(DataReader& reader) {
 	return reader.readS32(min) && reader.readS32(max);
 }
 
-bool XPFloat::load(DataReader &reader) {
-	if (reader.getProjectFormat() == kProjectFormatMacintosh)
-		return reader.readU16(signAndExponent) && reader.readU64(mantissa);
-	else if (reader.getProjectFormat() == kProjectFormatWindows) {
-		uint64 doublePrecFloatBits;
-		if (!reader.readU64(doublePrecFloatBits))
-			return false;
-
-		uint8 sign = ((doublePrecFloatBits >> 63) & 1);
-		int16 exponent = ((doublePrecFloatBits >> 52) & ((1 << 11) - 1));
-		uint64 workMantissa = (doublePrecFloatBits & ((static_cast<uint64>(1) << 52) - 1));
-
-		if (exponent == 0) {
-			// Subnormal number or zero
-			if (workMantissa != 0) {
-				while (((workMantissa >> 52) & 1) == 0) {
-					exponent--;
-					workMantissa <<= 1;
-				}
-			}
-		} else {
-			workMantissa |= (static_cast<uint64>(1) << 52);
-		}
-
-		exponent += 15360;
-
-		signAndExponent = static_cast<uint16>((sign << 15) | exponent);
-		mantissa = workMantissa << 11;
-
-		return true;
-	} else
-		return false;
+bool XPFloatVector::load(DataReader& reader) {
+	return reader.readPlatformFloat(angleRadians) && reader.readPlatformFloat(magnitude);
 }
 
-double XPFloat::toDouble() const {
-	uint8 sign = (signAndExponent >> 15) & 1;
-	int16 exponent = signAndExponent & 0x7fff;
-
-	// Eliminate implicit 1 and truncate from 63 to 52 bits
-	uint64 workMantissa = this->mantissa & (((static_cast<uint64>(1) << 52) - 1u) << 11);
-	workMantissa >>= 11;
-
-	if (workMantissa != 0 || exponent != 0) {
-		// Adjust exponent
-		exponent -= 15360;
-		if (exponent > 2046) {
-			// Too big, set to largest finite magnitude
-			exponent = 2046;
-			workMantissa = (static_cast<uint64>(1) << 52) - 1u;
-		} else if (exponent < 0) {
-			// Subnormal number
-			workMantissa |= (static_cast<uint64>(1) << 52);
-			if (exponent < -52) {
-				workMantissa = 0;
-				exponent = 0;
-			} else {
-				workMantissa >>= (-exponent);
-				exponent = 0;
-			}
-		}
-	}
+bool XPFloatPOD::load(DataReader &reader) {
+	Common::XPFloat xpFloat;
 
-	uint64 recombined = (static_cast<uint64>(sign) << 63) | (static_cast<uint64>(exponent) << 52) | static_cast<uint64>(workMantissa);
+	if (!reader.readPlatformFloat(xpFloat))
+		return false;
 
-	double d;
-	memcpy(&d, &recombined, 8);
+	signAndExponent = xpFloat.signAndExponent;
+	mantissa = xpFloat.mantissa;
 
-	return d;
+	return true;
 }
 
-bool XPFloatVector::load(DataReader& reader) {
-	return angleRadians.load(reader) && magnitude.load(reader);
+Common::XPFloat XPFloatPOD::toXPFloat() const {
+	return Common::XPFloat(signAndExponent, mantissa);
 }
 
 bool Label::load(DataReader &reader) {
@@ -1464,7 +1425,7 @@ DataReadErrorCode FloatingPointVariableModifier::load(DataReader &reader) {
 	if (_revision != 0x3e8)
 		return kDataReadErrorUnsupportedRevision;
 
-	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !value.load(reader))
+	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !reader.readPlatformFloat(value))
 		return kDataReadErrorReadFailed;
 
 	return kDataReadErrorNone;
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index 353c9fb4ffb..1c391b443b7 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -29,6 +29,7 @@
 #include "common/ptr.h"
 #include "common/stream.h"
 #include "common/rect.h"
+#include "common/xpfloat.h"
 
 // This contains defs related to parsing of mTropolis stored data into structured data objects.
 // This is separated from asset construction for a number of reasons, mainly that data parsing has
@@ -178,6 +179,7 @@ public:
 	bool readS64(int64 &value);
 	bool readF32(float &value);
 	bool readF64(double &value);
+	bool readPlatformFloat(Common::XPFloat &value);
 
 	bool read(void *dest, size_t size);
 
@@ -251,19 +253,19 @@ struct IntRange {
 	int32 max;
 };
 
-struct XPFloat {
+struct XPFloatVector {
 	bool load(DataReader &reader);
-	double toDouble() const;
 
-	uint64 mantissa;
-	uint16 signAndExponent;
+	Common::XPFloat angleRadians;
+	Common::XPFloat magnitude;
 };
 
-struct XPFloatVector {
-	bool load(DataReader &reader);
+struct XPFloatPOD {
+	uint16 signAndExponent;
+	uint64 mantissa;
 
-	XPFloat angleRadians;
-	XPFloat magnitude;
+	bool load(DataReader &reader);
+	Common::XPFloat toXPFloat() const;
 };
 
 struct Label {
@@ -303,7 +305,7 @@ struct InternalTypeTaggedValue {
 
 	union ValueUnion {
 		uint8 asBool;
-		XPFloat asFloat;
+		XPFloatPOD asFloat;
 		int32 asInteger;
 		IntRange asIntegerRange;
 		VariableReference asVariableReference;
@@ -336,7 +338,7 @@ struct PlugInTypeTaggedValue : public Common::NonCopyable {
 		int32 asInt;
 		Point asPoint;
 		IntRange asIntRange;
-		XPFloat asFloat;
+		XPFloatPOD asFloat;
 		uint16 asBoolean;
 		Event asEvent;
 		Label asLabel;
@@ -1404,7 +1406,7 @@ protected:
 struct FloatingPointVariableModifier : public DataObject {
 	TypicalModifierHeader modHeader;
 	uint8 unknown1[4];
-	XPFloat value;
+	Common::XPFloat value;
 
 protected:
 	DataReadErrorCode load(DataReader &reader) override;
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 1d3703b4685..2efc5720fc2 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -187,8 +187,8 @@ bool MiniscriptInstructionLoader<MiniscriptInstructions::PushValue>::loadInstruc
 	if (dataType == 0)
 		new (dest) MiniscriptInstructions::PushValue(MiniscriptInstructions::PushValue::kDataTypeNull, nullptr, false);
 	else if (dataType == 0x15) {
-		Data::XPFloat f;
-		if (!f.load(instrDataReader))
+		Common::XPFloat f;
+		if (!instrDataReader.readPlatformFloat(f))
 			return false;
 
 		double d = f.toDouble();
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 0f7c1ffc5ae..89e0301c7db 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -977,9 +977,9 @@ bool MidiModifier::load(const PlugInModifierLoaderContext &context, const Data::
 			|| data.embeddedTempo.type != Data::PlugInTypeTaggedValue::kFloat)
 			return false;
 
-		_modeSpecific.file.fadeIn = data.embeddedFadeIn.value.asFloat.toDouble();
-		_modeSpecific.file.fadeOut = data.embeddedFadeOut.value.asFloat.toDouble();
-		_modeSpecific.file.tempo = data.embeddedTempo.value.asFloat.toDouble();
+		_modeSpecific.file.fadeIn = data.embeddedFadeIn.value.asFloat.toXPFloat().toDouble();
+		_modeSpecific.file.fadeOut = data.embeddedFadeOut.value.asFloat.toXPFloat().toDouble();
+		_modeSpecific.file.tempo = data.embeddedTempo.value.asFloat.toXPFloat().toDouble();
 	} else {
 		_mode = kModeSingleNote;
 
@@ -990,7 +990,7 @@ bool MidiModifier::load(const PlugInModifierLoaderContext &context, const Data::
 		_modeSpecific.singleNote.note = data.modeSpecific.singleNote.note;
 		_modeSpecific.singleNote.velocity = data.modeSpecific.singleNote.velocity;
 		_modeSpecific.singleNote.program = data.modeSpecific.singleNote.program;
-		_modeSpecific.singleNote.duration = data.singleNoteDuration.value.asFloat.toDouble();
+		_modeSpecific.singleNote.duration = data.singleNoteDuration.value.asFloat.toXPFloat().toDouble();
 
 		_volume = 100;
 	}
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 586399f7665..50ce88dbad8 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -1089,7 +1089,7 @@ bool DynamicValue::load(const Data::InternalTypeTaggedValue &data, const Common:
 		break;
 	case Data::InternalTypeTaggedValue::kFloat:
 		_type = DynamicValueTypes::kFloat;
-		_value.asFloat = data.value.asFloat.toDouble();
+		_value.asFloat = data.value.asFloat.toXPFloat().toDouble();
 		break;
 	case Data::InternalTypeTaggedValue::kBool:
 		_type = DynamicValueTypes::kBoolean;
@@ -1133,7 +1133,7 @@ bool DynamicValue::load(const Data::PlugInTypeTaggedValue &data) {
 		break;
 	case Data::PlugInTypeTaggedValue::kFloat:
 		_type = DynamicValueTypes::kFloat;
-		_value.asFloat = data.value.asFloat.toDouble();
+		_value.asFloat = data.value.asFloat.toXPFloat().toDouble();
 		break;
 	case Data::PlugInTypeTaggedValue::kBoolean:
 		_type = DynamicValueTypes::kBoolean;


Commit: 06963b2f3d06e062169919cd232d3792e151819e
    https://github.com/scummvm/scummvm/commit/06963b2f3d06e062169919cd232d3792e151819e
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix missing header

Changed paths:
    engines/mtropolis/detection_tables.h


diff --git a/engines/mtropolis/detection_tables.h b/engines/mtropolis/detection_tables.h
index d41f9810ffc..a27e8935908 100644
--- a/engines/mtropolis/detection_tables.h
+++ b/engines/mtropolis/detection_tables.h
@@ -24,6 +24,8 @@
 
 #include "engines/advancedDetector.h"
 
+#include "mtropolis/detection.h"
+
 namespace MTropolis {
 
 static const MTropolisGameDescription gameDescriptions[] = {


Commit: 4bc7b4644074b287a99370de0321d5338d88654c
    https://github.com/scummvm/scummvm/commit/4bc7b4644074b287a99370de0321d5338d88654c
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add credits

Changed paths:
  A engines/mtropolis/credits.pl


diff --git a/engines/mtropolis/credits.pl b/engines/mtropolis/credits.pl
new file mode 100644
index 00000000000..aeba108ca44
--- /dev/null
+++ b/engines/mtropolis/credits.pl
@@ -0,0 +1,3 @@
+begin_section("mTropolis");
+	add_person("Eric Lasota", "OneEightHundred", "");
+end_section();


Commit: 53dde2193e7dbb3ad22043256c082fc493c1f287
    https://github.com/scummvm/scummvm/commit/53dde2193e7dbb3ad22043256c082fc493c1f287
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Merge prep header and warning cleanup

Changed paths:
    engines/mtropolis/assets.cpp
    engines/mtropolis/data.cpp
    engines/mtropolis/debug.cpp
    engines/mtropolis/detection.cpp
    engines/mtropolis/elements.cpp
    engines/mtropolis/elements.h
    engines/mtropolis/hacks.cpp
    engines/mtropolis/metaengine.cpp
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/modifiers.cpp
    engines/mtropolis/modifiers.h
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/mtropolis.h
    engines/mtropolis/plugin/obsidian.cpp
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/render.cpp
    engines/mtropolis/runtime.cpp
    engines/mtropolis/saveload.cpp


diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index 7d5755f7984..893754d18df 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -19,9 +19,6 @@
  *
  */
 
-#include "mtropolis/assets.h"
-#include "mtropolis/asset_factory.h"
-
 #include "graphics/managed_surface.h"
 #include "graphics/surface.h"
 
@@ -29,6 +26,9 @@
 
 #include "common/endian.h"
 
+#include "mtropolis/assets.h"
+#include "mtropolis/asset_factory.h"
+
 namespace MTropolis {
 
 Asset::Asset() : _assetID(0) {
@@ -127,15 +127,11 @@ bool CachedMToon::loadFromStream(const Common::SharedPtr<MToonMetadata> &metadat
 		uint16 fullWidth = metadata->rect.width();
 		uint16 fullHeight = metadata->rect.height();
 
-		uint16 firstFrameWidth = 0;
-		uint16 firstFrameHeight = 0;
-
 		bool haveAnyTemporalFrames = false;
 		bool haveDifferentDimensions = false;
 		_isRLETemporalCompressed = false;
 
 		for (size_t i = 0; i < metadata->frames.size(); i++) {
-			const MToonMetadata::FrameDef &frame = metadata->frames[i];
 			if (!_rleData[i].isKeyframe)
 				haveAnyTemporalFrames = true;
 
@@ -265,11 +261,6 @@ static bool decompressMToonRLE(const CachedMToon::RleFrame &frame, const Common:
 void CachedMToon::decompressRLEFrameToImage(size_t frameIndex, Graphics::Surface &surface) {
 	assert(surface.format == _rleOptimizedFormat);
 
-	const MToonMetadata::FrameDef &frameDef = _metadata->frames[frameIndex];
-
-	int32 originX = frameDef.rect.left;
-	int32 originY = frameDef.rect.top;
-
 	bool isBottomUp = (_metadata->imageFormat == MToonMetadata::kImageFormatWindows);
 
 	bool decompressedOK = false;
diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index 144f11fb106..c7f53e3eb78 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -19,10 +19,11 @@
  *
  */
 
-#include "mtropolis/data.h"
 #include "common/debug.h"
 #include "common/memstream.h"
 
+#include "mtropolis/data.h"
+
 namespace MTropolis {
 
 namespace Data {
diff --git a/engines/mtropolis/debug.cpp b/engines/mtropolis/debug.cpp
index ea7a2359af4..20baaa84b42 100644
--- a/engines/mtropolis/debug.cpp
+++ b/engines/mtropolis/debug.cpp
@@ -19,16 +19,15 @@
  *
  */
 
-#include "mtropolis/debug.h"
-#include "mtropolis/render.h"
-#include "mtropolis/runtime.h"
-
 #include "gui/dialog.h"
 
 #include "graphics/fontman.h"
 
 #include "common/hash-ptr.h"
 
+#include "mtropolis/debug.h"
+#include "mtropolis/render.h"
+#include "mtropolis/runtime.h"
 
 
 namespace MTropolis {
@@ -840,10 +839,10 @@ void DebugSceneTreeWindow::toolOnMouseDown(int32 x, int32 y, int mouseButton) {
 	if (mouseButton != Actions::kMouseButtonLeft)
 		return;
 
-	for (int i = 0; i < _sceneStack.size(); i++) {
+	for (uint i = 0; i < _sceneStack.size(); i++) {
 		int buttonLeft = kSceneStackGoToButtonX;
 		int buttonRight = buttonLeft + kSceneStackGoToButtonWidth;
-		int buttonTop = kSceneStackGoToButtonFirstY + i * kSceneStackRowHeight;
+		int buttonTop = kSceneStackGoToButtonFirstY + static_cast<int>(i) * kSceneStackRowHeight;
 		int buttonBottom = buttonTop + kSceneStackGoToButtonHeight;
 
 		if (x >= buttonLeft && x < buttonRight && y >= buttonTop && y < buttonBottom) {
@@ -858,7 +857,7 @@ void DebugSceneTreeWindow::toolOnMouseDown(int32 x, int32 y, int mouseButton) {
 
 	int32 row = (y - _treeYOffset) / kRowHeight;
 
-	if (row >= _renderEntries.size())
+	if (static_cast<uint32>(row) >= _renderEntries.size())
 		return;
 
 	const RenderEntry &renderEntry = _renderEntries[row];
@@ -995,7 +994,6 @@ void DebugInspectorWindow::update() {
 
 		_labeledRow.clear();
 	} else {
-		size_t oldNumLabeled = _unlabeledRow.size();
 		inspector->getDebuggable()->debugInspect(this);
 
 		_unlabeledRow.resize(_declUnlabeledRow);
@@ -1148,7 +1146,6 @@ void DebugStepThroughWindow::update() {
 void DebugStepThroughWindow::toolRenderSurface(int32 subAreaWidth, int32 subAreaHeight) {
 	const Graphics::PixelFormat fmt = _debugger->getRuntime()->getRenderPixelFormat();
 
-	uint32 whiteColor = fmt.RGBToColor(255, 255, 255);
 	uint32 blackColor = fmt.RGBToColor(0, 0, 0);
 
 	int32 renderHeight = subAreaHeight;
diff --git a/engines/mtropolis/detection.cpp b/engines/mtropolis/detection.cpp
index fccbdb4f637..b2b77089fa2 100644
--- a/engines/mtropolis/detection.cpp
+++ b/engines/mtropolis/detection.cpp
@@ -22,11 +22,11 @@
 #include "base/plugins.h"
 #include "engines/advancedDetector.h"
 
-#include "mtropolis/detection.h"
-
 #include "common/config-manager.h"
 #include "common/translation.h"
 
+#include "mtropolis/detection.h"
+
 static const PlainGameDescriptor mTropolisGames[] = {
 	{"obsidian", "Obsidian"},
 	{nullptr, nullptr}
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index 7ae61b86911..dee65e5f5cd 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -19,12 +19,6 @@
  *
  */
 
-#include "mtropolis/elements.h"
-#include "mtropolis/assets.h"
-#include "mtropolis/element_factory.h"
-#include "mtropolis/miniscript.h"
-#include "mtropolis/render.h"
-
 #include "video/video_decoder.h"
 #include "video/qt_decoder.h"
 
@@ -35,6 +29,12 @@
 #include "graphics/font.h"
 #include "graphics/managed_surface.h"
 
+#include "mtropolis/elements.h"
+#include "mtropolis/assets.h"
+#include "mtropolis/element_factory.h"
+#include "mtropolis/miniscript.h"
+#include "mtropolis/render.h"
+
 namespace MTropolis {
 
 
@@ -447,7 +447,6 @@ void GraphicElement::render(Window *window) {
 	if (clippedSrcRect.isEmpty())
 		return;
 
-	int32 srcToDestX = clippedDrawRect.left - clippedSrcRect.left;
 	int32 srcToDestY = clippedDrawRect.top - clippedSrcRect.top;
 
 	switch (_renderProps.getInkMode()) {
@@ -461,7 +460,6 @@ void GraphicElement::render(Window *window) {
 			uint32 fillColor = pixFmt.ARGBToColor(255, fillColorRGB8.r, fillColorRGB8.g, fillColorRGB8.b);
 
 			for (int32 srcY = clippedSrcRect.top; srcY < clippedSrcRect.bottom; srcY++) {
-				int32 destY = srcY + srcToDestY;
 				int32 spanWidth = clippedDrawRect.width();
 				void *destPixels = window->getSurface()->getBasePtr(clippedDrawRect.left, srcY + srcToDestY);
 				if (_mask) {
@@ -504,7 +502,6 @@ void GraphicElement::render(Window *window) {
 				colorMask = pixFmt.ARGBToColor(0, 255, 255, 255);
 
 			for (int32 srcY = clippedSrcRect.top; srcY < clippedSrcRect.bottom; srcY++) {
-				int32 destY = srcY + srcToDestY;
 				int32 spanWidth = clippedDrawRect.width();
 				void *destPixels = window->getSurface()->getBasePtr(clippedDrawRect.left, srcY + srcToDestY);
 				if (_mask) {
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index 70b1d3456c2..5925313cc83 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -22,12 +22,12 @@
 #ifndef MTROPOLIS_ELEMENTS_H
 #define MTROPOLIS_ELEMENTS_H
 
+#include "audio/mixer.h"
+
 #include "mtropolis/data.h"
 #include "mtropolis/runtime.h"
 #include "mtropolis/render.h"
 
-#include "audio/mixer.h"
-
 namespace Video {
 
 class VideoDecoder;
diff --git a/engines/mtropolis/hacks.cpp b/engines/mtropolis/hacks.cpp
index 5b08440540b..b0ce41c4ebc 100644
--- a/engines/mtropolis/hacks.cpp
+++ b/engines/mtropolis/hacks.cpp
@@ -19,10 +19,10 @@
  *
  */
 
-#include "mtropolis/hacks.h"
-
 #include "common/system.h"
 
+#include "mtropolis/hacks.h"
+
 namespace MTropolis {
 
 Hacks::Hacks() {
diff --git a/engines/mtropolis/metaengine.cpp b/engines/mtropolis/metaengine.cpp
index 4ade910fea5..11d99973d3f 100644
--- a/engines/mtropolis/metaengine.cpp
+++ b/engines/mtropolis/metaengine.cpp
@@ -21,17 +21,17 @@
 
 #include "engines/advancedDetector.h"
 
+#include "backends/keymapper/action.h"
+#include "backends/keymapper/keymap.h"
+
+#include "common/translation.h"
+
 #include "mtropolis/actions.h"
 #include "mtropolis/debug.h"
 #include "mtropolis/detection.h"
 
 #include "mtropolis/mtropolis.h"
 
-#include "backends/keymapper/action.h"
-#include "backends/keymapper/keymap.h"
-
-#include "common/translation.h"
-
 namespace MTropolis {
 
 uint32 MTropolisEngine::getGameID() const {
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index 2efc5720fc2..aee2a546926 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -19,12 +19,13 @@
  *
  */
 
-#include "mtropolis/miniscript.h"
 #include "common/config-manager.h"
 
 #include "common/random.h"
 #include "common/memstream.h"
 
+#include "mtropolis/miniscript.h"
+
 namespace MTropolis {
 
 static bool miniscriptEvaluateTruth(const DynamicValue& value) {
diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp
index 418e3b24025..83aeb998049 100644
--- a/engines/mtropolis/modifiers.cpp
+++ b/engines/mtropolis/modifiers.cpp
@@ -19,6 +19,8 @@
  *
  */
 
+#include "common/memstream.h"
+
 #include "mtropolis/miniscript.h"
 #include "mtropolis/modifiers.h"
 #include "mtropolis/modifier_factory.h"
@@ -26,8 +28,6 @@
 
 #include "mtropolis/elements.h"
 
-#include "common/memstream.h"
-
 namespace MTropolis {
 
 class CompoundVarSaver : public ISaveWriter {
diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h
index 6182e0cf8b3..e7c59224ae8 100644
--- a/engines/mtropolis/modifiers.h
+++ b/engines/mtropolis/modifiers.h
@@ -22,12 +22,12 @@
 #ifndef MTROPOLIS_MODIFIERS_H
 #define MTROPOLIS_MODIFIERS_H
 
+#include "common/events.h"
+
 #include "mtropolis/render.h"
 #include "mtropolis/runtime.h"
 #include "mtropolis/data.h"
 
-#include "common/events.h"
-
 namespace MTropolis {
 
 struct ModifierLoaderContext;
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index f92874d41ca..28289ac2a45 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -557,7 +557,6 @@ Common::Error MTropolisEngine::run() {
 #endif
 
 	bool paused = false;
-	int frameCounter = 0;
 
 	while (!shouldQuit()) {
 		handleEvents();
diff --git a/engines/mtropolis/mtropolis.h b/engines/mtropolis/mtropolis.h
index 0e9936320c8..fe62e4fba92 100644
--- a/engines/mtropolis/mtropolis.h
+++ b/engines/mtropolis/mtropolis.h
@@ -22,13 +22,13 @@
 #ifndef MTROPOLIS_MTROPOLIS_H
 #define MTROPOLIS_MTROPOLIS_H
 
-#include "mtropolis/detection.h"
-#include "mtropolis/saveload.h"
-
 #include "engines/engine.h"
 
 #include "common/random.h"
 
+#include "mtropolis/detection.h"
+#include "mtropolis/saveload.h"
+
 /**
  * This is the namespace of the mTropolis engine.
  *
diff --git a/engines/mtropolis/plugin/obsidian.cpp b/engines/mtropolis/plugin/obsidian.cpp
index c20a759eb4a..909024968d7 100644
--- a/engines/mtropolis/plugin/obsidian.cpp
+++ b/engines/mtropolis/plugin/obsidian.cpp
@@ -521,7 +521,6 @@ MiniscriptInstructionOutcome WordMixerModifier::scriptSetSearch(MiniscriptThread
 	if (searchLength < buckets.size()) {
 		const WordGameData::WordBucket &bucket = buckets[searchLength];
 
-		bool found = false;
 		for (size_t wi = 0; wi < bucket.wordIndexes.size(); wi++) {
 			const char *wordChars = &bucket.chars[wi * bucket.spacing];
 
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 89e0301c7db..94f93c5fd4f 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -19,17 +19,17 @@
  *
  */
 
-#include "mtropolis/plugin/standard.h"
-#include "mtropolis/plugins.h"
-
-#include "mtropolis/miniscript.h"
-
 #include "audio/mididrv.h"
 #include "audio/midiplayer.h"
 #include "audio/midiparser.h"
 
 #include "common/random.h"
 
+#include "mtropolis/plugin/standard.h"
+#include "mtropolis/plugins.h"
+
+#include "mtropolis/miniscript.h"
+
 namespace MTropolis {
 
 namespace Standard {
diff --git a/engines/mtropolis/render.cpp b/engines/mtropolis/render.cpp
index 94d6de26aa5..f5a2395987c 100644
--- a/engines/mtropolis/render.cpp
+++ b/engines/mtropolis/render.cpp
@@ -19,14 +19,14 @@
  *
  */
 
-#include "mtropolis/render.h"
-#include "mtropolis/runtime.h"
-
 #include "graphics/surface.h"
 #include "graphics/managed_surface.h"
 
 #include "graphics/cursorman.h"
 
+#include "mtropolis/render.h"
+#include "mtropolis/runtime.h"
+
 namespace MTropolis {
 
 template<class TNumber, int TResolution>
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 50ce88dbad8..1c3c55e1216 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -19,16 +19,6 @@
  *
  */
 
-#include "mtropolis/runtime.h"
-#include "mtropolis/data.h"
-#include "mtropolis/vthread.h"
-#include "mtropolis/asset_factory.h"
-#include "mtropolis/element_factory.h"
-#include "mtropolis/miniscript.h"
-#include "mtropolis/modifier_factory.h"
-#include "mtropolis/modifiers.h"
-#include "mtropolis/render.h"
-
 #include "common/debug.h"
 #include "common/file.h"
 #include "common/random.h"
@@ -44,6 +34,16 @@
 
 #include "audio/mixer.h"
 
+#include "mtropolis/runtime.h"
+#include "mtropolis/data.h"
+#include "mtropolis/vthread.h"
+#include "mtropolis/asset_factory.h"
+#include "mtropolis/element_factory.h"
+#include "mtropolis/miniscript.h"
+#include "mtropolis/modifier_factory.h"
+#include "mtropolis/modifiers.h"
+#include "mtropolis/render.h"
+
 namespace MTropolis {
 
 class MainWindow : public Window {
@@ -3546,7 +3546,6 @@ bool Runtime::runFrame() {
 	uint32 timeMillis = _system->getMillis();
 
 	uint32 realMSec = timeMillis - _realTimeBase - _realTime;
-	uint32 playMSec = timeMillis - _playTimeBase - _playTime;
 
 	_realTime = timeMillis - _realTimeBase;
 	_playTime = timeMillis - _playTimeBase;
@@ -5577,9 +5576,9 @@ void KeyboardEventSignaller::removeReceiver(IKeyboardEventReceiver *receiver) {
 }
 
 void MediaCueState::checkTimestampChange(Runtime *runtime, uint32 oldTS, uint32 newTS, bool continuousTimestamps, bool canTriggerDuring) {
-	bool entersRange = (oldTS < minTime && newTS >= minTime);
-	bool exitsRange = (oldTS <= maxTime && newTS > maxTime);
-	bool endsInRange = (newTS >= minTime && newTS <= maxTime);
+	bool entersRange = (static_cast<int32>(oldTS) < minTime && static_cast<int32>(newTS) >= minTime);
+	bool exitsRange = (static_cast<int32>(oldTS) <= maxTime && static_cast<int32>(newTS) > maxTime);
+	bool endsInRange = (static_cast<int32>(newTS) >= minTime && static_cast<int32>(newTS) <= maxTime);
 
 	bool shouldTrigger = false;
 	switch (triggerTiming)
@@ -7133,6 +7132,9 @@ bool ModifierSaveLoad::load(Modifier *modifier, Common::ReadStream *stream) {
 			return false;
 	}
 
+	if (modifier->getStaticGUID() != checkGUID)
+		return false;
+
 	return loadInternal(stream);
 }
 
diff --git a/engines/mtropolis/saveload.cpp b/engines/mtropolis/saveload.cpp
index 0995f18f47c..3bb93cb45e4 100644
--- a/engines/mtropolis/saveload.cpp
+++ b/engines/mtropolis/saveload.cpp
@@ -22,10 +22,10 @@
 #include "common/savefile.h"
 #include "common/translation.h"
 
-#include "mtropolis/mtropolis.h"
-
 #include "gui/saveload.h"
 
+#include "mtropolis/mtropolis.h"
+
 namespace MTropolis {
 
 bool MTropolisEngine::promptSave(ISaveWriter *writer) {


Commit: 1dae5a71ebf41574ee0352aeec32d1e3a78854c9
    https://github.com/scummvm/scummvm/commit/1dae5a71ebf41574ee0352aeec32d1e3a78854c9
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Remove MIDI source allocation/deallocation to fix compile error when INT8_MAX is unavailable

Changed paths:
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h


diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 94f93c5fd4f..dbc58b9ff9e 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -1634,8 +1634,7 @@ StandardPlugIn::StandardPlugIn()
 	, _objRefVarModifierFactory(this)
 	, _midiModifierFactory(this)
 	, _listVarModifierFactory(this)
-	, _sysInfoModifierFactory(this)
-	, _lastAllocatedSourceID(0) {
+	, _sysInfoModifierFactory(this) {
 	_midi.reset(new MultiMidiPlayer());
 }
 
@@ -1664,24 +1663,6 @@ MultiMidiPlayer *StandardPlugIn::getMidi() const {
 	return _midi.get();
 }
 
-int8 StandardPlugIn::allocateMidiSource() {
-	if (_deallocatedSources.size() > 0) {
-		int8 src = _deallocatedSources.back();
-		_deallocatedSources.pop_back();
-		return src;
-	}
-
-	if (_lastAllocatedSourceID == INT8_MAX)
-		error("Ran out of MIDI sources");
-
-	_lastAllocatedSourceID++;
-	return _lastAllocatedSourceID;
-}
-
-void StandardPlugIn::deallocateMidiSource(int8 source) {
-	_deallocatedSources.push_back(source);
-}
-
 } // End of namespace Standard
 
 namespace PlugIns {
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index b95070dcc62..00039be36aa 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -374,9 +374,6 @@ public:
 
 	MultiMidiPlayer *getMidi() const;
 
-	int8 allocateMidiSource();
-	void deallocateMidiSource(int8 source);
-
 private:
 	PlugInModifierFactory<CursorModifier, Data::Standard::CursorModifier> _cursorModifierFactory;
 	PlugInModifierFactory<STransCtModifier, Data::Standard::STransCtModifier> _sTransCtModifierFactory;
@@ -388,9 +385,6 @@ private:
 
 	Common::SharedPtr<MultiMidiPlayer> _midi;
 	StandardPlugInHacks _hacks;
-
-	Common::Array<int8> _deallocatedSources;
-	int8 _lastAllocatedSourceID;
 };
 
 } // End of namespace Standard


Commit: 3baac5a15e74d679e59cec8bce4c30ee1b1200fa
    https://github.com/scummvm/scummvm/commit/3baac5a15e74d679e59cec8bce4c30ee1b1200fa
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Prep for moving some things from runtime to core, add IInterfaceBase to quiet warnings about missing virtual dtor

Changed paths:
  A engines/mtropolis/core.cpp
  A engines/mtropolis/core.h
    engines/mtropolis/debug.h
    engines/mtropolis/module.mk
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/core.cpp b/engines/mtropolis/core.cpp
new file mode 100644
index 00000000000..8814cf03cb0
--- /dev/null
+++ b/engines/mtropolis/core.cpp
@@ -0,0 +1,29 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mtropolis/core.h"
+
+namespace MTropolis {
+
+IInterfaceBase::~IInterfaceBase() {
+}
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/core.h b/engines/mtropolis/core.h
new file mode 100644
index 00000000000..d79dd850b72
--- /dev/null
+++ b/engines/mtropolis/core.h
@@ -0,0 +1,33 @@
+/* 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MTROPOLIS_CORE_H
+#define MTROPOLIS_CORE_H
+
+namespace MTropolis {
+
+struct IInterfaceBase {
+	virtual ~IInterfaceBase();
+};
+
+} // End of namespace MTropolis
+
+#endif /* MTROPOLIS_CORE_H */
diff --git a/engines/mtropolis/debug.h b/engines/mtropolis/debug.h
index 80877f7491b..66ea4c3507c 100644
--- a/engines/mtropolis/debug.h
+++ b/engines/mtropolis/debug.h
@@ -26,6 +26,8 @@
 #include "common/ptr.h"
 #include "common/hashmap.h"
 
+#include "mtropolis/core.h"
+
 #define MTROPOLIS_DEBUG_VTHREAD_STACKS
 
 #define MTROPOLIS_DEBUG_ENABLE
@@ -68,7 +70,7 @@ struct IDebugInspectionReport {
 	virtual void declareLoose(const Common::String &data) = 0;
 };
 
-struct IDebuggable {
+struct IDebuggable : public IInterfaceBase {
 	virtual SupportStatus debugGetSupportStatus() const = 0;
 	virtual const char *debugGetTypeName() const = 0;
 	virtual const Common::String &debugGetName() const = 0;
diff --git a/engines/mtropolis/module.mk b/engines/mtropolis/module.mk
index b5febdf1c2b..aa6ad880d4b 100644
--- a/engines/mtropolis/module.mk
+++ b/engines/mtropolis/module.mk
@@ -3,6 +3,7 @@ MODULE := engines/mtropolis
 MODULE_OBJS = \
 	asset_factory.o \
 	assets.o \
+	core.o \
 	data.o \
 	debug.o \
 	detection.o \
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 87883e1141e..a2186b07534 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -34,6 +34,7 @@
 #include "graphics/pixelformat.h"
 
 #include "mtropolis/actions.h"
+#include "mtropolis/core.h"
 #include "mtropolis/data.h"
 #include "mtropolis/debug.h"
 #include "mtropolis/hacks.h"
@@ -376,7 +377,7 @@ struct ObjectReference {
 	inline ObjectReference() {
 	}
 
-	inline explicit ObjectReference(const Common::WeakPtr<RuntimeObject> object) : object(object) {
+	inline explicit ObjectReference(const Common::WeakPtr<RuntimeObject> objectPtr) : object(objectPtr) {
 	}
 
 	inline bool operator==(const ObjectReference &other) const {
@@ -450,13 +451,13 @@ struct MessageFlags {
 struct DynamicValue;
 struct DynamicList;
 
-struct IDynamicValueReadInterface {
+struct IDynamicValueReadInterface : public IInterfaceBase {
 	virtual MiniscriptInstructionOutcome read(MiniscriptThread *thread, DynamicValue &dest, const void *objectRef, uintptr ptrOrOffset) const = 0;
 	virtual MiniscriptInstructionOutcome readAttrib(MiniscriptThread *thread, DynamicValue &dest, const void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const = 0;
 	virtual MiniscriptInstructionOutcome readAttribIndexed(MiniscriptThread *thread, DynamicValue &dest, const void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const = 0;
 };
 
-struct IDynamicValueWriteInterface {
+struct IDynamicValueWriteInterface : public IInterfaceBase {
 	virtual MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr ptrOrOffset) const = 0;
 	virtual MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) const = 0;
 	virtual MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) const = 0;
@@ -1113,7 +1114,7 @@ struct SegmentDescription {
 	Common::SeekableReadStream *stream;
 };
 
-struct IPlugInModifierRegistrar {
+struct IPlugInModifierRegistrar : public IInterfaceBase {
 	virtual void registerPlugInModifier(const char *name, const Data::IPlugInModifierDataFactory *loader, const IPlugInModifierFactory *factory) = 0;
 	void registerPlugInModifier(const char *name, const IPlugInModifierFactoryAndDataFactory *loaderFactory);
 };
@@ -1756,7 +1757,7 @@ private:
 #endif
 };
 
-struct IModifierContainer {
+struct IModifierContainer : public IInterfaceBase {
 	virtual const Common::Array<Common::SharedPtr<Modifier> > &getModifiers() const = 0;
 	virtual void appendModifier(const Common::SharedPtr<Modifier> &modifier) = 0;
 };
@@ -1823,14 +1824,14 @@ private:
 	Common::WeakPtr<RuntimeObject> _source;
 };
 
-struct IStructuralReferenceVisitor {
+struct IStructuralReferenceVisitor : public IInterfaceBase {
 	virtual void visitChildStructuralRef(Common::SharedPtr<Structural> &structural) = 0;
 	virtual void visitChildModifierRef(Common::SharedPtr<Modifier> &modifier) = 0;
 	virtual void visitWeakStructuralRef(Common::WeakPtr<Structural> &structural) = 0;
 	virtual void visitWeakModifierRef(Common::WeakPtr<Modifier> &modifier) = 0;
 };
 
-struct IMessageConsumer {
+struct IMessageConsumer : public IInterfaceBase {
 	// These should only be implemented as direct responses - child traversal is handled by the message propagation process
 	virtual bool respondsToEvent(const Event &evt) const = 0;
 	virtual VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) = 0;
@@ -2013,7 +2014,7 @@ private:
 	Common::HashMap<Common::String, const IPlugInModifierFactory *> _factoryRegistry;
 };
 
-struct IPlayMediaSignalReceiver {
+struct IPlayMediaSignalReceiver : public IInterfaceBase {
 	virtual void playMedia(Runtime *runtime, Project *project) = 0;
 };
 
@@ -2030,7 +2031,7 @@ private:
 	Common::Array<IPlayMediaSignalReceiver *> _receivers;
 };
 
-struct ISegmentUnloadSignalReceiver {
+struct ISegmentUnloadSignalReceiver : public IInterfaceBase {
 	virtual void onSegmentUnloaded(int segmentIndex) = 0;
 };
 
@@ -2049,7 +2050,7 @@ private:
 	Common::Array<ISegmentUnloadSignalReceiver *> _receivers;
 };
 
-struct IKeyboardEventReceiver {
+struct IKeyboardEventReceiver : public IInterfaceBase {
 	virtual void onKeyboardEvent(Runtime *runtime, Common::EventType evtType, bool repeat, const Common::KeyState &keyEvt) = 0;
 };
 
@@ -2067,7 +2068,7 @@ private:
 	Common::SharedPtr<KeyboardEventSignaller> _signaller;
 };
 
-struct ICollider {
+struct ICollider : public IInterfaceBase {
 	virtual void getCollisionProperties(Modifier *&modifier, bool &collideInFront, bool &collideBehind, bool &excludeParents) const = 0;
 	virtual void triggerCollision(Runtime *runtime, Structural *collidingElement, bool wasInContact, bool isInContact, bool &outShouldStop) = 0;
 };


Commit: b52b80c8fa3671b233efa818933dd2e689086211
    https://github.com/scummvm/scummvm/commit/b52b80c8fa3671b233efa818933dd2e689086211
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Warning/error cleanup

Changed paths:
    engines/mtropolis/asset_factory.cpp
    engines/mtropolis/asset_factory.h
    engines/mtropolis/core.h
    engines/mtropolis/data.h
    engines/mtropolis/debug.h
    engines/mtropolis/elements.cpp
    engines/mtropolis/miniscript.cpp
    engines/mtropolis/modifier_factory.h
    engines/mtropolis/render.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h
    engines/mtropolis/saveload.h


diff --git a/engines/mtropolis/asset_factory.cpp b/engines/mtropolis/asset_factory.cpp
index 1441be568e9..08760613693 100644
--- a/engines/mtropolis/asset_factory.cpp
+++ b/engines/mtropolis/asset_factory.cpp
@@ -24,7 +24,7 @@
 
 namespace MTropolis {
 
-AssetLoaderContext::AssetLoaderContext(size_t streamIndex) : streamIndex(streamIndex) {
+AssetLoaderContext::AssetLoaderContext(size_t pStreamIndex) : streamIndex(pStreamIndex) {
 }
 
 template<typename TAsset, typename TAssetData>
diff --git a/engines/mtropolis/asset_factory.h b/engines/mtropolis/asset_factory.h
index 9b3b0a96814..7466290fb3d 100644
--- a/engines/mtropolis/asset_factory.h
+++ b/engines/mtropolis/asset_factory.h
@@ -22,6 +22,7 @@
 #ifndef MTROPOLIS_ASSET_FACTORY_H
 #define MTROPOLIS_ASSET_FACTORY_H
 
+#include "mtropolis/core.h"
 #include "mtropolis/data.h"
 #include "mtropolis/runtime.h"
 
@@ -33,7 +34,7 @@ struct AssetLoaderContext {
 	size_t streamIndex;
 };
 
-struct IAssetFactory {
+struct IAssetFactory : public IInterfaceBase {
 	virtual Common::SharedPtr<Asset> createAsset(AssetLoaderContext &context, const Data::DataObject &dataObject) const = 0;
 };
 
diff --git a/engines/mtropolis/core.h b/engines/mtropolis/core.h
index d79dd850b72..41ed0e1ab2c 100644
--- a/engines/mtropolis/core.h
+++ b/engines/mtropolis/core.h
@@ -22,6 +22,12 @@
 #ifndef MTROPOLIS_CORE_H
 #define MTROPOLIS_CORE_H
 
+#include "common/scummsys.h"
+
+#ifndef INT32_MIN
+#define INT32_MIN (-((int32)(0x7fffffff)) - 1)
+#endif
+
 namespace MTropolis {
 
 struct IInterfaceBase {
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index 1c391b443b7..721859774d6 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -31,6 +31,8 @@
 #include "common/rect.h"
 #include "common/xpfloat.h"
 
+#include "mtropolis/core.h"
+
 // This contains defs related to parsing of mTropolis stored data into structured data objects.
 // This is separated from asset construction for a number of reasons, mainly that data parsing has
 // several quirky parses, and there are a lot of fields where, due to platform-specific byte
@@ -1773,7 +1775,7 @@ protected:
 	DataReadErrorCode load(DataReader &reader) override;
 };
 
-struct IPlugInModifierDataFactory {
+struct IPlugInModifierDataFactory : public IInterfaceBase {
 	virtual Common::SharedPtr<Data::PlugInModifierData> createModifierData() const = 0;
 	virtual PlugIn &getPlugIn() const = 0;
 };
diff --git a/engines/mtropolis/debug.h b/engines/mtropolis/debug.h
index 66ea4c3507c..96d3920ded5 100644
--- a/engines/mtropolis/debug.h
+++ b/engines/mtropolis/debug.h
@@ -56,7 +56,7 @@ enum SupportStatus {
 	kSupportStatusDone,		// Not necessarily "done" but "done" enough that unsupported cases will trigger errors
 };
 
-struct IDebugInspectionReport {
+struct IDebugInspectionReport : public IInterfaceBase {
 	// Attempts to declare a row with static contents.  If this returns true, then declareStaticContents must be called.
 	virtual bool declareStatic(const char *name) = 0;
 
diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index dee65e5f5cd..e621ab2b8d6 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -120,7 +120,7 @@ int AudioPlayer::readBuffer(int16 *buffer, const int numSamplesTimesChannelCount
 		// TODO: Support more formats
 		if (_metadata->bitsPerSample == 8 && _metadata->encoding == AudioMetadata::kEncodingUncompressed) {
 			const uint8 *inSamples = static_cast<const uint8 *>(_audio->getData()) + _currentPos * numChannels;
-			for (int i = 0; i < numSampleValues; i++)
+			for (size_t i = 0; i < numSampleValues; i++)
 				buffer[i] = (inSamples[i] - 0x80) * 256;
 		} else if (_metadata->bitsPerSample == 16 && _metadata->encoding == AudioMetadata::kEncodingUncompressed) {
 			const int16 *inSamples = static_cast<const int16 *>(_audio->getData()) + _currentPos * numChannels;
diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp
index aee2a546926..11f7e889fac 100644
--- a/engines/mtropolis/miniscript.cpp
+++ b/engines/mtropolis/miniscript.cpp
@@ -241,7 +241,7 @@ bool MiniscriptInstructionLoader<MiniscriptInstructions::PushString>::loadInstru
 	return true;
 }
 
-struct IMiniscriptInstructionFactory {
+struct IMiniscriptInstructionFactory : public IInterfaceBase {
 	virtual bool create(void *dest, uint32 instrFlags, Data::DataReader &instrDataReader, MiniscriptInstruction *&outMiniscriptInstructionPtr) const = 0;
 	virtual void getSizeAndAlignment(size_t &outSize, size_t &outAlignment) const = 0;
 };
@@ -768,6 +768,9 @@ MiniscriptInstructionOutcome UnorderedCompareInstruction::execute(MiniscriptThre
 				case DynamicValueTypes::kBoolean:
 					isEqual = ((rs.getBool() ? 1.0 : 0.0) == lsDest.getFloat());
 					break;
+				default:
+					isEqual = false;
+					break;
 				}
 			}
 		} break;
@@ -785,6 +788,9 @@ MiniscriptInstructionOutcome UnorderedCompareInstruction::execute(MiniscriptThre
 			case DynamicValueTypes::kBoolean:
 				isEqual = ((rs.getBool() ? 1 : 0) == lsDest.getInt());
 				break;
+			default:
+				isEqual = false;
+				break;
 			}
 		} break;
 	case DynamicValueTypes::kLabel: {
diff --git a/engines/mtropolis/modifier_factory.h b/engines/mtropolis/modifier_factory.h
index fa96d20ad9b..1e9e708b3e2 100644
--- a/engines/mtropolis/modifier_factory.h
+++ b/engines/mtropolis/modifier_factory.h
@@ -33,11 +33,11 @@ struct ModifierLoaderContext {
 	ChildLoaderStack *childLoaderStack;
 };
 
-struct IModifierFactory {
+struct IModifierFactory : public IInterfaceBase {
 	virtual Common::SharedPtr<Modifier> createModifier(ModifierLoaderContext &context, const Data::DataObject &dataObject) const = 0;
 };
 
-struct IPlugInModifierFactory {
+struct IPlugInModifierFactory : public IInterfaceBase {
 	virtual Common::SharedPtr<Modifier> createModifier(ModifierLoaderContext &context, const Data::PlugInModifier &plugInModifierData) const = 0;
 };
 
diff --git a/engines/mtropolis/render.h b/engines/mtropolis/render.h
index 3bb45f6bd86..33496298582 100644
--- a/engines/mtropolis/render.h
+++ b/engines/mtropolis/render.h
@@ -88,7 +88,7 @@ struct WindowParameters {
 class Window {
 public:
 	explicit Window(const WindowParameters &windowParams);
-	~Window();
+	virtual ~Window();
 
 	int32 getX() const;
 	int32 getY() const;
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 1c3c55e1216..adffad0ac65 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -4221,7 +4221,7 @@ bool Runtime::isModifierMouseInteractive(Modifier *modifier, MouseInteractivityT
 	return false;
 }
 
-void Runtime::recursiveFindMouseCollision(Structural *&bestResult, int &bestLayer, int &bestStackHeight, bool &bestDirect, Structural *candidate, int stackHeight, int32 relativeX, int32 relativeY, MouseInteractivityTestType testType) {
+void Runtime::recursiveFindMouseCollision(Structural *&bestResult, int32 &bestLayer, int32 &bestStackHeight, bool &bestDirect, Structural *candidate, int32 stackHeight, int32 relativeX, int32 relativeY, MouseInteractivityTestType testType) {
 	int32 childRelativeX = relativeX;
 	int32 childRelativeY = relativeY;
 	if (candidate->isElement()) {
@@ -4624,8 +4624,8 @@ VThreadState Runtime::updateMouseStateTask(const UpdateMouseStateTaskData &data)
 	if (data.mouseDown) {
 		// Mouse down
 		Structural *tracked = nullptr;
-		int bestSceneStack = INT_MIN;
-		int bestLayer = INT_MIN;
+		int32 bestSceneStack = INT32_MIN;
+		int32 bestLayer = INT32_MIN;
 		bool bestDirect = false;
 
 		for (size_t ri = 0; ri < _sceneStack.size(); ri++) {
@@ -4699,8 +4699,8 @@ VThreadState Runtime::updateMousePositionTask(const UpdateMousePositionTaskData
 	// a Mouse Up Inside event is sent when the button is released.
 
 	Structural *collisionItem = nullptr;
-	int bestSceneStack = INT_MIN;
-	int bestLayer = INT_MIN;
+	int32 bestSceneStack = INT32_MIN;
+	int32 bestLayer = INT32_MIN;
 	bool bestDirect = false;
 
 	for (size_t ri = 0; ri < _sceneStack.size(); ri++) {
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index a2186b07534..1e81fed8bd5 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -1635,7 +1635,7 @@ private:
 
 	static bool isStructuralMouseInteractive(Structural *structural, MouseInteractivityTestType testType);
 	static bool isModifierMouseInteractive(Modifier *modifier, MouseInteractivityTestType testType);
-	static void recursiveFindMouseCollision(Structural *&bestResult, int &bestLayer, int &bestStackHeight, bool &bestDirect, Structural *candidate, int stackHeight, int32 relativeX, int32 relativeY, MouseInteractivityTestType testType);
+	static void recursiveFindMouseCollision(Structural *&bestResult, int32 &bestLayer, int32 &bestStackHeight, bool &bestDirect, Structural *candidate, int32 stackHeight, int32 relativeX, int32 relativeY, MouseInteractivityTestType testType);
 
 	void queueEventAsLowLevelSceneStateTransitionAction(const Event &evt, Structural *root, bool cascade, bool relay);
 
diff --git a/engines/mtropolis/saveload.h b/engines/mtropolis/saveload.h
index f8867e3510f..47159fa97a9 100644
--- a/engines/mtropolis/saveload.h
+++ b/engines/mtropolis/saveload.h
@@ -22,6 +22,8 @@
 #ifndef MTROPOLIS_SAVELOAD_H
 #define MTROPOLIS_SAVELOAD_H
 
+#include "mtropolis/core.h"
+
 namespace Common {
 
 class ReadStream;
@@ -33,19 +35,19 @@ namespace MTropolis {
 
 class RuntimeObject;
 
-struct ISaveWriter {
+struct ISaveWriter : public IInterfaceBase {
 	virtual bool writeSave(Common::WriteStream *stream) = 0;
 };
 
-struct ISaveReader {
+struct ISaveReader : public IInterfaceBase {
 	virtual bool readSave(Common::ReadStream *stream) = 0;
 };
 
-struct ISaveUIProvider {
+struct ISaveUIProvider : public IInterfaceBase {
 	virtual bool promptSave(ISaveWriter *writer) = 0;
 };
 
-struct ILoadUIProvider {
+struct ILoadUIProvider : public IInterfaceBase {
 	virtual bool promptLoad(ISaveReader *reader) = 0;
 };
 


Commit: eb450cf5c4c2864fb1bca8272fae5e8fb8cfcb0e
    https://github.com/scummvm/scummvm/commit/eb450cf5c4c2864fb1bca8272fae5e8fb8cfcb0e
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Warning/error cleanup, fix Xcode build

Changed paths:
    engines/mtropolis/assets.cpp
    engines/mtropolis/assets.h
    engines/mtropolis/element_factory.h


diff --git a/engines/mtropolis/assets.cpp b/engines/mtropolis/assets.cpp
index 893754d18df..a9d62472261 100644
--- a/engines/mtropolis/assets.cpp
+++ b/engines/mtropolis/assets.cpp
@@ -169,7 +169,7 @@ void CachedMToon::decompressFrames(const Common::Array<uint8> &data) {
 }
 
 template<class TNumber, uint32 TLiteralMask, uint32 TTransparentRowSkipMask>
-static bool decompressMToonRLE(const CachedMToon::RleFrame &frame, const Common::Array<TNumber> &coefsArray, Graphics::Surface &surface, bool isBottomUp) {
+bool CachedMToon::decompressMToonRLE(const RleFrame &frame, const Common::Array<TNumber> &coefsArray, Graphics::Surface &surface, bool isBottomUp) {
 	assert(sizeof(TNumber) == surface.format.bytesPerPixel);
 
 	size_t size = coefsArray.size();
diff --git a/engines/mtropolis/assets.h b/engines/mtropolis/assets.h
index 07285adb400..e8401b58f18 100644
--- a/engines/mtropolis/assets.h
+++ b/engines/mtropolis/assets.h
@@ -136,6 +136,9 @@ private:
 	template<class TSrcNumber, uint32 TSrcLiteralMask, uint32 TSrcTransparentSkipMask, class TDestNumber, uint32 TDestLiteralMask, uint32 TDestTransparentSkipMask>
 	void rleReformat(RleFrame &frame, const Common::Array<TSrcNumber> &srcData, const Graphics::PixelFormat &srcFormatRef, Common::Array<TDestNumber> &destData, const Graphics::PixelFormat &destFormatRef);
 
+	template<class TNumber, uint32 TLiteralMask, uint32 TTransparentRowSkipMask>
+	static bool decompressMToonRLE(const RleFrame &frame, const Common::Array<TNumber> &coefsArray, Graphics::Surface &surface, bool isBottomUp);
+
 	Common::Array<RleFrame> _rleData;
 	bool _isRLETemporalCompressed;
 
diff --git a/engines/mtropolis/element_factory.h b/engines/mtropolis/element_factory.h
index dae3329c978..86d351634e0 100644
--- a/engines/mtropolis/element_factory.h
+++ b/engines/mtropolis/element_factory.h
@@ -34,7 +34,7 @@ struct ElementLoaderContext {
 	size_t streamIndex;
 };
 
-struct IElementFactory {
+struct IElementFactory : public IInterfaceBase {
 	virtual Common::SharedPtr<Element> createElement(ElementLoaderContext &context, const Data::DataObject &dataObject) const = 0;
 };
 


Commit: ca322dbfbdb6b6ab0355cfad61add7198a57a6ac
    https://github.com/scummvm/scummvm/commit/ca322dbfbdb6b6ab0355cfad61add7198a57a6ac
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Add saveload.cpp to POTFILES

Changed paths:
    engines/mtropolis/POTFILES


diff --git a/engines/mtropolis/POTFILES b/engines/mtropolis/POTFILES
index 2162ac57fd0..1d78dd6a65c 100644
--- a/engines/mtropolis/POTFILES
+++ b/engines/mtropolis/POTFILES
@@ -1,2 +1,3 @@
 engines/mtropolis/detection.cpp
 engines/mtropolis/mtropolis.cpp
+engines/mtropolis/saveload.cpp


Commit: 0365b9c76d707e2cdc667e6078696d7afeb232c0
    https://github.com/scummvm/scummvm/commit/0365b9c76d707e2cdc667e6078696d7afeb232c0
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Cleanup

Changed paths:
  R engines/mtropolis/console.cpp
  R engines/mtropolis/console.h
    engines/mtropolis/configure.engine
    engines/mtropolis/data.cpp
    engines/mtropolis/data.h
    engines/mtropolis/elements.h
    engines/mtropolis/mtropolis.cpp
    engines/mtropolis/runtime.cpp


diff --git a/engines/mtropolis/configure.engine b/engines/mtropolis/configure.engine
index df678b51cdb..d27a0c44f6a 100644
--- a/engines/mtropolis/configure.engine
+++ b/engines/mtropolis/configure.engine
@@ -1,3 +1,3 @@
 # This file is included from the main "configure" script
 # add_engine [name] [desc] [build-by-default] [subengines] [base games] [deps]
-add_engine mtropolis "mTropolis" no
+add_engine mtropolis "mTropolis" no "" "" "16bit highres"
diff --git a/engines/mtropolis/console.cpp b/engines/mtropolis/console.cpp
deleted file mode 100644
index b76e257701f..00000000000
--- a/engines/mtropolis/console.cpp
+++ /dev/null
@@ -1,32 +0,0 @@
-/* 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 <http://www.gnu.org/licenses/>.
- *
- */
-
-#include "mtropolis/console.h"
-
-namespace MTropolis {
-
-Console::Console(MTropolisEngine *engine) : _engine(engine) {
-}
-
-Console::~Console() {
-}
-
-} // End of namespace MTropolis
diff --git a/engines/mtropolis/console.h b/engines/mtropolis/console.h
deleted file mode 100644
index 161a8aa110e..00000000000
--- a/engines/mtropolis/console.h
+++ /dev/null
@@ -1,41 +0,0 @@
-/* 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 <http://www.gnu.org/licenses/>.
- *
- */
-
-#ifndef MTROPOLIS_CONSOLE_H
-#define MTROPOLIS_CONSOLE_H
-
-#include "gui/debugger.h"
-
-namespace MTropolis {
-
-class MTropolisEngine;
-
-class Console : public GUI::Debugger {
-public:
-	explicit Console(MTropolisEngine *engine);
-	~Console() override;
-
-private:
-	MTropolisEngine *_engine;
-};
-}
-
-#endif
diff --git a/engines/mtropolis/data.cpp b/engines/mtropolis/data.cpp
index c7f53e3eb78..c04b7cb92c0 100644
--- a/engines/mtropolis/data.cpp
+++ b/engines/mtropolis/data.cpp
@@ -365,8 +365,7 @@ bool ColorRGB16::load(DataReader& reader) {
 		blue = bgra[0] * 0x101;
 
 		return true;
-	}
-	else
+	} else
 		return false;
 }
 
@@ -998,7 +997,7 @@ bool TypicalModifierHeader::load(DataReader& reader) {
 }
 
 DataReadErrorCode MiniscriptModifier::load(DataReader &reader) {
-	if (_revision != 0x3eb)
+	if (_revision != 1003)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader) || !enableWhen.load(reader) || !reader.readBytes(unknown6) || !reader.readU8(unknown7) || !program.load(reader))
@@ -1008,7 +1007,7 @@ DataReadErrorCode MiniscriptModifier::load(DataReader &reader) {
 }
 
 DataReadErrorCode SaveAndRestoreModifier::load(DataReader &reader) {
-	if (_revision != 0x3e9)
+	if (_revision != 1001)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !saveWhen.load(reader) || !restoreWhen.load(reader)
@@ -1022,7 +1021,7 @@ DataReadErrorCode SaveAndRestoreModifier::load(DataReader &reader) {
 }
 
 DataReadErrorCode MessengerModifier::load(DataReader &reader) {
-	if (_revision != 0x3ea)
+	if (_revision != 1002)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader))
@@ -1040,7 +1039,7 @@ DataReadErrorCode MessengerModifier::load(DataReader &reader) {
 }
 
 DataReadErrorCode SetModifier::load(DataReader &reader) {
-	if (_revision != 0x3e8)
+	if (_revision != 1000)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !executeWhen.load(reader)
@@ -1073,7 +1072,7 @@ DataReadErrorCode AliasModifier::load(DataReader& reader) {
 }
 
 DataReadErrorCode ChangeSceneModifier::load(DataReader &reader) {
-	if (_revision != 0x3e9)
+	if (_revision != 1001)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader) || !reader.readU32(changeSceneFlags) || !executeWhen.load(reader)
@@ -1084,7 +1083,7 @@ DataReadErrorCode ChangeSceneModifier::load(DataReader &reader) {
 }
 
 DataReadErrorCode SoundEffectModifier::load(DataReader &reader) {
-	if (_revision != 0x3e8)
+	if (_revision != 1000)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !executeWhen.load(reader)
@@ -1095,8 +1094,7 @@ DataReadErrorCode SoundEffectModifier::load(DataReader &reader) {
 	return kDataReadErrorNone;
 }
 
-bool PathMotionModifierV2::PointDef::load(DataReader &reader)
-{
+bool PathMotionModifierV2::PointDef::load(DataReader &reader) {
 	if (!point.load(reader)
 		|| !reader.readU32(frame)
 		|| !reader.readU32(frameFlags)
@@ -1116,7 +1114,7 @@ bool PathMotionModifierV2::PointDef::load(DataReader &reader)
 }
 
 DataReadErrorCode PathMotionModifierV2::load(DataReader &reader) {
-	if (_revision != 0x3e9)
+	if (_revision != 1001)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader)
@@ -1142,7 +1140,7 @@ DataReadErrorCode PathMotionModifierV2::load(DataReader &reader) {
 }
 
 DataReadErrorCode DragMotionModifier::load(DataReader &reader) {
-	if (_revision != 0x3e8)
+	if (_revision != 1000)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader))
@@ -1175,7 +1173,7 @@ DataReadErrorCode DragMotionModifier::load(DataReader &reader) {
 }
 
 DataReadErrorCode VectorMotionModifier::load(DataReader &reader) {
-	if (_revision != 0x3e9)
+	if (_revision != 1001)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader))
@@ -1191,7 +1189,7 @@ DataReadErrorCode VectorMotionModifier::load(DataReader &reader) {
 }
 
 DataReadErrorCode SceneTransitionModifier::load(DataReader &reader) {
-	if (_revision != 0x3e9)
+	if (_revision != 1001)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader))
@@ -1206,7 +1204,7 @@ DataReadErrorCode SceneTransitionModifier::load(DataReader &reader) {
 }
 
 DataReadErrorCode ElementTransitionModifier::load(DataReader &reader) {
-	if (_revision != 0x3e9)
+	if (_revision != 1001)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader))
@@ -1221,7 +1219,7 @@ DataReadErrorCode ElementTransitionModifier::load(DataReader &reader) {
 }
 
 DataReadErrorCode IfMessengerModifier::load(DataReader &reader) {
-	if (_revision != 0x3ea)
+	if (_revision != 1002)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader) || !reader.readU32(messageFlags) || !when.load(reader) || !send.load(reader)
@@ -1239,7 +1237,7 @@ DataReadErrorCode IfMessengerModifier::load(DataReader &reader) {
 }
 
 DataReadErrorCode TimerMessengerModifier::load(DataReader &reader) {
-	if (_revision != 0x3ea)
+	if (_revision != 1002)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader))
@@ -1258,7 +1256,7 @@ DataReadErrorCode TimerMessengerModifier::load(DataReader &reader) {
 }
 
 DataReadErrorCode BoundaryDetectionMessengerModifier::load(DataReader &reader) {
-	if (_revision != 0x3ea)
+	if (_revision != 1002)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader))
@@ -1274,7 +1272,7 @@ DataReadErrorCode BoundaryDetectionMessengerModifier::load(DataReader &reader) {
 }
 
 DataReadErrorCode CollisionDetectionMessengerModifier::load(DataReader &reader) {
-	if (_revision != 0x3ea)
+	if (_revision != 1002)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader))
@@ -1290,7 +1288,7 @@ DataReadErrorCode CollisionDetectionMessengerModifier::load(DataReader &reader)
 }
 
 DataReadErrorCode KeyboardMessengerModifier::load(DataReader &reader) {
-	if (_revision != 0x3eb)
+	if (_revision != 1003)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader) || !reader.readU32(messageFlagsAndKeyStates) || !reader.readU16(unknown2)
@@ -1307,7 +1305,7 @@ DataReadErrorCode KeyboardMessengerModifier::load(DataReader &reader) {
 }
 
 DataReadErrorCode TextStyleModifier::load(DataReader &reader) {
-	if (_revision != 0x3e8)
+	if (_revision != 1000)
 		return kDataReadErrorUnsupportedRevision;
 	
 	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !reader.readU16(macFontID)
@@ -1321,7 +1319,7 @@ DataReadErrorCode TextStyleModifier::load(DataReader &reader) {
 }
 
 DataReadErrorCode GraphicModifier::load(DataReader &reader) {
-	if (_revision != 0x3e9)
+	if (_revision != 1001)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader) || !reader.readU16(unknown1) || !applyWhen.load(reader)
@@ -1373,7 +1371,7 @@ DataReadErrorCode CompoundVariableModifier::load(DataReader &reader) {
 }
 
 DataReadErrorCode BooleanVariableModifier::load(DataReader &reader) {
-	if (_revision != 0x3e8)
+	if (_revision != 1000)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader) || !reader.readU8(value) || !reader.readU8(unknown5))
@@ -1383,7 +1381,7 @@ DataReadErrorCode BooleanVariableModifier::load(DataReader &reader) {
 }
 
 DataReadErrorCode IntegerVariableModifier::load(DataReader &reader) {
-	if (_revision != 0x3e8)
+	if (_revision != 1000)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !reader.readS32(value))
@@ -1393,7 +1391,7 @@ DataReadErrorCode IntegerVariableModifier::load(DataReader &reader) {
 }
 
 DataReadErrorCode IntegerRangeVariableModifier::load(DataReader &reader) {
-	if (_revision != 0x3e8)
+	if (_revision != 1000)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !range.load(reader))
@@ -1403,7 +1401,7 @@ DataReadErrorCode IntegerRangeVariableModifier::load(DataReader &reader) {
 }
 
 DataReadErrorCode VectorVariableModifier::load(DataReader &reader) {
-	if (_revision != 0x3e8)
+	if (_revision != 1000)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !this->vector.load(reader))
@@ -1413,7 +1411,7 @@ DataReadErrorCode VectorVariableModifier::load(DataReader &reader) {
 }
 
 DataReadErrorCode PointVariableModifier::load(DataReader &reader) {
-	if (_revision != 0x3e8)
+	if (_revision != 1000)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader) || !reader.readBytes(unknown5) || !value.load(reader))
@@ -1423,7 +1421,7 @@ DataReadErrorCode PointVariableModifier::load(DataReader &reader) {
 }
 
 DataReadErrorCode FloatingPointVariableModifier::load(DataReader &reader) {
-	if (_revision != 0x3e8)
+	if (_revision != 1000)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader) || !reader.readBytes(unknown1) || !reader.readPlatformFloat(value))
@@ -1433,7 +1431,7 @@ DataReadErrorCode FloatingPointVariableModifier::load(DataReader &reader) {
 }
 
 DataReadErrorCode StringVariableModifier::load(DataReader &reader) {
-	if (_revision != 0x3e8)
+	if (_revision != 1000)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!modHeader.load(reader) || !reader.readU32(lengthOfString) || !reader.readBytes(unknown1) || !reader.readTerminatedStr(value, lengthOfString))
@@ -1446,7 +1444,7 @@ PlugInModifierData::~PlugInModifierData() {
 }
 
 DataReadErrorCode PlugInModifier::load(DataReader &reader) {
-	if (_revision != 0x03e9)
+	if (_revision != 1001)
 		return kDataReadErrorUnsupportedRevision;
 
 	if (!reader.readU32(modifierFlags) || !reader.readU32(codedSize) || !reader.read(modifierName, 16)
@@ -1613,8 +1611,7 @@ DataReadErrorCode AudioAsset::load(DataReader &reader) {
 			|| !reader.readU8(encoding1) || !reader.readU8(channels) || !reader.readBytes(codedDuration)
 			|| !reader.readBytes(platform.win.unknown11) || !reader.readU16(sampleRate2) || !reader.readBytes(platform.win.unknown12_1))
 			return kDataReadErrorReadFailed;
-	}
-	else
+	} else
 		return kDataReadErrorUnrecognized;
 
 	if (!reader.readU32(cuePointDataSize) || !reader.readU16(numCuePoints) || !reader.readBytes(unknown14)
@@ -1625,8 +1622,7 @@ DataReadErrorCode AudioAsset::load(DataReader &reader) {
 		return kDataReadErrorUnrecognized;
 
 	cuePoints.resize(numCuePoints);
-	for (size_t i = 0; i < numCuePoints; i++)
-	{
+	for (size_t i = 0; i < numCuePoints; i++) {
 		CuePoint& cuePoint = cuePoints[i];
 		if (!reader.readBytes(cuePoint.unknown13) || !reader.readU32(cuePoint.unknown14) || !reader.readU32(cuePoint.position)
 			|| !reader.readU32(cuePoint.cuePointID))
diff --git a/engines/mtropolis/data.h b/engines/mtropolis/data.h
index 721859774d6..cbe4c8a2887 100644
--- a/engines/mtropolis/data.h
+++ b/engines/mtropolis/data.h
@@ -77,76 +77,76 @@ enum TextAlignmentCode {
 namespace DataObjectTypes {
 
 enum DataObjectType {
-	kUnknown                             = 0,
-
-	kProjectLabelMap                     = 0x22,
-	kProjectCatalog                      = 0x3e8,
-	kStreamHeader                        = 0x3e9,
-	kProjectHeader                       = 0x3ea,
-	kPresentationSettings                = 0x3ec,
-
-	kAssetCatalog                        = 0xd,
-    kGlobalObjectInfo                    = 0x17,
-	kUnknown19                           = 0x19,
-
-	kProjectStructuralDef                = 0x2,
-	kSectionStructuralDef                = 0x3,
-	kSubsectionStructuralDef             = 0x21,
-
-	kGraphicElement                      = 0x8,
-	kMovieElement                        = 0x5,
-	kMToonElement                        = 0x6,		// NYI
-	kImageElement                        = 0x7,
-	kSoundElement                        = 0xa,
-	kTextLabelElement                    = 0x15,
-
-	kAliasModifier                       = 0x27,
-	kChangeSceneModifier                 = 0x136,
-	kReturnModifier                      = 0x140,	// NYI
-	kSoundEffectModifier                 = 0x1a4,
-	kDragMotionModifier                  = 0x208,
-	kPathMotionModifierV1                = 0x21c,	// NYI - Obsolete version
-	kPathMotionModifierV2                = 0x21b,
-	kVectorMotionModifier                = 0x226,
-	kSceneTransitionModifier             = 0x26c,
-	kElementTransitionModifier           = 0x276,
-	kSharedSceneModifier                 = 0x29a,	// NYI
-	kIfMessengerModifier                 = 0x2bc,
-	kBehaviorModifier                    = 0x2c6,
-	kMessengerModifier                   = 0x2da,
-	kSetModifier                         = 0x2df,
-	kTimerMessengerModifier              = 0x2e4,
-	kCollisionDetectionMessengerModifier = 0x2ee,
-	kBoundaryDetectionMessengerModifier  = 0x2f8,
-	kKeyboardMessengerModifier           = 0x302,
-	kTextStyleModifier                   = 0x32a,
-	kGraphicModifier                     = 0x334,
-	kImageEffectModifier                 = 0x384,	// NYI
-	kMiniscriptModifier                  = 0x3c0,
-	kCursorModifierV1                    = 0x3ca,	// NYI - Obsolete version
-	kGradientModifier                    = 0x4b0,	// NYI
-	kColorTableModifier                  = 0x4c4,	// NYI
-	kSaveAndRestoreModifier              = 0x4d8,
-
-	kCompoundVariableModifier            = 0x2c7,
-	kBooleanVariableModifier             = 0x321,
-	kIntegerVariableModifier             = 0x322,
-	kIntegerRangeVariableModifier        = 0x324,
-	kVectorVariableModifier              = 0x327,
-	kPointVariableModifier               = 0x326,
-	kFloatingPointVariableModifier       = 0x328,
-	kStringVariableModifier              = 0x329,
-	kDebris                              = 0xfffffffe,	// Deleted modifier in alias list
-	kPlugInModifier                      = 0xffffffff,
-
-	kMovieAsset                          = 0x10,	// NYI
-	kAudioAsset                          = 0x11,
-	kColorTableAsset                     = 0x1e,
-	kImageAsset                          = 0xe,		// NYI
-	kMToonAsset                          = 0xf,		// NYI
-	kTextAsset                           = 0x1f,	// NYI
-
-	kAssetDataChunk                      = 0xffff,
+	kUnknown								= 0,
+
+	kProjectLabelMap						= 0x22,
+	kProjectCatalog							= 0x3e8,
+	kStreamHeader							= 0x3e9,
+	kProjectHeader							= 0x3ea,
+	kPresentationSettings					= 0x3ec,
+
+	kAssetCatalog							= 0xd,
+	kGlobalObjectInfo						= 0x17,
+	kUnknown19								= 0x19,
+
+	kProjectStructuralDef					= 0x2,
+	kSectionStructuralDef					= 0x3,
+	kSubsectionStructuralDef				= 0x21,
+
+	kGraphicElement							= 0x8,
+	kMovieElement							= 0x5,
+	kMToonElement							= 0x6,		// NYI
+	kImageElement							= 0x7,
+	kSoundElement							= 0xa,
+	kTextLabelElement						= 0x15,
+
+	kAliasModifier							= 0x27,
+	kChangeSceneModifier					= 0x136,
+	kReturnModifier							= 0x140,	// NYI
+	kSoundEffectModifier					= 0x1a4,
+	kDragMotionModifier						= 0x208,
+	kPathMotionModifierV1					= 0x21c,	// NYI - Obsolete version
+	kPathMotionModifierV2					= 0x21b,
+	kVectorMotionModifier					= 0x226,
+	kSceneTransitionModifier				= 0x26c,
+	kElementTransitionModifier				= 0x276,
+	kSharedSceneModifier					= 0x29a,	// NYI
+	kIfMessengerModifier					= 0x2bc,
+	kBehaviorModifier						= 0x2c6,
+	kMessengerModifier						= 0x2da,
+	kSetModifier							= 0x2df,
+	kTimerMessengerModifier					= 0x2e4,
+	kCollisionDetectionMessengerModifier	= 0x2ee,
+	kBoundaryDetectionMessengerModifier		= 0x2f8,
+	kKeyboardMessengerModifier				= 0x302,
+	kTextStyleModifier						= 0x32a,
+	kGraphicModifier						= 0x334,
+	kImageEffectModifier					= 0x384,	// NYI
+	kMiniscriptModifier						= 0x3c0,
+	kCursorModifierV1						= 0x3ca,	// NYI - Obsolete version
+	kGradientModifier						= 0x4b0,	// NYI
+	kColorTableModifier						= 0x4c4,	// NYI
+	kSaveAndRestoreModifier					= 0x4d8,
+
+	kCompoundVariableModifier				= 0x2c7,
+	kBooleanVariableModifier				= 0x321,
+	kIntegerVariableModifier				= 0x322,
+	kIntegerRangeVariableModifier			= 0x324,
+	kVectorVariableModifier					= 0x327,
+	kPointVariableModifier					= 0x326,
+	kFloatingPointVariableModifier			= 0x328,
+	kStringVariableModifier					= 0x329,
+	kDebris									= 0xfffffffe,	// Deleted modifier in alias list
+	kPlugInModifier							= 0xffffffff,
+
+	kMovieAsset								= 0x10,
+	kAudioAsset								= 0x11,
+	kColorTableAsset						= 0x1e,
+	kImageAsset								= 0xe,
+	kMToonAsset								= 0xf,
+	kTextAsset								= 0x1f,
+
+	kAssetDataChunk							= 0xffff,
 };
 
 bool isValidSceneRootElement(DataObjectType type);
@@ -456,7 +456,7 @@ struct AssetCatalog : public DataObject {
 		uint32 flags1;
 		uint16 nameLength;
 		uint16 alwaysZero;
-		uint32 unknown1;     // Possibly scene ID
+		uint32 unknown1;	 // Possibly scene ID
 		uint32 filePosition; // Contains a static value in Obsidian
 		uint32 assetType;
 		uint32 flags2;
@@ -541,9 +541,9 @@ namespace ElementFlags {
 
 namespace AnimationFlags {
 	enum AnimationFlags {
-		kAlternate      = 0x10000000,
-		kLoop           = 0x08000000,
-		kPlayEveryFrame = 0x02000000,
+		kAlternate			= 0x10000000,
+		kLoop				= 0x08000000,
+		kPlayEveryFrame		= 0x02000000,
 	};
 }
 
@@ -926,12 +926,12 @@ protected:
 
 struct ChangeSceneModifier : public DataObject {
 	enum ChangeSceneFlags {
-		kChangeSceneFlagNextScene       = 0x80000000,
-		kChangeSceneFlagPrevScene       = 0x40000000,
-		kChangeSceneFlagSpecificScene   = 0x20000000,
-		kChangeSceneFlagAddToReturnList = 0x10000000,
-		kChangeSceneFlagAddToDestList   = 0x08000000,
-		kChangeSceneFlagWrapAround      = 0x04000000,
+		kChangeSceneFlagNextScene			= 0x80000000,
+		kChangeSceneFlagPrevScene			= 0x40000000,
+		kChangeSceneFlagSpecificScene		= 0x20000000,
+		kChangeSceneFlagAddToReturnList		= 0x10000000,
+		kChangeSceneFlagAddToDestList		= 0x08000000,
+		kChangeSceneFlagWrapAround			= 0x04000000,
 	};
 
 	TypicalModifierHeader modHeader;
diff --git a/engines/mtropolis/elements.h b/engines/mtropolis/elements.h
index 5925313cc83..61f2986b229 100644
--- a/engines/mtropolis/elements.h
+++ b/engines/mtropolis/elements.h
@@ -22,8 +22,6 @@
 #ifndef MTROPOLIS_ELEMENTS_H
 #define MTROPOLIS_ELEMENTS_H
 
-#include "audio/mixer.h"
-
 #include "mtropolis/data.h"
 #include "mtropolis/runtime.h"
 #include "mtropolis/render.h"
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index 28289ac2a45..2ad7109bd98 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -19,16 +19,6 @@
  *
  */
 
-#include "mtropolis/mtropolis.h"
-
-#include "mtropolis/actions.h"
-#include "mtropolis/debug.h"
-#include "mtropolis/runtime.h"
-
-#include "mtropolis/plugins.h"
-#include "mtropolis/plugin/standard.h"
-#include "mtropolis/plugin/obsidian.h"
-
 #include "common/config-manager.h"
 #include "common/debug.h"
 #include "common/events.h"
@@ -47,6 +37,16 @@
 #include "graphics/pixelformat.h"
 #include "graphics/wincursor.h"
 
+#include "mtropolis/mtropolis.h"
+
+#include "mtropolis/actions.h"
+#include "mtropolis/debug.h"
+#include "mtropolis/runtime.h"
+
+#include "mtropolis/plugins.h"
+#include "mtropolis/plugin/standard.h"
+#include "mtropolis/plugin/obsidian.h"
+
 namespace MTropolis {
 
 static Common::SharedPtr<Obsidian::WordGameData> loadWinObsidianWordGameData() {
@@ -465,8 +465,7 @@ Common::Error MTropolisEngine::run() {
 
 		Graphics::PixelFormat clut8Format = Graphics::PixelFormat::createFormatCLUT8();
 
-		for (Common::List<Graphics::PixelFormat>::const_iterator it = pixelFormats.begin(), itEnd = pixelFormats.end(); it != itEnd; ++it) {
-			const Graphics::PixelFormat &candidateFormat = *it;
+		for (const Graphics::PixelFormat &candidateFormat : pixelFormats) {
 			ColorDepthMode thisFormatMode = kColorDepthModeInvalid;
 			bool isExactMatch = false;
 			if (candidateFormat.rBits() == 8 && candidateFormat.gBits() == 8 && candidateFormat.bBits() == 8) {
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index adffad0ac65..97c432e681d 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -321,9 +321,9 @@ bool Label::load(const Data::Label &label) {
 }
 
 bool ColorRGB8::load(const Data::ColorRGB16 &color) {
-	this->r = (color.red * 510 + 1) / 131070;
-	this->g = (color.green * 510 + 1) / 131070;
-	this->b = (color.blue * 510 + 1) / 131070;
+	this->r = (color.red * 0xff * 2 + 1) / (0xffff * 2);
+	this->g = (color.green * 0xff * 2 + 1) / (0xffff * 2);
+	this->b = (color.blue * 0xff * 2 + 1) / (0xffff * 2);
 
 	return true;
 }
@@ -3156,8 +3156,7 @@ VThreadState MessageDispatch::continuePropagating(Runtime *runtime) {
 	while (_propagationStack.size() > 0) {
 		PropagationStack &stackTop = _propagationStack.back();
 
-		switch (stackTop.propagationStage)
-		{
+		switch (stackTop.propagationStage) {
 		case PropagationStack::kStageSendToModifier: {
 				Modifier *modifier = stackTop.ptr.modifier;
 				_propagationStack.pop_back();
@@ -3367,9 +3366,8 @@ Scheduler::Scheduler() {
 }
 
 Scheduler::~Scheduler() {
-	for (Common::Array<Common::SharedPtr<ScheduledEvent>>::iterator it = _events.begin(), itEnd = _events.end(); it != itEnd; ++it) {
-		it->get()->_scheduler = nullptr;
-	}
+	for (const Common::SharedPtr<ScheduledEvent> &evt : _events)
+		evt->_scheduler = nullptr;
 
 	_events.clear();
 }
@@ -3567,8 +3565,7 @@ bool Runtime::runFrame() {
 			_osEventQueue.remove_at(0);
 
 			OSEventType evtType = evt->getEventType();
-			switch (evtType)
-			{
+			switch (evtType) {
 			case kOSEventTypeKeyboard:
 				if (_project) {
 					Common::SharedPtr<KeyEventDispatch> dispatch(new KeyEventDispatch(evt.staticCast<KeyboardInputEvent>()));
@@ -4526,8 +4523,7 @@ void Runtime::sendMessageOnVThread(const Common::SharedPtr<MessageDispatch> &dis
 	const DynamicValue &payload = dispatch->getMsg()->getValue();
 
 	if (payload.getType() != DynamicValueTypes::kNull) {
-		switch (payload.getType())
-		{
+		switch (payload.getType()) {
 		case DynamicValueTypes::kBoolean:
 			valueStr = (payload.getBool() ? "true" : "false");
 			break;


Commit: 3b68a84f0e9679de0968af7ff2d785d65dafe1fd
    https://github.com/scummvm/scummvm/commit/3b68a84f0e9679de0968af7ff2d785d65dafe1fd
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Remove notes since they are in the wiki now

Changed paths:
  R engines/mtropolis/notes/notes.txt
  R engines/mtropolis/notes/obsidian.txt


diff --git a/engines/mtropolis/notes/notes.txt b/engines/mtropolis/notes/notes.txt
deleted file mode 100644
index fab615102f2..00000000000
--- a/engines/mtropolis/notes/notes.txt
+++ /dev/null
@@ -1,170 +0,0 @@
-GUID resolution and structure loading:
-
-Loading of objects in the mTropolis backend is done in multiple stages due to
-the complex GUID resolution process, inline assets, and other things.
-
-The first step to loading is to load data objects.  Data objects are JUST data,
-but due to weird aspects of the data loading process (like the fact that asset
-defs can appear anywhere) it's separate.
-
-The second step is conversion of data objects into runtime objects and
-formation of the scene structure.
-
-The third step is "materialization" which converts loaded objects into a
-ready-to-use state.  This involves replacing any non-variable aliases with a
-copy of the original, and assigning new GUIDs to the new objects.
-
-Objects are only ever materialized once.  Aliasable variables in the global
-modifier table are materialized when the project is loaded, while everything
-else is materialized when it's imported into the project.  (Important note:
-This doesn't apply to aliased compound variables because they are not
-actually considered variable modifiers!)
-
-Objects cloned from already-materialized objects should NOT be materialized
-again, instead they should be fixed up using an object reference remap table,
-shallowClone, and visitInternalReferences.  Cloning is not currently supported
-because Obsidian doesn't use it.
-
-An important aspect of this loading process that GUIDs are resolved in the
-scope of where they are inserted.  This is necessary because mTropolis allows
-objects to be converted into aliases anywhere that they occur and will NOT
-do anything to patch up GUID references from where they were, so the GUID
-needs to be resolvable from the location that the modifier exists.
-
-
-
-Scene transitions:
-
-mTropolis Player's handling of scene transitions is very buggy/quirky.
-Basically, there is a feature called "add to destination scene" (ATDS) which
-loads the target scene on top of a stack of scenes and adds it to the return
-list, which is intended for dialogs and such.
-
-Unfortunately, it only works properly in the straightforward case, and
-transitions are done in a way that basically does the exact actions required
-for it to work in the typical case but are broken in edge cases.
-
-Basically scenes are ordered in a stack, and there is a scene return list
-separate from the scene stack.  The shared scene is always at the bottom of
-the stack.
-
-On forward scene transitions:
- - The shared scene is changed to the shared scene of the new scene
- - If not doing an ATDS-type transfer, then the current scene is unloaded
- - If the target scene is not loaded, then it is loaded at the top of the stack
-
-On return transitions:
- - The active scene is unloaded
- - If returning from a non-ATDS transfer, then the return scene is loaded and its
-   shared scene is made active.
-
-This has a bunch of confirmed broken cases:
-- Returning from an ATDS transfer into a different shared scene will not reset
-  the shared scene on return.
-- Transitioning into an ATDS scene that's already in the stack will cause it to
-  be removed on returning.  This can even cause no scenes to be loaded.
-- Doing a regular transition out of ATDS stacked scenes and then returning will
-  cause only the top scene to be loaded.
-- Transitioning to the shared scene causes very nonsensical behavior, including
-  doubled-up events, modifiers not working, and other chaos.  Currently the
-  mTropolis engine errors out if you attempt this.
-- Probably a bunch of other cases.
-
-
-Media play times:
-
-Sounds, QuickTime movies, and mToons can be commanded to play at any time during
-initial scene loads, but their play times start when the scene transition completes.
-This applies even when there is no scene transition because actions that occur
-prior to the drawing of the first frame may change the media's play state.
-Obsidian, for instance, starts with a bunch of sounds in a non-paused state and
-sets them all to paused via a script, so they must not ever play.
-
-
-Object reference liveness:
-
-A lot of things internally go off of an assumption that structural objects and
-modifiers are NEVER deleted unless the VThread task queue is empty.  Basically that
-means that if any messages are queued or any actions are in the middle of being
-performed, then objects may not be deleted.  The only thing that can delete an
-object is a scheduled Teardown.
-
-Currently this is unused, but mTropolis supports a "kill" event type which will
-remove an object when sent to it, and a "kill" attribute that removes an object
-when it is assigned to.
-
-A big implication of this is that it's okay to use raw pointers to scene objects
-in VThread tasks.  In particular, tasks calling member functions are OK to
-schedule.
-
-
-Miniscript:
-
-Miniscript has a lot of weird quirks.  Most symbols inside of a script are resolved
-as element members, so "WorldManager" in a script doesn't reference some global
-object named WorldManager, it compiles into the equivalent of "element.worldmanager"
-and the actual WorldManager is resolved via hierarchical lookup when attempting to
-read the attribute from the element.
-
-Also, "this" is not reserved - if you look up the "this" attribute from ANYTHING
-then it will resolve to the modifier executing the script.
-
-
-Collision messengers:
-
-Collision messengers behave strangely and their exact logic hasn't been determined.
-
-If there are 2 collision detection modifiers on an object and the first one triggers
-on exit, and the second triggers while in contact, then the second will fire twice.
-
-The "First element only" option behaves kind of nonsensically and contrary to its
-description.  It only prevents multiple collisions from being sent THAT FRAME, but
-continuously moving the object will cause multiple detections to trigger if they
-occur on separate frames.
-
-Moving an object from the shared scene will trigger collision with collision
-messenger modifiers in the main scene, but moving a main scene object will not
-collide with the same object?  Needs more research.
-
-Collisions only occur with visible objects.  It seems they are also not capable
-of colliding with scenes.
-
-
-
-mToon event and cel behavior:
-
-Setting individual values of mToons has extremely quirky behavior with a lot of
-unexpected or broken behavior in edge cases.  In theory, you can reverse an mToon
-by setting its range, but what actually happens is that whenever you set a range,
-the range is sanitized (incorrectly) and the rate is set negative if the
-unsanitized range was backwards.
-
-Setting the range to a forward or backward range causes the rate to be set
-negative or positive.  In theory this is supposed to ensure that the range is
-forward and the rate controls play direction, but it actually has some broken
-cases.
-
-Some sample situations, assuming an mToon with 7 valid frames:
-set mtoon.range to (20 thru 10)
-... will result in a negative rate and range of 10 thru 7.
-
-set mtoon.range.start to 2
-set mtoon.range.end to 4
-set mtoon.range.start to 5
-set mtoon.range.end to 6
-
-... will result in a positive rate and a range of 4 thru 6, because the range
-becomes "5 thru 4" on the third line, which inverts to 5 thru 4.
-
-The cel is always set to a valid value, but this is subject to some quirks
-as well: The cel can be set to an out-of-range value and will go to that cel.
-(The Obsidian booth hint room depends on this behavior.)
-
-Cel changes from scripts and clamping do not fire events.  "At first cel" is
-only fired from play control looping or starting the animation.  "At last cel"
-is only fired from play control.  If the animation is 1 frame, only "at first
-cel" is fired.
-
-"At first cel" is also fired on auto-play even if the animation is paused.
-
-The inter-frame timer is not changed by changing the cel from scripts.
\ No newline at end of file
diff --git a/engines/mtropolis/notes/obsidian.txt b/engines/mtropolis/notes/obsidian.txt
deleted file mode 100644
index 3b4b22aa8c2..00000000000
--- a/engines/mtropolis/notes/obsidian.txt
+++ /dev/null
@@ -1,31 +0,0 @@
-MIDI behavior:
-
-Uses a special MIDI file "ANO&Vol.mid" which triggers AllNoteOff on all channels.
-
-
-Navigation mapping:
-	current  fwright right       rear       left fwleft
-	+0       +1      +2    +3    +4   +5    +6   +7
-
-
-Known script errors:
-
-Forest:
-Miniscript error in (59db9 'set iVol on GEN_SND_Set_iVol'): Failed to assign value to proxy
-
-Triggers on startup.  Caused by kiCrixVolNormal and kiCrixVolLow variables being absent in
-"Forest Intro" section.  The GUIDs are valid for the "Forest" section though, which is always
-loaded.  Harmless?
-
-
-Miniscript error in (11ace '<set cel> on F199_SetState'): Failed to get a writeable reference to attribute 'cel'
-
-Triggers on viewing the drawing pages in the "Journal" section.  Caused by F199_SetState being posted to an
-image elmeent.  Harmless?  Is "cel" a valid attrib?
-
-
-
-Bureau:
-Miniscript error in (5d6349 '<init> on PE'): Failed to get a writeable reference to attribute 'cel'
-
-Triggers on looking down at the chapter start.  Caused by script setting "cel" on a sound.
\ No newline at end of file


Commit: 5d738ee6ac3c221e1a1941c723404f4fe9eec57b
    https://github.com/scummvm/scummvm/commit/5d738ee6ac3c221e1a1941c723404f4fe9eec57b
Author: elasota (ejlasota at gmail.com)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix formatting

Changed paths:
    engines/mtropolis/runtime.cpp


diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 97c432e681d..f7b68d2742c 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -5577,8 +5577,7 @@ void MediaCueState::checkTimestampChange(Runtime *runtime, uint32 oldTS, uint32
 	bool endsInRange = (static_cast<int32>(newTS) >= minTime && static_cast<int32>(newTS) <= maxTime);
 
 	bool shouldTrigger = false;
-	switch (triggerTiming)
-	{
+	switch (triggerTiming) {
 	case kTriggerTimingStart:
 		shouldTrigger = continuousTimestamps ? entersRange : endsInRange;
 		break;


Commit: 39ac21f6b65dbdb8bb39b4206693709755ec1b58
    https://github.com/scummvm/scummvm/commit/39ac21f6b65dbdb8bb39b4206693709755ec1b58
Author: D G Turner (digitall at scummvm.org)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix Missing Field Initializers for Group ID of Extra GUI Options

Changed paths:
    engines/mtropolis/detection.cpp


diff --git a/engines/mtropolis/detection.cpp b/engines/mtropolis/detection.cpp
index b2b77089fa2..cc00b0040bb 100644
--- a/engines/mtropolis/detection.cpp
+++ b/engines/mtropolis/detection.cpp
@@ -70,13 +70,17 @@ const ExtraGuiOptions MTropolisMetaEngineDetection::getExtraGuiOptions(const Com
 		_s("Start with debugger"),
 		_s("Starts with the debugger dashboard active"),
 		"mtropolis_debug_at_start",
-		false
+		false,
+		0,
+		0
 	};
 	static const ExtraGuiOption launchBreakOption = {
 		_s("Start debugging immediately"),
 		_s("Halts progress and stops at the debugger immediately"),
 		"mtropolis_pause_at_start",
-		false
+		false,
+		0,
+		0
 	};
 
 	options.push_back(launchDebugOption);


Commit: 46d70265a6addc9476235c5ccdfa8575f4a88c60
    https://github.com/scummvm/scummvm/commit/46d70265a6addc9476235c5ccdfa8575f4a88c60
Author: D G Turner (digitall at scummvm.org)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix Stray Extra Semicolon Causing GCC Pedantic Warnings

Changed paths:
    engines/mtropolis/runtime.cpp


diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index f7b68d2742c..154d0a3ba7a 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -5465,7 +5465,7 @@ void Runtime::debugGetPrimaryTaskList(Common::Array<Common::SharedPtr<DebugPrima
 
 const Common::Array<Common::SharedPtr<Modifier> > &IModifierContainer::getModifiers() const {
 	return const_cast<IModifierContainer &>(*this).getModifiers();
-};
+}
 
 ChildLoaderContext::ChildLoaderContext() : remainingCount(0), type(kTypeUnknown) {
 }


Commit: 9fdb8d3e83c3fddd8b96015233b5bdb59a793259
    https://github.com/scummvm/scummvm/commit/9fdb8d3e83c3fddd8b96015233b5bdb59a793259
Author: D G Turner (digitall at scummvm.org)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix GCC Compiler Shadowing Warnings

Changed paths:
    engines/mtropolis/element_factory.cpp
    engines/mtropolis/element_factory.h
    engines/mtropolis/modifier_factory.cpp
    engines/mtropolis/modifier_factory.h
    engines/mtropolis/render.cpp
    engines/mtropolis/render.h
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/element_factory.cpp b/engines/mtropolis/element_factory.cpp
index c70afeaca9c..e9ba3334f9f 100644
--- a/engines/mtropolis/element_factory.cpp
+++ b/engines/mtropolis/element_factory.cpp
@@ -25,7 +25,7 @@
 
 namespace MTropolis {
 
-ElementLoaderContext::ElementLoaderContext(Runtime *runtime, size_t streamIndex) : runtime(runtime), streamIndex(streamIndex) {
+ElementLoaderContext::ElementLoaderContext(Runtime *elc_runtime, size_t elc_streamIndex) : runtime(elc_runtime), streamIndex(elc_streamIndex) {
 }
 
 template<typename TElement, typename TElementData>
diff --git a/engines/mtropolis/element_factory.h b/engines/mtropolis/element_factory.h
index 86d351634e0..a98349429ac 100644
--- a/engines/mtropolis/element_factory.h
+++ b/engines/mtropolis/element_factory.h
@@ -28,7 +28,7 @@
 namespace MTropolis {
 
 struct ElementLoaderContext {
-	ElementLoaderContext(Runtime *runtime, size_t streamIndex);
+	ElementLoaderContext(Runtime *elc_runtime, size_t elc_streamIndex);
 
 	Runtime *runtime;
 	size_t streamIndex;
diff --git a/engines/mtropolis/modifier_factory.cpp b/engines/mtropolis/modifier_factory.cpp
index 0491c8ddeda..e2b2cb01559 100644
--- a/engines/mtropolis/modifier_factory.cpp
+++ b/engines/mtropolis/modifier_factory.cpp
@@ -24,10 +24,10 @@
 
 namespace MTropolis {
 
-ModifierLoaderContext::ModifierLoaderContext(ChildLoaderStack *childLoaderStack) : childLoaderStack(childLoaderStack) {}
+ModifierLoaderContext::ModifierLoaderContext(ChildLoaderStack *mlc_childLoaderStack) : childLoaderStack(mlc_childLoaderStack) {}
 
-PlugInModifierLoaderContext::PlugInModifierLoaderContext(ModifierLoaderContext &modifierLoaderContext, const Data::PlugInModifier &plugInModifierData, PlugIn *plugIn)
-	: modifierLoaderContext(modifierLoaderContext), plugInModifierData(plugInModifierData), plugIn(plugIn) {
+PlugInModifierLoaderContext::PlugInModifierLoaderContext(ModifierLoaderContext &pmlc_modifierLoaderContext, const Data::PlugInModifier &pmlc_plugInModifierData, PlugIn *pmlc_plugIn)
+	: modifierLoaderContext(pmlc_modifierLoaderContext), plugInModifierData(pmlc_plugInModifierData), plugIn(pmlc_plugIn) {
 }
 
 template<typename TModifier, typename TModifierData>
diff --git a/engines/mtropolis/modifier_factory.h b/engines/mtropolis/modifier_factory.h
index 1e9e708b3e2..63c1760b517 100644
--- a/engines/mtropolis/modifier_factory.h
+++ b/engines/mtropolis/modifier_factory.h
@@ -28,7 +28,7 @@
 namespace MTropolis {
 
 struct ModifierLoaderContext {
-	explicit ModifierLoaderContext(ChildLoaderStack *childLoaderStack);
+	explicit ModifierLoaderContext(ChildLoaderStack *mlc_childLoaderStack);
 
 	ChildLoaderStack *childLoaderStack;
 };
@@ -46,7 +46,7 @@ struct IPlugInModifierFactoryAndDataFactory : public IPlugInModifierFactory, pub
 
 // Helper classes for plug-in modifier loaders
 struct PlugInModifierLoaderContext {
-	PlugInModifierLoaderContext(ModifierLoaderContext &modifierLoaderContext, const Data::PlugInModifier &plugInModifierData, PlugIn *plugIn);
+	PlugInModifierLoaderContext(ModifierLoaderContext &pmlc_modifierLoaderContext, const Data::PlugInModifier &pmlc_plugInModifierData, PlugIn *pmlc_plugIn);
 
 	ModifierLoaderContext &modifierLoaderContext;
 	const Data::PlugInModifier &plugInModifierData;
diff --git a/engines/mtropolis/render.cpp b/engines/mtropolis/render.cpp
index f5a2395987c..175717b9d18 100644
--- a/engines/mtropolis/render.cpp
+++ b/engines/mtropolis/render.cpp
@@ -96,11 +96,11 @@ bool TextStyleFlags::load(uint8 dataStyleFlags) {
 MacFontFormatting::MacFontFormatting() : fontID(0), fontFlags(0), size(12) {
 }
 
-MacFontFormatting::MacFontFormatting(uint16 fontID, uint8 fontFlags, uint16 size) : fontID(fontID), fontFlags(fontFlags), size(size) {
+MacFontFormatting::MacFontFormatting(uint16 mff_fontID, uint8 mff_fontFlags, uint16 mff_size) : fontID(mff_fontID), fontFlags(mff_fontFlags), size(mff_size) {
 }
 
-WindowParameters::WindowParameters(Runtime *runtime, int32 x, int32 y, int16 width, int16 height, const Graphics::PixelFormat &format)
-	: runtime(runtime), x(x), y(y), width(width), height(height), format(format) {
+WindowParameters::WindowParameters(Runtime *wp_runtime, int32 wp_x, int32 wp_y, int16 wp_width, int16 wp_height, const Graphics::PixelFormat &wp_format)
+	: runtime(wp_runtime), x(wp_x), y(wp_y), width(wp_width), height(wp_height), format(wp_format) {
 }
 
 Window::Window(const WindowParameters &windowParams)
diff --git a/engines/mtropolis/render.h b/engines/mtropolis/render.h
index 33496298582..17e847a4466 100644
--- a/engines/mtropolis/render.h
+++ b/engines/mtropolis/render.h
@@ -62,7 +62,7 @@ struct TextStyleFlags {
 
 struct MacFontFormatting {
 	MacFontFormatting();
-	MacFontFormatting(uint16 fontID, uint8 fontFlags, uint16 size);
+	MacFontFormatting(uint16 mff_fontID, uint8 mff_fontFlags, uint16 mff_size);
 
 	uint16 fontID;
 	uint8 fontFlags;
@@ -82,7 +82,7 @@ struct WindowParameters {
 	int16 height;
 	const Graphics::PixelFormat format;
 
-	WindowParameters(Runtime *runtime, int32 x, int32 y, int16 width, int16 height, const Graphics::PixelFormat &format);
+	WindowParameters(Runtime *wp_runtime, int32 wp_x, int32 wp_y, int16 wp_width, int16 wp_height, const Graphics::PixelFormat &wp_format);
 };
 
 class Window {
diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index 154d0a3ba7a..f6c7f078c62 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -3103,8 +3103,8 @@ LowLevelSceneStateTransitionAction &LowLevelSceneStateTransitionAction::operator
 }
 
 
-HighLevelSceneTransition::HighLevelSceneTransition(const Common::SharedPtr<Structural> &scene, Type type, bool addToDestinationScene, bool addToReturnList)
-	: scene(scene), type(type), addToDestinationScene(addToDestinationScene), addToReturnList(addToReturnList) {
+HighLevelSceneTransition::HighLevelSceneTransition(const Common::SharedPtr<Structural> &hlst_scene, Type hlst_type, bool hlst_addToDestinationScene, bool hlst_addToReturnList)
+	: scene(hlst_scene), type(hlst_type), addToDestinationScene(hlst_addToDestinationScene), addToReturnList(hlst_addToReturnList) {
 }
 
 MessageDispatch::MessageDispatch(const Common::SharedPtr<MessageProperties> &msgProps, Structural *root, bool cascade, bool relay, bool couldBeCommand)
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 1e81fed8bd5..49bf237875d 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -1266,7 +1266,7 @@ struct HighLevelSceneTransition {
 		kTypeChangeToScene,
 	};
 
-	HighLevelSceneTransition(const Common::SharedPtr<Structural> &scene, Type type, bool addToDestinationScene, bool addToReturnList);
+	HighLevelSceneTransition(const Common::SharedPtr<Structural> &hlst_scene, Type hlst_type, bool hlst_addToDestinationScene, bool hlst_addToReturnList);
 
 	Common::SharedPtr<Structural> scene;
 	Type type;


Commit: 1d0ea143a7662f7b875ed58c495509ac0524b69e
    https://github.com/scummvm/scummvm/commit/1d0ea143a7662f7b875ed58c495509ac0524b69e
Author: D G Turner (digitall at scummvm.org)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix Set But Unused Variable GCC Compiler Warnings

Changed paths:
    engines/mtropolis/debug.cpp
    engines/mtropolis/mtropolis.cpp


diff --git a/engines/mtropolis/debug.cpp b/engines/mtropolis/debug.cpp
index 20baaa84b42..f4ac3499671 100644
--- a/engines/mtropolis/debug.cpp
+++ b/engines/mtropolis/debug.cpp
@@ -974,14 +974,12 @@ DebugInspectorWindow::DebugInspectorWindow(Debugger *debugger, const WindowParam
 void DebugInspectorWindow::update() {
 	const Common::SharedPtr<DebugInspector> inspector = _debugger->getInspector();
 
-	bool inspectorChanged = false;
 	if (inspector != _inspector) {
 		_maxLabelWidth = 0;
 		_labeledRow.clear();
 		_unlabeledRow.clear();
 
 		_inspector = inspector;
-		inspectorChanged = true;
 		setDirty();
 	}
 
diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp
index 2ad7109bd98..8e0894c2b05 100644
--- a/engines/mtropolis/mtropolis.cpp
+++ b/engines/mtropolis/mtropolis.cpp
@@ -555,16 +555,9 @@ Common::Error MTropolisEngine::run() {
 	}
 #endif
 
-	bool paused = false;
-
 	while (!shouldQuit()) {
 		handleEvents();
 
-#ifdef MTROPOLIS_DEBUG_ENABLE
-		if (_runtime->debugGetDebugger())
-			paused = _runtime->debugGetDebugger()->isPaused();
-#endif
-
 		if (!_runtime->runFrame())
 			break;
 


Commit: 17c24579b0e8be5b4eda2f656e6d5d0b0d50f58a
    https://github.com/scummvm/scummvm/commit/17c24579b0e8be5b4eda2f656e6d5d0b0d50f58a
Author: D G Turner (digitall at scummvm.org)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Make Explicit Unhandled Type Cases in DynamicValueType Change

This removes several unhandled type in switch GCC compiler warnings.

Changed paths:
    engines/mtropolis/runtime.cpp


diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index f6c7f078c62..e0252696764 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -908,6 +908,9 @@ void DynamicList::createWriteProxyForIndex(size_t index, DynamicValueWriteProxy
 
 bool DynamicList::changeToType(DynamicValueTypes::DynamicValueType type) {
 	switch (type) {
+	case DynamicValueTypes::kInvalid:
+		// FIXME: Set _container as per kNull case?
+		break;
 	case DynamicValueTypes::kNull:
 		_container = new DynamicListContainer<void>();
 		break;
@@ -950,6 +953,15 @@ bool DynamicList::changeToType(DynamicValueTypes::DynamicValueType type) {
 	case DynamicValueTypes::kObject:
 		_container = new DynamicListContainer<ObjectReference>();
 		break;
+	case DynamicValueTypes::kReadProxy:
+		// FIXME
+		break;
+	case DynamicValueTypes::kWriteProxy:
+		// FIXME
+		break;
+	case DynamicValueTypes::kEmpty:
+		// FIXME: Set _container as per kNull case?
+		break;
 	}
 
 	_type = type;


Commit: e58ae7772866cef0d7e755950f05c694f4ddd015
    https://github.com/scummvm/scummvm/commit/e58ae7772866cef0d7e755950f05c694f4ddd015
Author: D G Turner (digitall at scummvm.org)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix Signed vs. Unsigned Comparison GCC Compiler Warnings

Changed paths:
    engines/mtropolis/elements.cpp


diff --git a/engines/mtropolis/elements.cpp b/engines/mtropolis/elements.cpp
index e621ab2b8d6..361167c2ac5 100644
--- a/engines/mtropolis/elements.cpp
+++ b/engines/mtropolis/elements.cpp
@@ -775,10 +775,10 @@ void MovieElement::playMedia(Runtime *runtime, Project *project) {
 		if (framesDecodedThisFrame > 1)
 			debug(1, "Perf warning: %i video frames decoded in one frame", framesDecodedThisFrame);
 
-		if (targetTS < _playRange.min)
-			targetTS = _playRange.min;
-		if (targetTS > _playRange.max)
-			targetTS = _playRange.max;
+		if (targetTS < minTS)
+			targetTS = minTS;
+		if (targetTS > maxTS)
+			targetTS = maxTS;
 
 		// Sync TS to the end of video if we hit the end
 
@@ -868,7 +868,7 @@ MiniscriptInstructionOutcome MovieElement::scriptSetTimestamp(MiniscriptThread *
 	else if (asInteger > _playRange.max)
 		asInteger = _playRange.max;
 
-	if (asInteger != _currentTimestamp) {
+	if (asInteger != (int32)_currentTimestamp) {
 		SeekToTimeTaskData *taskData = thread->getRuntime()->getVThread().pushTask("MovieElement::seekToTimeTask", this, &MovieElement::seekToTimeTask);
 		taskData->runtime = _runtime;
 		taskData->timestamp = asInteger;
@@ -917,10 +917,10 @@ MiniscriptInstructionOutcome MovieElement::scriptSetRangeTyped(MiniscriptThread
 
 	if (_playRange.min < 0)
 		_playRange.min = 0;
-	else if (_playRange.min > _maxTimestamp)
+	else if (_playRange.min > (int32)_maxTimestamp)
 		_playRange.min = _maxTimestamp;
 
-	if (_playRange.max > _maxTimestamp)
+	if (_playRange.max > (int32)_maxTimestamp)
 		_playRange.max = _maxTimestamp;
 
 	if (_playRange.max < _playRange.min)


Commit: 3192daf0a7af4f9e1d35b9c01eba8d43b6d46784
    https://github.com/scummvm/scummvm/commit/3192daf0a7af4f9e1d35b9c01eba8d43b6d46784
Author: D G Turner (digitall at scummvm.org)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
VIDEO: Fix Signed vs. Unsigned Warnings in MTROPOLIS changes to QT Decoder

Changed paths:
    video/qt_decoder.cpp


diff --git a/video/qt_decoder.cpp b/video/qt_decoder.cpp
index 165acf0d0fa..85121a4783e 100644
--- a/video/qt_decoder.cpp
+++ b/video/qt_decoder.cpp
@@ -563,7 +563,7 @@ Audio::Timestamp QuickTimeDecoder::VideoTrackHandler::getFrameTime(uint frame) c
 	int cumulativeDuration = 0;
 	for (int ttsIndex = 0; ttsIndex < _parent->timeToSampleCount; ttsIndex++) {
 		const TimeToSampleEntry &tts = _parent->timeToSample[ttsIndex];
-		if (frame < tts.count)
+		if ((int)frame < tts.count)
 			return Audio::Timestamp(0, _parent->timeScale).addFrames(cumulativeDuration + frame * tts.duration);
 		else {
 			frame -= tts.count;


Commit: 0a9539d1df057e95e76cc953e0709f996b985422
    https://github.com/scummvm/scummvm/commit/0a9539d1df057e95e76cc953e0709f996b985422
Author: D G Turner (digitall at scummvm.org)
Date: 2022-06-16T21:58:09+02:00

Commit Message:
MTROPOLIS: Fix Deprecated Implicit Copy Operator GCC Warning

This copy operator appears to be identical to the implicit default
copy operator generated by the compiler which causes a warning to be
emitted.

Changed paths:
    engines/mtropolis/runtime.cpp
    engines/mtropolis/runtime.h


diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp
index e0252696764..c0b8b3db5ae 100644
--- a/engines/mtropolis/runtime.cpp
+++ b/engines/mtropolis/runtime.cpp
@@ -6588,6 +6588,7 @@ void VisualElementRenderProperties::clearDirty() {
 	_isDirty = false;
 }
 
+/*
 VisualElementRenderProperties &VisualElementRenderProperties::operator=(const VisualElementRenderProperties &other) {
 	_inkMode = other._inkMode;
 	_shape = other._shape;
@@ -6604,6 +6605,7 @@ VisualElementRenderProperties &VisualElementRenderProperties::operator=(const Vi
 
 	return *this;
 }
+*/
 
 VisualElement::VisualElement()
 	: _rect(0, 0, 0, 0), _cachedAbsoluteOrigin(Common::Point(0, 0))
diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h
index 49bf237875d..4c951f9293d 100644
--- a/engines/mtropolis/runtime.h
+++ b/engines/mtropolis/runtime.h
@@ -2362,7 +2362,7 @@ public:
 	bool isDirty() const;
 	void clearDirty();
 
-	VisualElementRenderProperties &operator=(const VisualElementRenderProperties &other);
+	//VisualElementRenderProperties &operator=(const VisualElementRenderProperties &other);
 
 private:
 	InkMode _inkMode;




More information about the Scummvm-git-logs mailing list