[Scummvm-git-logs] scummvm master -> 151c31226ba2140a0efc28f4bf1e93e0f27b3d84

mgerhardy noreply at scummvm.org
Sun Aug 2 19:30:54 UTC 2026


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

Summary:
d869392cd9 MACS2: fixed argument order in isPathWalkable call
d93c8d422c MACS2: renamed adlib to music
0a6d5df7f6 MACS2: cleanup for amiga preparations
944105844c MACS2: extract some constants into functions to prepare for the amiga demo support
f86c012f35 MACS2: minor cleanup in renaming methods and removing loadSongFromSceneData
ab78c9292a MACS2: added amiga demo detection
f33e897885 MACS2: fill some of the platform specific magic values for the amiga
b6556791e7 MACS2: amiga string decoding is different
14601bf994 MACS2: resize of the top of the function
2764981a74 MACS2: don't read data from exe for amiga
7d36e8edeb MACS2: music is (or will be) a facade not just for adlib
0484fcbbf8 MACS2: leave a comment for my future me regarding full version and demo version offsets
1a6023f754 MACS2: opcode table
fe4f8be393 MACS2: fixed opcode outer loop exit
ef2c114ffb MACS2: improved amiga demo support
4330a2ada7 MACS2: improved mcs v1 magic number situation (named a few things)
3dad819e2d MACS2: fixed inventory item transfer in scumm verb ui mode
151c31226b MACS2: replaced opcode functions with script callbacks


Commit: d869392cd9c27d934908c51c907bcaee0686ff03
    https://github.com/scummvm/scummvm/commit/d869392cd9c27d934908c51c907bcaee0686ff03
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00

Commit Message:
MACS2: fixed argument order in isPathWalkable call

Changed paths:
    engines/macs2/scriptexecutor.cpp


diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 0734b2c504b..40cb21fbcc2 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -1974,7 +1974,8 @@ void Script::ScriptExecutor::scriptMoveToPosition() {
 	}
 
 	const Common::Point target(x, y);
-	if (!_engine->isPathWalkable(object->_position.y, object->_position.x, y, x) &&
+	// Binary scriptMoveToPosition (1008:bafc): isPathWalkable(targetY, targetX, objY, objX).
+	if (!_engine->isPathWalkable(y, x, object->_position.y, object->_position.x) &&
 		Macs2Engine::isWalkabilityWalkable(_engine->getWalkabilityAt(target))) {
 		setScriptError(0x15);
 		return;


Commit: d93c8d422cd3c4b99b158bf73aaaf9b90d094ecf
    https://github.com/scummvm/scummvm/commit/d93c8d422cd3c4b99b158bf73aaaf9b90d094ecf
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00

Commit Message:
MACS2: renamed adlib to music

Changed paths:
    engines/macs2/debugtools.cpp
    engines/macs2/macs2.cpp
    engines/macs2/macs2.h
    engines/macs2/scriptexecutor.cpp
    engines/macs2/view1.cpp


diff --git a/engines/macs2/debugtools.cpp b/engines/macs2/debugtools.cpp
index baae93e051d..b9ea027eb96 100644
--- a/engines/macs2/debugtools.cpp
+++ b/engines/macs2/debugtools.cpp
@@ -2197,7 +2197,7 @@ static void showSoundWindow() {
 		return;
 	}
 
-	Music *adlib = g_engine->getAdlib();
+	Music *adlib = g_engine->getMusic();
 	const Music::DebugState &ds = adlib->_debug;
 
 	ImGui::Text("Master Volume: %u/63", ds.masterVolume);
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 2725c640e3b..970b5f82c73 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -305,7 +305,7 @@ void Macs2Engine::readExecutable() {
 		exeFileStream.reset(new Common::MemoryReadStream(fileData, size, DisposeAfterUse::YES));
 	}
 
-	_adlib->readDataFromExecutable(exeFileStream.get());
+	_music->readDataFromExecutable(exeFileStream.get());
 
 	exeFileStream->seek(0x0001B610, SEEK_SET);
 	inventoryIconIndices.resize(6);
@@ -396,7 +396,7 @@ Macs2Engine::Macs2Engine(OSystem *syst, const ADGameDescription *gameDesc) : Eng
 	g_engine = this;
 	_scriptExecutor = new Script::ScriptExecutor();
 	_scriptExecutor->_engine = this;
-	_adlib = new Music();
+	_music = new Music();
 
 	// We have a fixed 0x10 number of entries
 	_hotspotOverrides.resize(0x11);
@@ -411,8 +411,8 @@ Macs2Engine::~Macs2Engine() {
 #endif
 	stopInputRecording();
 	clearCurrentSoundData();
-	_adlib->deinit();
-	delete _adlib;
+	_music->deinit();
+	delete _music;
 	delete _fileStream;
 	delete _scriptExecutor;
 	for (uint i = 0; i < GameObjects::instance()._objects.size(); i++) {
@@ -437,13 +437,13 @@ void Macs2Engine::sayText(const Common::String &text, Common::TextToSpeechManage
 void Macs2Engine::syncSoundSettings() {
 	Engine::syncSoundSettings();
 
-	if (_adlib && _scriptExecutor) {
+	if (_music && _scriptExecutor) {
 		int musicVolume = ConfMan.getInt("music_volume");
 		// OPL emulator is registered as kPlainSoundType; mute it at mixer level
 		// when user sets music volume to 0 (OPL attenuation 0x3F is not true silence).
 		_mixer->muteSoundType(Audio::Mixer::kPlainSoundType,
 							  (musicVolume == 0) || (ConfMan.hasKey("mute") && ConfMan.getBool("mute")));
-		_adlib->setVolume(scaledMusicVolume(_scriptExecutor->_musicControlVolume));
+		_music->setVolume(scaledMusicVolume(_scriptExecutor->_musicControlVolume));
 	}
 }
 
@@ -1961,7 +1961,7 @@ void Macs2Engine::loadSongFromSceneData(uint8 dataIndex) {
 	Common::Array<uint8> data;
 	data.resize(size);
 	_fileStream->read(data.data(), size);
-	_adlib->playSongData(data);
+	_music->playSongData(data);
 }
 
 void Macs2Engine::setCurrentSoundData(const Common::Array<uint8> &data) {
@@ -2021,7 +2021,7 @@ Common::Error Macs2Engine::run() {
 	CursorMan.showMouse(false);
 
 	// Initialize Adlib
-	_adlib->init();
+	_music->init();
 	syncSoundSettings();
 
 	// Set the engine's debugger console
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index ea07d195a76..b4f22f649bc 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -236,7 +236,7 @@ class Macs2Engine : public Engine, public Events {
 private:
 	const ADGameDescription *_gameDescription;
 
-	Music *_adlib = nullptr;
+	Music *_music = nullptr;
 
 protected:
 	// Engine APIs
@@ -413,7 +413,7 @@ public:
 	void sortObjectsByDepth(uint16 objectIndex);
 
 	void loadSongFromSceneData(uint8 dataIndex);
-	Music *getAdlib() const { return _adlib; }
+	Music *getMusic() const { return _music; }
 	// Returns the Music volume (0-63) scaled by the user's music_volume setting
 	uint16 scaledMusicVolume(uint16 gameAttenuation) const;
 	void setCurrentSoundData(const Common::Array<uint8> &data);
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 40cb21fbcc2..9b57dd6dd81 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -2707,7 +2707,7 @@ void Script::ScriptExecutor::scriptPlayMusicSlot() {
 	}
 
 	if (_activeMusicSlot != 0) {
-		_engine->getAdlib()->stopMusic();
+		_engine->getMusic()->stopMusic();
 		_activeMusicSlot = 0;
 	}
 
@@ -2717,17 +2717,17 @@ void Script::ScriptExecutor::scriptPlayMusicSlot() {
 		return;
 	}
 
-	_engine->getAdlib()->playSongData(_musicSlots[slotID - 1]);
+	_engine->getMusic()->playSongData(_musicSlots[slotID - 1]);
 	if (startMuted == 0) {
 		_musicControlMode = 1;
 		_musicControlStep = fadeParam;
 		_musicControlVolume = 0x3F;
-		_engine->getAdlib()->setVolume(_engine->scaledMusicVolume(_musicControlVolume));
+		_engine->getMusic()->setVolume(_engine->scaledMusicVolume(_musicControlVolume));
 	} else {
 		_musicControlMode = 0;
 		_musicControlStep = 0;
 		_musicControlVolume = 0;
-		_engine->getAdlib()->setVolume(_engine->scaledMusicVolume(0));
+		_engine->getMusic()->setVolume(_engine->scaledMusicVolume(0));
 	}
 
 	_activeMusicSlot = slotID;
@@ -2755,7 +2755,7 @@ void Script::ScriptExecutor::scriptStopMusicSlot() {
 			_musicControlStep = fadeParam;
 			_musicControlVolume = 0;
 		} else {
-			_engine->getAdlib()->stopMusic();
+			_engine->getMusic()->stopMusic();
 			_activeMusicSlot = 0;
 		}
 	}
@@ -2783,7 +2783,7 @@ void Script::ScriptExecutor::scriptFreeMusicSlot() {
 
 	if (_activeMusicSlot == slotID) {
 		if (_musicEnabled && _soundSystemActive) {
-			_engine->getAdlib()->stopMusic();
+			_engine->getMusic()->stopMusic();
 		}
 		_activeMusicSlot = 0;
 	}
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 1c7a503691f..9c2770bf9ed 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -2278,17 +2278,17 @@ bool View1::tick() {
 			} else {
 				se->_musicControlVolume = vol;
 			}
-			g_engine->getAdlib()->setVolume(g_engine->scaledMusicVolume(se->_musicControlVolume));
+			g_engine->getMusic()->setVolume(g_engine->scaledMusicVolume(se->_musicControlVolume));
 		} else {
 			// Fade in: volume += step. When >= 63: stop music.
 			int vol = (int)se->_musicControlVolume + (int)musicStep;
 			if (vol >= 0x3F) {
 				se->_musicControlMode = 0;
 				se->_activeMusicSlot = 0;
-				g_engine->getAdlib()->stopMusic();
+				g_engine->getMusic()->stopMusic();
 			} else {
 				se->_musicControlVolume = vol;
-				g_engine->getAdlib()->setVolume(g_engine->scaledMusicVolume(se->_musicControlVolume));
+				g_engine->getMusic()->setVolume(g_engine->scaledMusicVolume(se->_musicControlVolume));
 			}
 		}
 	}
@@ -2452,7 +2452,7 @@ bool View1::tick() {
 				}
 			} else if (executor->_waitForAdlibReady) {
 				drawSceneUpdate();
-				if (g_engine->getAdlib()->isPlaybackReady()) {
+				if (g_engine->getMusic()->isPlaybackReady()) {
 					executor->_waitForAdlibReady = false;
 					g_engine->runScriptExecutor();
 				}
@@ -4088,7 +4088,7 @@ void View1::openOriginalSaveLoadPanel() {
 	// if (g_wMusicEnabled && sceneData[g_wActiveMusicSlot] != 0) adlibStopMusic()
 	if (g_engine->_scriptExecutor->_musicEnabled &&
 		g_engine->_scriptExecutor->_activeMusicSlot != 0) {
-		g_engine->getAdlib()->stopMusic();
+		g_engine->getMusic()->stopMusic();
 	}
 
 	// First loop: calculate max icon width/height from the 7 button images
@@ -4388,12 +4388,12 @@ void View1::handleOriginalSaveLoadClick(const Common::Point &pos) {
 					g_engine->_scriptExecutor->_soundSystemActive) {
 					uint16 slot = g_engine->_scriptExecutor->_activeMusicSlot;
 					if (slot != 0 && !g_engine->_scriptExecutor->_musicSlots[slot - 1].empty()) {
-						g_engine->getAdlib()->playSongData(g_engine->_scriptExecutor->_musicSlots[slot - 1]);
+						g_engine->getMusic()->playSongData(g_engine->_scriptExecutor->_musicSlots[slot - 1]);
 						// Original's adlibTickHandler resets g_bAdlibMasterVolume=0 (full volume).
 						// ScummVM layers user volume on top via scaledMusicVolume, so re-apply it.
 						g_engine->_scriptExecutor->_musicControlMode = 0;
 						g_engine->_scriptExecutor->_musicControlVolume = 0;
-						g_engine->getAdlib()->setVolume(g_engine->scaledMusicVolume(0));
+						g_engine->getMusic()->setVolume(g_engine->scaledMusicVolume(0));
 					}
 				}
 			}


Commit: 0a6d5df7f672c22093678baa3067966156ba7c8b
    https://github.com/scummvm/scummvm/commit/0a6d5df7f672c22093678baa3067966156ba7c8b
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00

Commit Message:
MACS2: cleanup for amiga preparations

Changed paths:
    engines/macs2/events.cpp
    engines/macs2/gameobjects.cpp
    engines/macs2/gameobjects.h
    engines/macs2/macs2.cpp
    engines/macs2/macs2.h
    engines/macs2/music.cpp
    engines/macs2/music.h
    engines/macs2/saveload.cpp
    engines/macs2/scriptexecutor.cpp
    engines/macs2/view1.cpp
    engines/macs2/view1.h


diff --git a/engines/macs2/events.cpp b/engines/macs2/events.cpp
index ca4b0f759da..e4d6b65f28d 100644
--- a/engines/macs2/events.cpp
+++ b/engines/macs2/events.cpp
@@ -49,13 +49,12 @@ void Events::runGame() {
 	if (saveSlot != -1)
 		g_engine->loadGameState(saveSlot);
 
-	// DOS timer ISR fires at ~21.6Hz (PIT divisor 0xD7B0 = 55216, 1193182/55216).
-	// Main loop processes a game frame when g_wTimerTickCounter > 1 (every ~2 ticks).
-	// Effective game frame rate: ~10.8fps (every ~92ms).
-	// We use wall-clock timing to match the original rate precisely.
+	// DOS: PIT ISR ~21.6Hz (divisor 0xD7B0). Main loop runs a game frame when the
+	// ISR counter exceeds 1 (every 2 ticks) -> ~10.8fps / ~92.6ms.
+	// Tick period and ticks-per-frame are locals so other platforms can retune later.
 	uint32 lastTickTime = g_system->getMillis();
-	// One timer tick = 1000/21.6 ≈ 46.3ms. Game ticks every 2 timer ticks = ~92.6ms.
-	static constexpr uint32 kTimerTickMs = 46;
+	const uint32 kTimerTickMs = 46;
+	const uint16 kNormalTicksPerGameFrame = 2;
 	uint16 timerTickCounter = 0;
 
 	Common::Event e;
@@ -91,8 +90,8 @@ void Events::runGame() {
 		case 2: // slow mode: tick when counter >= 0x12
 			doTick = (timerTickCounter >= 0x12); // TODO: this is running at different speed compared to running this in dosbox
 			break;
-		default: // normal: tick when counter > 1
-			doTick = (timerTickCounter > 1);
+		default: // normal: every kNormalTicksPerGameFrame timer ticks
+			doTick = (timerTickCounter >= kNormalTicksPerGameFrame);
 			break;
 		}
 
diff --git a/engines/macs2/gameobjects.cpp b/engines/macs2/gameobjects.cpp
index 3a4cb0ad45d..6ad3304d653 100644
--- a/engines/macs2/gameobjects.cpp
+++ b/engines/macs2/gameobjects.cpp
@@ -30,25 +30,19 @@ DECLARE_SINGLETON(Macs2::Scenes);
 
 } // namespace Common
 
-Common::MemoryReadStream *Macs2::Scenes::readSceneScript(uint16 sceneIndex, Common::MemoryReadStream *fileStream) {
-	// Calculate the offset of the script data offset
-	// This addressing can be found in the l0037_2856 code block
-
-	uint16 sceneDataOffset = sceneIndex * 0xC;
-	// Offset of the data in [0752h] global
-	constexpr uint16 globalDataOffset = 0xC + 0x4;
-	sceneDataOffset += globalDataOffset;
-	fileStream->seek(sceneDataOffset - 0x8);
+Common::MemoryReadStream *Macs2::Scenes::readSceneScript(uint16 sceneIndex, Common::SeekableReadStream *fileStream) {
+	// Directory entry for sceneIndex: absolute seek uses
+	//   directoryOffset + sceneIndex * 0xC - 8  (second dword of entry sceneIndex-1).
+	const uint32 directoryOffset = g_engine->getMcsDirectoryOffset();
+	fileStream->seek(directoryOffset + sceneIndex * 0xC - 0x8);
 	uint32 sceneDataOffset2 = fileStream->readUint32LE();
 	fileStream->seek(sceneDataOffset2, SEEK_SET);
 
-	// Read the script from there
-	// We read 80h bytes
+	// DOS: skip 0x80 resource offsets, then script size + bytecode.
 	fileStream->seek(0x80, SEEK_CUR);
 	uint16 scriptSize = fileStream->readUint16LE();
 	if (scriptSize == 0) {
 		warning("Macs2::Scenes::ReadSceneScript: scene %u has empty script", sceneIndex);
-		// Return a valid but empty stream
 		return new Common::MemoryReadStream(nullptr, 0);
 	}
 	byte *scriptData = (byte *)malloc(scriptSize);
@@ -56,56 +50,45 @@ Common::MemoryReadStream *Macs2::Scenes::readSceneScript(uint16 sceneIndex, Comm
 	return new Common::MemoryReadStream(scriptData, scriptSize, DisposeAfterUse::YES);
 }
 
-Common::Array<uint32> Macs2::Scenes::readSpecialAnimsOffsets(uint16 sceneIndex, Common::MemoryReadStream *fileStream) {
+Common::Array<uint32> Macs2::Scenes::readSpecialAnimsOffsets(uint16 sceneIndex, Common::SeekableReadStream *fileStream) {
 	Common::Array<uint32> result;
 	result.resize(0x80 / 4);
 
-	// Calculate the offset of the script data offset
-	// This addressing can be found in the l0037_2856 code block
-	// TODO: This part is copy-pasted and could be refactored into one proper loading function
-
-	uint16 sceneDataOffset = sceneIndex * 0xC;
-	// Offset of the data in [0752h] global
-	constexpr uint16 globalDataOffset = 0xC + 0x4;
-	sceneDataOffset += globalDataOffset;
-	fileStream->seek(sceneDataOffset - 0x8);
+	const uint32 directoryOffset = g_engine->getMcsDirectoryOffset();
+	fileStream->seek(directoryOffset + sceneIndex * 0xC - 0x8);
 	uint32 sceneDataOffset2 = fileStream->readUint32LE();
 	fileStream->seek(sceneDataOffset2, SEEK_SET);
 
-	// Read the script from there
-	// We read 80h bytes for the special animations offsets
 	fileStream->read(result.data(), 0x80);
 
 	return result;
 }
 
-Common::MemoryReadStream *Macs2::Scenes::readSceneStrings(uint16 sceneIndex, Common::MemoryReadStream *fileStream) {
-	// Calculate the offset of the script data offset
-	// This addressing can be found in the l0037_2856 code block
-
-	uint16 sceneDataOffset = sceneIndex * 0xC;
-	// Offset of the data in [0752h] global
-	constexpr uint16 globalDataOffset = 0xC + 0x4;
-	sceneDataOffset += globalDataOffset;
-	fileStream->seek(sceneDataOffset - 0x4);
+Common::MemoryReadStream *Macs2::Scenes::readSceneStrings(uint16 sceneIndex, Common::SeekableReadStream *fileStream) {
+	const uint32 directoryOffset = g_engine->getMcsDirectoryOffset();
+	// Third dword of entry (sceneIndex-1) / strings blob — DOS formula directory+scene*0xC-4.
+	fileStream->seek(directoryOffset + sceneIndex * 0xC - 0x4);
 	uint32 sceneDataOffset2 = fileStream->readUint32LE();
 	fileStream->seek(sceneDataOffset2, SEEK_SET);
 
-	// This lives in l0037_A4FC:
-
-	// Read the script from there
-	// Size - lives in global [0F84h]
 	uint16 size = fileStream->readUint16LE();
 
-	// Stringdata lives in the pointer [0F80h]
 	byte *stringData = (byte *)malloc(size);
 	fileStream->read(stringData, size);
 	return new Common::MemoryReadStream(stringData, size, DisposeAfterUse::YES);
-	// Note: We save the current scene number to [0F86h] - maybe "scene we have strings loaded for"?
 }
 
-Common::Array<uint8> Macs2::Scenes::readSpecialAnimBlob(uint16 index, Common::MemoryReadStream *fileStream) {
+Common::Array<uint8> Macs2::Scenes::readSpecialAnimBlob(uint16 index, Common::SeekableReadStream *fileStream) {
+	if (index == 0 || index > _currentSceneSpecialAnimOffsets.size()) {
+		warning("readSpecialAnimBlob: index %u out of range (table size %u)",
+				index, _currentSceneSpecialAnimOffsets.size());
+		return Common::Array<uint8>();
+	}
 	uint32 offset = _currentSceneSpecialAnimOffsets[index - 1];
+	if (offset == 0 || fileStream == nullptr) {
+		warning("readSpecialAnimBlob: null offset for index %u", index);
+		return Common::Array<uint8>();
+	}
 	fileStream->seek(offset, SEEK_SET);
 	uint32 length = fileStream->readUint32LE();
 	// Skip a string - note the original code adds 0x4 for the previously read size since
@@ -401,33 +384,18 @@ Macs2::GameObject *Macs2::GameObjects::getObjectByIndex(uint16 index) {
 	return instance()._objects[index - 1];
 }
 
-Common::MemoryReadStream *Macs2::GameObjects::readGameObjectStrings(uint16 index, Common::MemoryReadStream *fileStream) {
-	// TODO: The original binary caches the last loaded object's string data in memory
-	// (g_wStringDecodeCacheObjectId at DS:0f86 / g_pSavedScriptState). It skips the file
-	// read if the same object is requested again. We re-read from file every time.
-	// TODO: Copy&Pasted code from ReadSceneStrings
-	// Calculate the offset of the script data offset
-	// This addressing can be found in the l0037_2856 code block
-
-	uint16 gameObjectDataOffset = index * 0xC;
-	// Offset of the data in [0752h] global
-	constexpr uint16 globalDataOffset = 0xC + 0x4;
-	gameObjectDataOffset += globalDataOffset;
-	fileStream->seek(gameObjectDataOffset + 0x17FC);
+Common::MemoryReadStream *Macs2::GameObjects::readGameObjectStrings(uint16 index, Common::SeekableReadStream *fileStream) {
+	const uint32 directoryOffset = g_engine->getMcsDirectoryOffset();
+	// Object string table pointer at directory + index*0xC + 0x17FC.
+	fileStream->seek(directoryOffset + index * 0xC + 0x17FC);
 	uint32 sceneDataOffset2 = fileStream->readUint32LE();
 	fileStream->seek(sceneDataOffset2, SEEK_SET);
 
-	// This lives in l0037_A4FC:
-
-	// Read the script from there
-	// Size - lives in global [0F84h]
 	uint16 size = fileStream->readUint16LE();
 
-	// Stringdata lives in the pointer [0F80h]
 	byte *stringData = (byte *)malloc(size);
 	fileStream->read(stringData, size);
 	return new Common::MemoryReadStream(stringData, size, DisposeAfterUse::YES);
-	// Note: We save the current scene number to [0F86h] - maybe "scene we have strings loaded for"?
 }
 
 Common::Array<uint8> *Macs2::GameObject::getAnimSlotBlob(uint16 slot) {
diff --git a/engines/macs2/gameobjects.h b/engines/macs2/gameobjects.h
index 1d0482b5602..b158190e0cb 100644
--- a/engines/macs2/gameobjects.h
+++ b/engines/macs2/gameobjects.h
@@ -29,6 +29,7 @@
 namespace Common {
 class MemoryReadStream;
 class MemoryReadStreamEndian;
+class SeekableReadStream;
 } // namespace Common
 
 namespace Macs2 {
@@ -60,10 +61,10 @@ public:
 	class Common::MemoryReadStream *_currentSceneStrings;
 	Common::Array<uint32> _currentSceneSpecialAnimOffsets;
 
-	class Common::MemoryReadStream *readSceneScript(uint16 sceneIndex, Common::MemoryReadStream *fileStream);
-	Common::Array<uint32> readSpecialAnimsOffsets(uint16 sceneIndex, Common::MemoryReadStream *fileStream);
-	class Common::MemoryReadStream *readSceneStrings(uint16 sceneIndex, Common::MemoryReadStream *fileStream);
-	Common::Array<uint8> readSpecialAnimBlob(uint16 index, Common::MemoryReadStream *fileStream);
+	class Common::MemoryReadStream *readSceneScript(uint16 sceneIndex, Common::SeekableReadStream *fileStream);
+	Common::Array<uint32> readSpecialAnimsOffsets(uint16 sceneIndex, Common::SeekableReadStream *fileStream);
+	class Common::MemoryReadStream *readSceneStrings(uint16 sceneIndex, Common::SeekableReadStream *fileStream);
+	Common::Array<uint8> readSpecialAnimBlob(uint16 index, Common::SeekableReadStream *fileStream);
 };
 
 class AnimationReader {
@@ -230,7 +231,7 @@ public:
 
 	static GameObject *getObjectByIndex(uint16 index);
 
-	static class Common::MemoryReadStream *readGameObjectStrings(uint16 index, Common::MemoryReadStream *fileStream);
+	static class Common::MemoryReadStream *readGameObjectStrings(uint16 index, Common::SeekableReadStream *fileStream);
 };
 
 } // namespace Macs2
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 970b5f82c73..44e434475ec 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -398,8 +398,7 @@ Macs2Engine::Macs2Engine(OSystem *syst, const ADGameDescription *gameDesc) : Eng
 	_scriptExecutor->_engine = this;
 	_music = new Music();
 
-	// We have a fixed 0x10 number of entries
-	_hotspotOverrides.resize(0x11);
+	_hotspotOverrides.resize(0x21);
 	for (uint i = 0; i < _hotspotOverrides.size(); i++) {
 		_hotspotOverrides[i] = 0xFFFF;
 	}
@@ -1167,7 +1166,7 @@ void Macs2Engine::setCursorMode(Script::MouseMode newMode) {
 	cursorHalfSize(oldMode, oldHalfW, oldHalfH);
 
 	Common::Point mouse = g_system->getEventManager()->getMousePos();
-	const bool mouseInUiPanel = scummVerbUI && mouse.y >= kGameHeight;
+	const bool mouseInUiPanel = scummVerbUI && mouse.y >= gameHeight();
 
 	_scriptExecutor->_cursorMode = newMode;
 
@@ -1182,8 +1181,8 @@ void Macs2Engine::setCursorMode(Script::MouseMode newMode) {
 		mouse.y -= newHalfH;
 
 		const int maxY = scummVerbUI ? (kScreenHeightLast - (int)newHalfH)
-									: (kGameHeightLast - (int)newHalfH);
-		mouse.x = CLIP<int>(mouse.x, (int)newHalfW, kScreenWidthLast - (int)newHalfW);
+									: (gameHeightLast() - (int)newHalfH);
+		mouse.x = CLIP<int>(mouse.x, (int)newHalfW, screenWidthLast() - (int)newHalfW);
 		mouse.y = CLIP<int>(mouse.y, (int)newHalfH, maxY);
 		g_system->warpMouse(mouse.x, mouse.y);
 	}
@@ -1209,8 +1208,7 @@ void Macs2Engine::setCursorMode(Script::MouseMode newMode) {
 
 uint16 Macs2Engine::getHotspotAtPoint(const Common::Point &p) {
 	uint16 result = 0;
-	// TODO: Abstract the screen sizes
-	if (p.x < 0 || p.x >= kScreenWidth || p.y < 0 || p.y >= kGameHeight || _hotspotMap.w == 0) {
+	if (p.x < 0 || p.x >= screenWidth() || p.y < 0 || p.y >= gameHeight() || _hotspotMap.w == 0) {
 		return result;
 	}
 
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index b4f22f649bc..3e3f7a23ac4 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -332,6 +332,8 @@ public:
 	int computeMinCostToReachable(int nodeIndex, int prevNode, uint16 actorIndex, const bool *reachable, int nodeCount, const Common::Point &finalDest);
 
 	// This is the override list living at [5BD1]
+	// Savegames sync 16 words into indices 1..16 (array size 0x11 during sync).
+	// Runtime capacity is 0x21 for later growth after load.
 	Common::Array<uint16> _hotspotOverrides;
 
 	Common::Array<Macs2::AnimFrame> _imageResources;
@@ -506,6 +508,13 @@ public:
 
 	bool isDemo() const { return getFeatures() & ADGF_DEMO; }
 
+	/** MCS directory base; DOS resource layout uses 0x10. */
+	uint32 getMcsDirectoryOffset() const { return 0x10; }
+	int screenWidth() const { return kScreenWidth; }
+	int screenWidthLast() const { return screenWidth() - 1; }
+	int gameHeight() const { return kGameHeight; }
+	int gameHeightLast() const { return gameHeight() - 1; }
+
 	/**
 	 * Returns the game Id
 	 */
diff --git a/engines/macs2/music.cpp b/engines/macs2/music.cpp
index 16de6a2cab5..df6e575327a 100644
--- a/engines/macs2/music.cpp
+++ b/engines/macs2/music.cpp
@@ -47,6 +47,8 @@ Music::~Music() {
 
 void Music::init() {
 	_opl = OPL::Config::create();
+	if (_opl == nullptr)
+		return;
 	_opl->init();
 	_opl->start(new Common::Functor0Mem<void, Music>(this, &Music::onTimer), CALLBACKS_PER_SECOND);
 }
@@ -91,9 +93,12 @@ void Music::silenceAll() {
 	}
 }
 
-void Music::playSongData(const Common::Array<uint8> &data) {
+bool Music::playSongData(const Common::Array<uint8> &data) {
 	stopMusic();
 
+	if (_opl == nullptr)
+		return false;
+
 	_songData = data;
 
 	_parser = new MidiParser_Macs2();
@@ -122,11 +127,12 @@ void Music::playSongData(const Common::Array<uint8> &data) {
 	if (!_parser->loadMusic(_songData.data(), _songData.size())) {
 		delete _parser;
 		_parser = nullptr;
-		return;
+		return false;
 	}
 
 	_playing = true;
 	_adlibPlaybackReady = false;
+	return true;
 }
 
 void Music::stopMusic() {
@@ -137,12 +143,15 @@ void Music::stopMusic() {
 		delete _parser;
 		_parser = nullptr;
 	}
-	silenceAll();
+	if (_opl != nullptr)
+		silenceAll();
 	_songData.clear();
 	_masterVolume = 0;
 }
 
 void Music::setVolume(uint16 volume) {
+	if (_opl == nullptr)
+		return;
 	_masterVolume = volume & 0x3F;
 }
 
diff --git a/engines/macs2/music.h b/engines/macs2/music.h
index 61e827e6199..64c5a21db10 100644
--- a/engines/macs2/music.h
+++ b/engines/macs2/music.h
@@ -39,11 +39,12 @@ class OPL;
 namespace Macs2 {
 
 /**
- * Music player for the macs2 engine.
+ * Music facade for the macs2 engine.
  *
- * Uses MidiParser_Macs2 for event parsing and drives OPL hardware directly,
- * preserving the original game's register-write behavior for authentic sound.
- * Implements MidiDriver_BASE to receive events from the MidiParser.
+ * Callers always use these methods; backend selection belongs here so additional
+ * drivers can be wired later without scattering checks across the engine.
+ *
+ * Current backend: MidiParser_Macs2 + direct OPL register writes (MidiDriver_BASE).
  */
 class Music : public MidiDriver_BASE {
 public:
@@ -53,10 +54,12 @@ public:
 	void init();
 	void deinit();
 
-	void playSongData(const Common::Array<uint8> &data);
+	/** Start song data on the active backend. Returns false if unavailable or load failed. */
+	bool playSongData(const Common::Array<uint8> &data);
 	void stopMusic();
 	void setVolume(uint16 volume);
 	bool isPlaybackReady() const { return _adlibPlaybackReady; }
+	bool hasAdlibBackend() const { return _opl != nullptr; }
 
 	void readDataFromExecutable(Common::MemoryReadStream *fileStream);
 
diff --git a/engines/macs2/saveload.cpp b/engines/macs2/saveload.cpp
index 53a3a0895e3..cb75087318e 100644
--- a/engines/macs2/saveload.cpp
+++ b/engines/macs2/saveload.cpp
@@ -357,7 +357,8 @@ Common::Error Macs2Engine::syncGame(Common::Serializer &s) {
 
 	// --- Scene data: hotspot overrides [+0x5BD3]: 32 bytes (16 x uint16) ---
 	// Binary table is at scene+i*2+0x5BD1, accessed with 1-based index (1..16).
-	// C++ array needs 17 entries (index 0 unused, indices 1-16 used).
+	// Save format is fixed at 16 words (AHFFMSGM0100). Runtime capacity may be
+	// larger; expand only after the DOS fields are restored.
 	if (s.isLoading()) {
 		_hotspotOverrides.clear();
 		_hotspotOverrides.resize(0x11, 0xFFFF);
@@ -371,6 +372,8 @@ Common::Error Macs2Engine::syncGame(Common::Serializer &s) {
 			_hotspotOverrides[i + 1] = val;
 		}
 	}
+	if (s.isLoading() && _hotspotOverrides.size() < 0x21)
+		_hotspotOverrides.resize(0x21, 0xFFFF);
 
 	for (int i = 0; i < 4; i++) {
 		s.syncAsUint32LE(_sceneTimerParams[i]);
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 9b57dd6dd81..679cb038662 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -2717,7 +2717,14 @@ void Script::ScriptExecutor::scriptPlayMusicSlot() {
 		return;
 	}
 
-	_engine->getMusic()->playSongData(_musicSlots[slotID - 1]);
+	if (!_engine->getMusic()->playSongData(_musicSlots[slotID - 1])) {
+		_musicControlMode = 0;
+		_musicControlStep = 0;
+		_musicControlVolume = 0;
+		_activeMusicSlot = slotID;
+		return;
+	}
+
 	if (startMuted == 0) {
 		_musicControlMode = 1;
 		_musicControlStep = fadeParam;
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 9c2770bf9ed..f84a74a2409 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -4387,8 +4387,8 @@ void View1::handleOriginalSaveLoadClick(const Common::Point &pos) {
 				if (g_engine->_scriptExecutor->_musicEnabled &&
 					g_engine->_scriptExecutor->_soundSystemActive) {
 					uint16 slot = g_engine->_scriptExecutor->_activeMusicSlot;
-					if (slot != 0 && !g_engine->_scriptExecutor->_musicSlots[slot - 1].empty()) {
-						g_engine->getMusic()->playSongData(g_engine->_scriptExecutor->_musicSlots[slot - 1]);
+					if (slot != 0 && !g_engine->_scriptExecutor->_musicSlots[slot - 1].empty() &&
+						g_engine->getMusic()->playSongData(g_engine->_scriptExecutor->_musicSlots[slot - 1])) {
 						// Original's adlibTickHandler resets g_bAdlibMasterVolume=0 (full volume).
 						// ScummVM layers user volume on top via scaledMusicVolume, so re-apply it.
 						g_engine->_scriptExecutor->_musicControlMode = 0;
diff --git a/engines/macs2/view1.h b/engines/macs2/view1.h
index 1950f57e986..41592ce2574 100644
--- a/engines/macs2/view1.h
+++ b/engines/macs2/view1.h
@@ -138,7 +138,7 @@ public:
 	// 2=advance sequence after returning current frame. Hit testing uses 0; drawing uses 2.
 	bool fillCurrentAnimationFrame(uint16 advanceMode, Macs2::AnimFrame &out);
 	Macs2::AnimFrame *getCurrentAnimationFrame(uint16 advanceMode);
-	Macs2::AnimFrame *getCurrentPortrait(bool onRightSide = false, uint16 frameIndex = 2);
+	Macs2::AnimFrame *getCurrentPortrait(bool onRightSide = false, uint16 frameIndex = 0);
 
 	void update();
 };


Commit: 944105844c5dbf0607a3684c64a8f69ecf4479fa
    https://github.com/scummvm/scummvm/commit/944105844c5dbf0607a3684c64a8f69ecf4479fa
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00

Commit Message:
MACS2: extract some constants into functions to prepare for the amiga demo support

Changed paths:
    engines/macs2/events.cpp
    engines/macs2/gameobjects.cpp
    engines/macs2/gameobjects.h
    engines/macs2/macs2.cpp
    engines/macs2/macs2.h
    engines/macs2/saveload.cpp
    engines/macs2/scriptexecutor.cpp
    engines/macs2/scriptexecutor.h
    engines/macs2/view1.cpp


diff --git a/engines/macs2/events.cpp b/engines/macs2/events.cpp
index e4d6b65f28d..a9acb40fbfb 100644
--- a/engines/macs2/events.cpp
+++ b/engines/macs2/events.cpp
@@ -51,10 +51,10 @@ void Events::runGame() {
 
 	// DOS: PIT ISR ~21.6Hz (divisor 0xD7B0). Main loop runs a game frame when the
 	// ISR counter exceeds 1 (every 2 ticks) -> ~10.8fps / ~92.6ms.
-	// Tick period and ticks-per-frame are locals so other platforms can retune later.
+	// Tick period / ticks-per-frame come from engine facades for later platforms.
 	uint32 lastTickTime = g_system->getMillis();
-	const uint32 kTimerTickMs = 46;
-	const uint16 kNormalTicksPerGameFrame = 2;
+	const uint32 kTimerTickMs = g_engine->timerTickMs();
+	const uint16 kNormalTicksPerGameFrame = g_engine->ticksPerGameFrame();
 	uint16 timerTickCounter = 0;
 
 	Common::Event e;
diff --git a/engines/macs2/gameobjects.cpp b/engines/macs2/gameobjects.cpp
index 6ad3304d653..28e900f95c8 100644
--- a/engines/macs2/gameobjects.cpp
+++ b/engines/macs2/gameobjects.cpp
@@ -399,11 +399,14 @@ Common::MemoryReadStream *Macs2::GameObjects::readGameObjectStrings(uint16 index
 }
 
 Common::Array<uint8> *Macs2::GameObject::getAnimSlotBlob(uint16 slot) {
-	if (slot < 1 || slot > 0x15)
+	const uint16 maxSlots = g_engine->maxAnimSlots();
+	const uint16 overloadSlot = g_engine->overloadAnimSlot();
+	if (slot < 1 || slot > maxSlots)
 		return nullptr;
-	if (slot == 0x15) {
-		if (_blobs.size() > 20 && !_blobs[20].empty())
-			return &_blobs[20];
+	if (slot == overloadSlot) {
+		const uint overloadIndex = overloadSlot - 1;
+		if (_blobs.size() > overloadIndex && !_blobs[overloadIndex].empty())
+			return &_blobs[overloadIndex];
 		return &_overloadAnimation;
 	}
 	const uint index = slot - 1;
@@ -417,17 +420,19 @@ const Common::Array<uint8> *Macs2::GameObject::getAnimSlotBlob(uint16 slot) cons
 }
 
 bool Macs2::GameObject::isAnimSlotLoaded(uint16 orient) const {
+	const uint16 overloadSlot = g_engine->overloadAnimSlot();
+	const uint16 maxOrient = g_engine->maxOrientations();
 	if (_overloadAnimTriggerDirection != 0x7FFF &&
 		(int16)_overloadAnimTriggerDirection >= 0 &&
 		_overloadAnimTriggerDirection == orient) {
-		const Common::Array<uint8> *blob = getAnimSlotBlob(0x15);
+		const Common::Array<uint8> *blob = getAnimSlotBlob(overloadSlot);
 		return blob != nullptr && !blob->empty();
 	}
-	if (orient == 0x15) {
-		const Common::Array<uint8> *blob = getAnimSlotBlob(0x15);
+	if (orient == overloadSlot) {
+		const Common::Array<uint8> *blob = getAnimSlotBlob(overloadSlot);
 		return blob != nullptr && !blob->empty();
 	}
-	if (orient < 1 || orient > 0x14)
+	if (orient < 1 || orient > maxOrient)
 		return false;
 	const uint slot = orient - 1;
 	if (slot < _blobs.size() && !_blobs[slot].empty())
diff --git a/engines/macs2/gameobjects.h b/engines/macs2/gameobjects.h
index b158190e0cb..2fc91936bb8 100644
--- a/engines/macs2/gameobjects.h
+++ b/engines/macs2/gameobjects.h
@@ -202,8 +202,8 @@ public:
 
 	Common::MemoryReadStream *getScriptStream();
 
-	// Binary pAnimSlots[1..0x15] at runtime+slot*0x10. Slot 0x15 may be in _blobs[20]
-	// (loadObjectData) or overloadAnimation (script load / savegame).
+	// Binary pAnimSlots[1..maxAnimSlots] at runtime+slot*0x10. Overload slot may be in
+	// _blobs[overload-1] (loadObjectData) or _overloadAnimation (script load / savegame).
 	Common::Array<uint8> *getAnimSlotBlob(uint16 slot);
 	const Common::Array<uint8> *getAnimSlotBlob(uint16 slot) const;
 
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 44e434475ec..879eaebcc41 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -209,7 +209,8 @@ void Macs2Engine::readResourceFile() {
 		gameObject->_orientation = _fileStream->readUint16LE();
 		gameObject->_verticalOffsetScale = _fileStream->readUint16LE();
 
-		for (int j = 1; j <= 0x15; j++) {
+		const uint16 animSlotCount = maxAnimSlots();
+		for (int j = 1; j <= (int)animSlotCount; j++) {
 			// Per-slot data in file: 2 bytes animID, 2 bytes sourceKey, 4 bytes dataSize, data, 2 bytes speed, 1 byte mirrorFlag, 1 byte (discarded)
 			_fileStream->readUint16LE(); // runtime+0x24: animation slot ID (not used at runtime, editor metadata)
 			uint16 blobSourceKey = _fileStream->readUint16LE();
@@ -266,7 +267,8 @@ void Macs2Engine::readResourceFile() {
 		_fileStream->seek(objectOffset, SEEK_SET);
 		// Resource offset table at +0x18D equivalent in file (128 bytes = 32 dword offsets).
 		// Binary loadObjectData reads this into runtime+0x18D.
-		for (int r = 0; r < 32; r++) {
+		const uint maxObjRes = maxObjectResources();
+		for (uint r = 0; r < maxObjRes; r++) {
 			gameObject->_resourceOffsets[r] = _fileStream->readUint32LE();
 		}
 		uint16 scriptLength = _fileStream->readUint16LE();
@@ -459,11 +461,8 @@ uint16 Macs2Engine::scaledMusicVolume(uint16 gameAttenuation) const {
 	return (total > 0x3F) ? 0x3F : total;
 }
 
-void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
-	// Release old scene resources
-	_backgroundAnimations.clear();
-	_backgroundAnimationsBlobs.clear();
-	memset(_areaOverrides, 0, sizeof(_areaOverrides));
+bool Macs2Engine::loadSceneGraphics(uint32 sceneIndex) {
+	const uint32 newSceneIndex = sceneIndex;
 
 	// Background image
 	// [0752h] is pointing to 3000h bytes data starting at Ch + 4h in the file
@@ -496,11 +495,11 @@ void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
 
 	uint8 data[0x320];
 
-	for (int y = 0; y < kGameHeight; y++) {
+	for (int y = 0; y < gameHeight(); y++) {
 		// TODO: Use the proper read function, it seems to be available
 		uint16 length = _fileStream->readUint16LE();
 		_fileStream->read(data, length);
-		int16 remainingPixels = kScreenWidth; // signed: see readRLEImage (matches decodeRLERows 1008:0666)
+		int16 remainingPixels = screenWidth(); // signed: see readRLEImage (matches decodeRLERows 1008:0666)
 		uint8 *dataPointer = data;
 		uint16 x = 0;
 		while (remainingPixels > 0) {
@@ -516,7 +515,7 @@ void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
 				dataPointer++;
 				const uint8 &encodedValue = dataPointer[0];
 				dataPointer++;
-				for (int i = 0; i < runlength && x < kScreenWidth; i++) {
+				for (int i = 0; i < runlength && x < screenWidth(); i++) {
 					_sceneBackground.setPixel(x++, y, encodedValue);
 				}
 				remainingPixels -= runlength;
@@ -630,6 +629,18 @@ void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
 	// Apply palette darkening if this scene has it (binary: sceneData+0x5203 != 1)
 	applyPaletteDarkening();
 
+	return true;
+}
+
+void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
+	// Release old scene resources
+	_backgroundAnimations.clear();
+	_backgroundAnimationsBlobs.clear();
+	memset(_areaOverrides, 0, sizeof(_areaOverrides));
+
+	if (!loadSceneGraphics(newSceneIndex))
+		error("changeScene(): Failed to load scene graphics for scene %u", newSceneIndex);
+
 	// Refresh characters
 	View1 *currentView = (View1 *)findView("View1");
 	if (!currentView) {
@@ -1729,7 +1740,7 @@ bool Macs2Engine::loadAnimationFromSceneData(uint16 objectIndex, uint16 slotInde
 		address = _sceneResourceOffsets[arrayIndex - 1];
 	} else {
 		GameObject *execObj = GameObjects::getObjectByIndex(executingScriptObjectId);
-		if (execObj == nullptr || arrayIndex == 0 || arrayIndex > 32) {
+		if (execObj == nullptr || arrayIndex == 0 || arrayIndex > maxObjectResources()) {
 			_scriptExecutor->setScriptError(1);
 			return false;
 		}
@@ -1747,18 +1758,20 @@ bool Macs2Engine::loadAnimationFromSceneData(uint16 objectIndex, uint16 slotInde
 	data.resize(size);
 	_fileStream->read(data.data(), size);
 
-	while (go->_blobs.size() < 0x15)
+	const uint16 minSlots = maxAnimSlots();
+	const uint16 overloadSlot = overloadAnimSlot();
+	while (go->_blobs.size() < minSlots)
 		go->_blobs.push_back(Common::Array<uint8>());
-	while (go->_blobSourceKeys.size() < 0x15)
+	while (go->_blobSourceKeys.size() < minSlots)
 		go->_blobSourceKeys.push_back(0);
-	while (go->_blobMirrorFlags.size() < 0x15)
+	while (go->_blobMirrorFlags.size() < minSlots)
 		go->_blobMirrorFlags.push_back(false);
 
 	Common::Array<uint8> *targetBlob = nullptr;
-	if (slotIndex == 0x15) {
-		while (go->_blobs.size() <= 20)
+	if (slotIndex == overloadSlot) {
+		while (go->_blobs.size() <= (uint)(overloadSlot - 1))
 			go->_blobs.push_back(Common::Array<uint8>());
-		targetBlob = &go->_blobs[20];
+		targetBlob = &go->_blobs[overloadSlot - 1];
 		go->_overloadAnimationSourceKey = static_cast<uint16>(address >> 16);
 		go->_overloadAnimationMirrored = shouldMirror;
 	} else {
@@ -1770,7 +1783,7 @@ bool Macs2Engine::loadAnimationFromSceneData(uint16 objectIndex, uint16 slotInde
 
 	// Binary: memFree old blob if bSlotLoaded, then alloc + read; sets slot+0x33 = 1.
 	*targetBlob = data;
-	if (slotIndex == 0x15)
+	if (slotIndex == overloadSlot)
 		go->_overloadAnimation = data;
 	if (shouldMirror) {
 		BackgroundAnimationBlob::mirrorAnimBlob(*targetBlob);
@@ -1851,7 +1864,8 @@ bool Macs2Engine::loadObjectData(GameObject *obj) {
 		obj->_blobMirrorFlags = mirrorsBackup;
 	};
 
-	for (int j = 0; j < 0x15; j++) {
+	const uint16 animSlotCount = maxAnimSlots();
+	for (int j = 0; j < (int)animSlotCount; j++) {
 		_fileStream->readUint16LE(); // animID (editor metadata, unused at runtime)
 		uint16 blobSourceKey = _fileStream->readUint16LE();
 		uint32 dataSize = _fileStream->readUint32LE();
@@ -1940,7 +1954,8 @@ bool Macs2Engine::loadObjectData(GameObject *obj) {
 	const uint32 scriptOffset = _fileStream->readUint32LE();
 	if (scriptOffset != 0) {
 		_fileStream->seek(scriptOffset, SEEK_SET);
-		for (int r = 0; r < 32; r++) {
+		const uint maxObjRes = maxObjectResources();
+		for (uint r = 0; r < maxObjRes; r++) {
 			obj->_resourceOffsets[r] = _fileStream->readUint32LE();
 		}
 		const uint16 scriptLength = _fileStream->readUint16LE();
@@ -1997,6 +2012,16 @@ Common::String Macs2Engine::getGameId() const {
 	return _gameDescription->gameId;
 }
 
+uint16 Macs2Engine::resolveAnimSlotIndex(const GameObject *obj) const {
+	if (obj == nullptr)
+		return 0;
+	if ((int16)obj->_overloadAnimTriggerDirection < 0 ||
+		obj->_overloadAnimTriggerDirection != obj->_orientation) {
+		return obj->_orientation;
+	}
+	return overloadAnimSlot();
+}
+
 void Macs2Engine::setGameSpeedMode(uint16 mode) {
 	_gameSpeedMode = mode % 3;
 	ConfMan.setInt(kGameSpeedModeConfigKey, _gameSpeedMode);
@@ -2005,7 +2030,7 @@ void Macs2Engine::setGameSpeedMode(uint16 mode) {
 Common::Error Macs2Engine::run() {
 	GameObjects::instance().init();
 	setGameSpeedMode(ConfMan.getInt(kGameSpeedModeConfigKey));
-	readResourceFile();
+	loadBootstrapResources();
 	readExecutable();
 
 	// Load translation data if available
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index 3e3f7a23ac4..8066c0ff9b4 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -515,6 +515,53 @@ public:
 	int gameHeight() const { return kGameHeight; }
 	int gameHeightLast() const { return gameHeight() - 1; }
 
+	// --- Layout / dialect facades (DOS defaults; other platforms override later) ---
+
+	/** Game-loop timer quantum in milliseconds. */
+	uint32 timerTickMs() const { return 46; }
+	/** Normal-speed: game frames per that many timer ticks. */
+	uint16 ticksPerGameFrame() const { return 2; }
+
+	/** ReadyObject anim slots (1-based inclusive max). */
+	uint16 maxAnimSlots() const { return 0x15; }
+	/** Orientations that map to anim slots 1..N (inclusive). */
+	uint16 maxOrientations() const { return 0x14; }
+	/** Overload / special-anim slot index (DOS ReadyObject slot 0x15). */
+	uint16 overloadAnimSlot() const { return 0x15; }
+	/** Scene hotspot override table entries (1-based inclusive max). */
+	uint16 maxHotspots() const { return 0x10; }
+	/** Per-object resource offset table entries. */
+	uint maxObjectResources() const { return 32; }
+	/** Anim slot used for the current orientation (DOS overload-direction rule). */
+	uint16 resolveAnimSlotIndex(const GameObject *obj) const;
+
+	/** Script stream: literals carry an extra high word after the value word. */
+	bool scriptValuesHaveHighWord() const { return false; }
+	/** Script stream: variable index is followed by a padding word. */
+	bool scriptVarIndexHasPaddingWord() const { return false; }
+	/** Script coordinates → screen/runtime coordinates (identity on DOS). */
+	int16 scaleScriptCoord(int16 coord) const { return coord; }
+
+	/** Dialogue / text-box chrome (DOS l0037_B368 / B462). */
+	int dialogPadW() const { return 0x12; }
+	int dialogPadH() const { return 0x10; }
+	int dialogTextInset() const { return 0x09; }
+	int dialogLineGap() const { return 2; }
+	int portraitBorderPad() const { return 0x0D; }
+	int portraitContentInset() const { return 7; }
+	int portraitTextGap() const { return 0x12; }
+
+	/** Depth-map compare Y for sprite occlusion (full Y on DOS). */
+	uint8 depthThresholdForY(int16 charY) const { return (uint8)charY; }
+
+	/** Resource bootstrap (MCS fonts/cursors/shading + first scene). */
+	void loadBootstrapResources() { readResourceFile(); }
+	/**
+	 * Load scene background, maps, pathfinding, and related scene tables.
+	 * Called from changeScene; platforms can replace this body later.
+	 */
+	bool loadSceneGraphics(uint32 sceneIndex);
+
 	/**
 	 * Returns the game Id
 	 */
diff --git a/engines/macs2/saveload.cpp b/engines/macs2/saveload.cpp
index cb75087318e..3dd1ea0ae4c 100644
--- a/engines/macs2/saveload.cpp
+++ b/engines/macs2/saveload.cpp
@@ -704,15 +704,16 @@ Common::Error Macs2Engine::syncGame(Common::Serializer &s) {
 
 		// Script resource table [+0x18D]: 0x80 bytes (128 bytes = 32 dword offsets)
 		// Stored in GameObject::_resourceOffsets, loaded from file during readResourceFile.
-		byte scriptResourceTable[128] = {0};
+		const uint maxObjRes = maxObjectResources();
+		byte scriptResourceTable[128] = {0}; // DOS save table is always 0x80 bytes
 		if (s.isSaving()) {
-			for (int r = 0; r < 32; r++) {
+			for (uint r = 0; r < maxObjRes; r++) {
 				WRITE_LE_UINT32(&scriptResourceTable[r * 4], obj->_resourceOffsets[r]);
 			}
 		}
 		s.syncBytes(scriptResourceTable, 128);
 		if (s.isLoading()) {
-			for (int r = 0; r < 32; r++) {
+			for (uint r = 0; r < maxObjRes; r++) {
 				obj->_resourceOffsets[r] = READ_LE_UINT32(&scriptResourceTable[r * 4]);
 			}
 		}
@@ -729,9 +730,11 @@ Common::Error Macs2Engine::syncGame(Common::Serializer &s) {
 		//   +0x00: 2 bytes (frame cursor X), +0x02: 2 bytes (frame cursor Y),
 		//   +0x0C: 2 bytes (source key), +0x0E: 2 bytes (speed),
 		//   +0x04: 2 bytes (data size), then data_size bytes of pixel data.
-		for (int blobIdx = 0; blobIdx < 0x15; blobIdx++) {
-			// Slot 0x15 (blobIdx 20) is the overload animation, stored separately
-			bool isOverloadSlot = (blobIdx == 20);
+		const int animSlotCount = (int)maxAnimSlots();
+		const int overloadBlobIdx = (int)overloadAnimSlot() - 1;
+		for (int blobIdx = 0; blobIdx < animSlotCount; blobIdx++) {
+			// Overload slot is stored separately from normal orientation blobs.
+			bool isOverloadSlot = (blobIdx == overloadBlobIdx);
 
 			// Active flag (2 bytes, but only low byte matters)
 			uint16 blobActive = 0;
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 679cb038662..0ff40622f5a 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -186,10 +186,13 @@ void ScriptExecutor::scriptReadValuePair(uint16 &out1, uint16 &out2) {
 
 	byte type = readByte();
 	uint16 value = readUint16();
+	uint16 high = 0;
+	if (_engine->scriptValuesHaveHighWord())
+		high = readUint16();
 
 	if (type == 0x00) {
 		out1 = value;
-		out2 = 0;
+		out2 = high;
 		return;
 	}
 
@@ -226,6 +229,15 @@ uint16 ScriptExecutor::scriptReadValue16() {
 	return out1;
 }
 
+int16 ScriptExecutor::scriptReadCoord16() {
+	return _engine->scaleScriptCoord((int16)scriptReadValue16());
+}
+
+void ScriptExecutor::skipOptionalVarIndexPadding() {
+	if (_engine->scriptVarIndexHasPaddingWord())
+		(void)readUint16();
+}
+
 void ScriptExecutor::scriptSaveVariableHelper(uint32 value) {
 	uint8 subOpcode = readByte();
 	if (subOpcode == 0x00 || subOpcode == 0xFF) {
@@ -234,6 +246,7 @@ void ScriptExecutor::scriptSaveVariableHelper(uint32 value) {
 	}
 
 	uint16 variableID = readUint16();
+	skipOptionalVarIndexPadding();
 	debugC(kDebugScript, "SCRIPT::saveVariable(subOpcode=0x%02x, variableID=%u, value=%u)", subOpcode, variableID, value);
 	setVariableValue(variableID, value);
 }
@@ -316,8 +329,8 @@ bool ScriptExecutor::loadIndexedResource(Common::Array<uint8> &outData, uint8 re
 			return false;
 		}
 		// Binary reads from runtime+0x18D table (loaded during loadObjectData).
-		// Table is 32 dword file offsets, indexed by (resourceIndex - 1).
-		if (resourceIndex - 1 >= 32) {
+		// Table is maxObjectResources() dword file offsets, indexed by (resourceIndex - 1).
+		if ((uint)(resourceIndex - 1) >= _engine->maxObjectResources()) {
 			warning("Ignoring resource load for out-of-range index %u on object %u", resourceIndex, _executingScriptObjectId);
 			return false;
 		}
@@ -381,7 +394,7 @@ void ScriptExecutor::scriptPrintString(bool alignRight) {
 	int stringBoxX = x;
 	const int stringBoxY = y;
 	if (alignRight) {
-		const int totalWidth = g_engine->measureStrings(strings) + 0x12;
+		const int totalWidth = g_engine->measureStrings(strings) + g_engine->dialogPadW();
 		stringBoxX -= totalWidth;
 	}
 
@@ -732,6 +745,7 @@ void Script::ScriptExecutor::scriptSetVar() {
 	// This writes to a script variable.
 	(void)readByte();
 	uint16 variableIndex = readUint16();
+	skipOptionalVarIndexPadding();
 	ScriptVariable var;
 	scriptReadValuePair(var.a, var.b);
 	debugC(kDebugScript, "SCRIPT::setVar(variableIndex=%u, value1=%u, value2=%u)", variableIndex, var.a, var.b);
@@ -742,6 +756,7 @@ void Script::ScriptExecutor::scriptSetVarOr() {
 	// Padding/type byte (same as opcode 0x01) - read and discarded
 	(void)readByte();
 	uint16 variableIndex = readUint16();
+	skipOptionalVarIndexPadding();
 	// We skip the left shift and just read the first value directly
 	uint16 throwaway;
 	uint16 value1;
@@ -884,8 +899,8 @@ void Script::ScriptExecutor::scriptMoveObject() {
 	// Handles render list updates for both source and destination scenes.
 	const uint32 objectID = scriptReadValue32() - 0x400;
 	const uint16 sceneID = scriptReadValue16();
-	const int16 x = (int16)scriptReadValue16();
-	const int16 y = (int16)scriptReadValue16();
+	const int16 x = scriptReadCoord16();
+	const int16 y = scriptReadCoord16();
 	debugC(kDebugScript, "SCRIPT::moveObject(objectID=%u, sceneID=%u, x=%d, y=%d)", objectID, sceneID, x, y);
 
 	if (objectID < 1 || objectID > 0x200) {
@@ -1287,8 +1302,8 @@ void Script::ScriptExecutor::scriptWalkToPosition() {
 	// Sets up runtime movement state. Does NOT block - walkAlongPath handles
 	// actual movement per-frame from the game tick.
 	const uint32 objectID = scriptReadValue32() - 0x400;
-	const int16 x = (int16)scriptReadValue16();
-	const int16 y = (int16)scriptReadValue16();
+	const int16 x = scriptReadCoord16();
+	const int16 y = scriptReadCoord16();
 	debugC(kDebugScript, "SCRIPT::walkToPosition(objectID=%u, x=%d, y=%d)", objectID, x, y);
 
 	clearScriptError();
@@ -1618,12 +1633,10 @@ void Script::ScriptExecutor::scriptSetupObject() {
 		return;
 	}
 
-	if (slotID < 1 || slotID > 0x15) {
+	if (slotID < 1 || slotID > _engine->maxAnimSlots()) {
 		setScriptError(0x10);
 		return;
 	}
-
-	// Binary: runtime+slot*0x10+0x33 (bSlotLoaded) must be set.
 	if (!object->isAnimSlotLoaded(slotID)) {
 		setScriptError(0x10);
 		return;
@@ -1668,7 +1681,7 @@ void Script::ScriptExecutor::scriptPlayAnimation() {
 		setScriptError(2);
 		return;
 	}
-	if (slotID < 1 || slotID > 0x15) {
+	if (slotID < 1 || slotID > _engine->maxAnimSlots()) {
 		setScriptError(0x10);
 		return;
 	}
@@ -1952,9 +1965,9 @@ void Script::ScriptExecutor::scriptMoveToPosition() {
 	// Opcode 0x23 scriptMoveToPosition (1008:bafc): seeds runtime walk state directly;
 	// does not pathfind or use time-based lerp.
 	const int32 objectID = (int32)scriptReadValue32() - 0x400;
-	const int16 x = (int16)scriptReadValue16();
-	const int16 y = (int16)scriptReadValue16();
-	const uint16 targetVerticalOffset = scriptReadValue16();
+	const int16 x = scriptReadCoord16();
+	const int16 y = scriptReadCoord16();
+	const uint16 targetVerticalOffset = (uint16)_engine->scaleScriptCoord((int16)scriptReadValue16());
 	debugC(kDebugScript, "SCRIPT::moveToPosition(objectID=%d, target=(%d,%d), targetVerticalOffset=%u)", objectID, x, y, targetVerticalOffset);
 
 	clearScriptError();
@@ -2148,7 +2161,7 @@ void Script::ScriptExecutor::scriptLoadObjectAnim() {
 		setScriptError(2);
 		return;
 	}
-	if (slotID < 1 || slotID > 0x15) {
+	if (slotID < 1 || slotID > _engine->maxAnimSlots()) {
 		setScriptError(0x13);
 		return;
 	}
@@ -2277,7 +2290,7 @@ void Script::ScriptExecutor::scriptTestObjectAnimFrame() {
 		setScriptError(2);
 		return;
 	}
-	if (slotID < 1 || slotID > 0x15) {
+	if (slotID < 1 || slotID > _engine->maxAnimSlots()) {
 		setScriptError(0x10);
 		return;
 	}
@@ -2374,7 +2387,8 @@ void Script::ScriptExecutor::scriptSetHotspotOverride() {
 	debugC(kDebugScript, "SCRIPT::setHotspotOverride(hotspot=%u, override=%u)", v1, v2);
 
 	clearScriptError();
-	if (v1 < 1 || v1 > 0x10 || v2 < 1 || v2 > 0x10) {
+	const uint16 maxHotspot = _engine->maxHotspots();
+	if (v1 < 1 || v1 > maxHotspot || v2 < 1 || v2 > maxHotspot) {
 		setScriptError(0x1e);
 		return;
 	}
@@ -2629,14 +2643,13 @@ void Script::ScriptExecutor::scriptLoadPcmSound() {
 		return;
 	}
 
-	if (_engine->hasCurrentSound() && _soundEnabled) {
-		_engine->stopCurrentSound();
-	}
+	// Opcode 0x3E only installs the buffer; playback is opcode 0x40.
 	_engine->setCurrentSoundData(soundData);
 }
 
 void Script::ScriptExecutor::scriptPlayPcmSound() {
 	// Binary (1000:0c7f): no error path; no-op when Sound Blaster disabled.
+	// Buffer was installed by opcode 0x3E; only start playback here.
 	if (_soundEnabled) {
 		if (!_engine->hasCurrentSound()) {
 			warning("Opcode 0x40: playPcmSound with no loaded sound data");
diff --git a/engines/macs2/scriptexecutor.h b/engines/macs2/scriptexecutor.h
index b4a9b0b7aa5..9f37e4ae2b1 100644
--- a/engines/macs2/scriptexecutor.h
+++ b/engines/macs2/scriptexecutor.h
@@ -265,6 +265,12 @@ private:
 	// Returns only the first of the two 16 bit values
 	uint16 scriptReadValue16();
 
+	/** Read a script value and convert to screen/runtime coordinates. */
+	int16 scriptReadCoord16();
+
+	/** Skip optional padding word after a script variable index when required. */
+	void skipOptionalVarIndexPadding();
+
 	// Saves the given value in a script variable
 	void scriptSaveVariableHelper(uint32 value);
 
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index f84a74a2409..f29395847f4 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -44,15 +44,6 @@ constexpr int kNumLoadedCursors = 33;
 // Binary: local_4 = 1..7, each iteration reads *(local_4 * 2 + 0x26) as index into cursor array
 const uint16 kLookupTable[8] = {9, 15, 14, 27, 29, 16, 17, 9}; // index 0 unused
 
-// drawAllCharacters (1008:90a2) local_14: animation slot for current orientation.
-uint16 resolveAnimSlotIndex(const GameObject *obj) {
-	if ((int16)obj->_overloadAnimTriggerDirection < 0 ||
-		obj->_overloadAnimTriggerDirection != obj->_orientation) {
-		return obj->_orientation;
-	}
-	return 0x15;
-}
-
 Common::String joinDebugStrings(const Common::StringArray &strings) {
 	Common::String result;
 	for (uint i = 0; i < strings.size(); ++i) {
@@ -583,11 +574,13 @@ void View1::drawCurrentSpeaker(Graphics::ManagedSurface &s) {
 	// Draw the border
 	const int portraitWidth = MAX<int>(leftPortrait ? leftPortrait->_width : 0, rightPortrait ? rightPortrait->_width : 0);
 	const int portraitHeight = MAX<int>(leftPortrait ? leftPortrait->_height : 0, rightPortrait ? rightPortrait->_height : 0);
-	const Common::Point borderSize(portraitWidth + 0xD, portraitHeight + 0xD);
+	const int borderPad = g_engine->portraitBorderPad();
+	const int contentInset = g_engine->portraitContentInset();
+	const Common::Point borderSize(portraitWidth + borderPad, portraitHeight + borderPad);
 	drawBorder(currentSpeechActData.position, borderSize, s);
 
 	// Draw the portrait over the border
-	Common::Point pos = currentSpeechActData.position + Common::Point(7, 7);
+	Common::Point pos = currentSpeechActData.position + Common::Point(contentInset, contentInset);
 	drawSprite(pos, frame->_width, frame->_height, frame->_data.data(), s, false);
 	delete frame;
 	delete leftPortrait;
@@ -739,8 +732,12 @@ void View1::showStringBox(const Common::StringArray &sa) {
 	// This calculation can be found at l0037_B368:
 	// int borderWidth = 10;
 	// int padding = 3;
-	int totalWidth = g_engine->measureStrings(sa) + 0x12;
-	int totalHeight = g_engine->measureStringsVertically(sa) + 0x10;
+	const int padW = g_engine->dialogPadW();
+	const int padH = g_engine->dialogPadH();
+	const int textInset = g_engine->dialogTextInset();
+	const int lineGap = g_engine->dialogLineGap();
+	int totalWidth = g_engine->measureStrings(sa) + padW;
+	int totalHeight = g_engine->measureStringsVertically(sa) + padH;
 	g_engine->_textLog.push_back(Common::String::format(
 									 "Render text box: lines=%u pos=(%d,%d) size=(%d,%d) text=\"", sa.size(),
 									 _stringBoxPosition.x, _stringBoxPosition.y, totalWidth, totalHeight) +
@@ -749,11 +746,11 @@ void View1::showStringBox(const Common::StringArray &sa) {
 	Graphics::ManagedSurface s = getSurface();
 	drawBorder(_stringBoxPosition, Common::Point(totalWidth, totalHeight), s);
 	// TODO range based
-	int lineOffset = _stringBoxPosition.y + 0x9;
+	int lineOffset = _stringBoxPosition.y + textInset;
 	for (auto iter = sa.begin(); iter < sa.end(); iter++) {
-		logRenderedText("TextBox", _stringBoxPosition.x + 0x9, lineOffset, *iter);
-		renderString(_stringBoxPosition.x + 0x9, lineOffset, *iter);
-		lineOffset += g_engine->maxGlyphHeight + 2;
+		logRenderedText("TextBox", _stringBoxPosition.x + textInset, lineOffset, *iter);
+		renderString(_stringBoxPosition.x + textInset, lineOffset, *iter);
+		lineOffset += g_engine->maxGlyphHeight + lineGap;
 	}
 }
 
@@ -940,15 +937,18 @@ bool View1::handleDialogueChoiceClick(int clickY, int clickX) {
 	// Checks if click is within text box bounds (X+9..X+W-9, Y+9..Y+H-9).
 	// Iterates choice entries to find which line was clicked.
 	// Stores script index at scene+0x53B7 and clears scene+0x53B9.
-	const int boxW = g_engine->measureStrings(_drawnStringBox) + 0x12;
-	const int boxH = g_engine->measureStringsVertically(_drawnStringBox) + 0x10;
-	if (clickX < _stringBoxPosition.x + 9 || clickY < _stringBoxPosition.y + 9 ||
-		clickX > _stringBoxPosition.x + boxW - 9 || clickY > _stringBoxPosition.y + boxH - 9) {
+	const int padW = g_engine->dialogPadW();
+	const int padH = g_engine->dialogPadH();
+	const int textInset = g_engine->dialogTextInset();
+	const int boxW = g_engine->measureStrings(_drawnStringBox) + padW;
+	const int boxH = g_engine->measureStringsVertically(_drawnStringBox) + padH;
+	if (clickX < _stringBoxPosition.x + textInset || clickY < _stringBoxPosition.y + textInset ||
+		clickX > _stringBoxPosition.x + boxW - textInset || clickY > _stringBoxPosition.y + boxH - textInset) {
 		return false;
 	}
 
-	int lineHeight = g_engine->maxGlyphHeight + 2;
-	int firstLineY = _stringBoxPosition.y + 9;
+	int lineHeight = g_engine->maxGlyphHeight + g_engine->dialogLineGap();
+	int firstLineY = _stringBoxPosition.y + textInset;
 	int relY = clickY - firstLineY;
 	debug("handleDialogueChoiceClick: clickY=%d firstLineY=%d relY=%d lineHeight=%d clickedLine=%d",
 		  clickY, firstLineY, relY, lineHeight, relY >= 0 ? relY / lineHeight : -1);
@@ -2571,7 +2571,7 @@ void View1::drawAllCharacters(Graphics::ManagedSurface *surface, bool fullUpdate
 				}
 			}
 
-			const uint16 animSlot = resolveAnimSlotIndex(obj);
+			const uint16 animSlot = g_engine->resolveAnimSlotIndex(obj);
 
 			if (!obj->isAnimSlotLoaded(animSlot)) {
 				executor->setScriptError(10);
@@ -2647,7 +2647,7 @@ void View1::drawAllCharacters(Graphics::ManagedSurface *surface, bool fullUpdate
 
 			const int16 drawX = charX - (frameWidth >> 1) + frame._offsetX;
 			const int16 drawY = (charY - frameHeight) - walkabilityOffset + frame._offsetY;
-			const uint8 depthThreshold = (uint8)charY;
+			const uint8 depthThreshold = g_engine->depthThresholdForY(charY);
 			const byte *pixelData = frame._data.data();
 
 			// drawAllCharacters @ 1008:9573-9754: drawAnimFrame / drawAnimFrameShaded / drawAnimFrameDepth
@@ -3087,8 +3087,11 @@ void View1::showSpeechAct(uint16 characterIndex, const Common::Array<Common::Str
 	currentSpeechActData.position = position;
 	currentSpeechActData.onRightSide = onRightSide;
 
-	const int totalWidth = g_engine->measureStrings(strings) + 0x12;
-	const int totalHeight = g_engine->measureStringsVertically(strings) + 0x10;
+	const int padW = g_engine->dialogPadW();
+	const int padH = g_engine->dialogPadH();
+	const int portraitGap = g_engine->portraitTextGap();
+	const int totalWidth = g_engine->measureStrings(strings) + padW;
+	const int totalHeight = g_engine->measureStringsVertically(strings) + padH;
 	int stringBoxX = position.x;
 	int stringBoxY = position.y;
 	Common::Point portraitBoxPosition = position;
@@ -3099,10 +3102,10 @@ void View1::showSpeechAct(uint16 characterIndex, const Common::Array<Common::Str
 		const int portraitWidth = MAX<int>(leftPortrait ? leftPortrait->_width : 0, rightPortrait ? rightPortrait->_width : 0);
 		if (portraitWidth > 0) {
 			if (onRightSide) {
-				stringBoxX = position.x - portraitWidth - 0x12 - totalWidth;
+				stringBoxX = position.x - portraitWidth - portraitGap - totalWidth;
 				portraitBoxPosition.x = stringBoxX + totalWidth + 4;
 			} else {
-				stringBoxX = position.x + portraitWidth + 0x12;
+				stringBoxX = position.x + portraitWidth + portraitGap;
 			}
 		}
 		delete leftPortrait;
@@ -3653,7 +3656,7 @@ uint8 Character::getMirroredAnimation(uint8 original) const {
 }
 
 bool Character::fillCurrentAnimationFrame(uint16 advanceMode, Macs2::AnimFrame &out) {
-	const uint16 animSlot = resolveAnimSlotIndex(_gameObject);
+	const uint16 animSlot = g_engine->resolveAnimSlotIndex(_gameObject);
 
 	_shouldMirrorCurrentAnimation = false;
 
@@ -3822,7 +3825,7 @@ void Character::update() {
 	// = AnimSlot.wAnimSpeed (slot+0x0C). Stored in _blobWalkSpeeds.
 	uint16 animSpeed = 2; // default fallback
 	uint8 orient = _gameObject->_orientation;
-	if (orient >= 1 && orient <= 0x15 && (uint)(orient - 1) < _gameObject->_blobWalkSpeeds.size()) {
+	if (orient >= 1 && orient <= g_engine->maxAnimSlots() && (uint)(orient - 1) < _gameObject->_blobWalkSpeeds.size()) {
 		animSpeed = _gameObject->_blobWalkSpeeds[orient - 1];
 		if (animSpeed == 0)
 			animSpeed = 2;


Commit: f86c012f352a0078f18b9904e2609a630a4d0fe5
    https://github.com/scummvm/scummvm/commit/f86c012f352a0078f18b9904e2609a630a4d0fe5
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00

Commit Message:
MACS2: minor cleanup in renaming methods and removing loadSongFromSceneData

Changed paths:
    engines/macs2/macs2.cpp
    engines/macs2/macs2.h
    engines/macs2/scriptexecutor.cpp
    engines/macs2/view1.cpp


diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 879eaebcc41..0a70026e2f2 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -1967,44 +1967,34 @@ bool Macs2Engine::loadObjectData(GameObject *obj) {
 	return true;
 }
 
-void Macs2Engine::loadSongFromSceneData(uint8 dataIndex) {
-	uint32 address = _sceneResourceOffsets[dataIndex - 1];
-	_fileStream->seek(address);
-	uint32 size = _fileStream->readUint32LE();
-	Common::Array<uint8> data;
-	data.resize(size);
-	_fileStream->read(data.data(), size);
-	_music->playSongData(data);
-}
-
 void Macs2Engine::setCurrentSoundData(const Common::Array<uint8> &data) {
-	stopCurrentSound();
+	stopSample();
 	_currentSoundData = data;
 }
 
 void Macs2Engine::clearCurrentSoundData() {
-	stopCurrentSound();
+	stopSample();
 	_currentSoundData.clear();
 }
 
-void Macs2Engine::playCurrentSound() {
+void Macs2Engine::playSample() {
 	if (_currentSoundData.empty())
 		return;
 
-	stopCurrentSound();
+	stopSample();
 	MacsAudioStream *audioStream = new MacsAudioStream();
 	audioStream->_pos = 2; // Skip 2-byte header (original: size = stored_size - 2)
 	audioStream->_data = _currentSoundData;
 	g_system->getMixer()->playStream(Audio::Mixer::kSFXSoundType, &_currentSoundHandle, audioStream);
 }
 
-void Macs2Engine::stopCurrentSound() {
+void Macs2Engine::stopSample() {
 	Audio::Mixer *mixer = g_system->getMixer();
 	if (mixer->isSoundHandleActive(_currentSoundHandle))
 		mixer->stopHandle(_currentSoundHandle);
 }
 
-bool Macs2Engine::isCurrentSoundPlaying() const {
+bool Macs2Engine::isSamplePlaying() const {
 	return g_system->getMixer()->isSoundHandleActive(_currentSoundHandle);
 }
 
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index 8066c0ff9b4..1f41d9ae5af 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -414,16 +414,15 @@ public:
 	// sortObjectsByDepth @ 1008:0d79 - inventory cursor reset + free object runtime blobs.
 	void sortObjectsByDepth(uint16 objectIndex);
 
-	void loadSongFromSceneData(uint8 dataIndex);
 	Music *getMusic() const { return _music; }
 	// Returns the Music volume (0-63) scaled by the user's music_volume setting
 	uint16 scaledMusicVolume(uint16 gameAttenuation) const;
 	void setCurrentSoundData(const Common::Array<uint8> &data);
 	void clearCurrentSoundData();
-	void playCurrentSound();
-	void stopCurrentSound();
-	bool hasCurrentSound() const { return !_currentSoundData.empty(); }
-	bool isCurrentSoundPlaying() const;
+	bool hasCurrentSoundData() const { return !_currentSoundData.empty(); }
+	void playSample();
+	void stopSample();
+	bool isSamplePlaying() const;
 
 	// Offset 50D3h - This is used in 0037:10C4 to terminate the loop
 	uint16 _numHotspots;
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 0ff40622f5a..98acad5af50 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -2651,11 +2651,11 @@ void Script::ScriptExecutor::scriptPlayPcmSound() {
 	// Binary (1000:0c7f): no error path; no-op when Sound Blaster disabled.
 	// Buffer was installed by opcode 0x3E; only start playback here.
 	if (_soundEnabled) {
-		if (!_engine->hasCurrentSound()) {
+		if (!_engine->hasCurrentSoundData()) {
 			warning("Opcode 0x40: playPcmSound with no loaded sound data");
 			return;
 		}
-		_engine->playCurrentSound();
+		_engine->playSample();
 	}
 }
 
@@ -2665,7 +2665,7 @@ bool Script::ScriptExecutor::scriptWaitForSound() {
 		endTimer();
 		endBuffering(_lastOpcodeTriggeredSkip);
 		debugC(kDebugScript, "SCRIPT::waitForSound start (soundPlaying=%d hasSound=%d)",
-			   g_engine->isCurrentSoundPlaying() ? 1 : 0, g_engine->hasCurrentSound() ? 1 : 0);
+			   g_engine->isSamplePlaying() ? 1 : 0, g_engine->hasCurrentSoundData() ? 1 : 0);
 		debugLogActorWalkState("waitForSound start");
 		enterBlockingWaitCursor();
 		return true;
@@ -2677,7 +2677,7 @@ bool Script::ScriptExecutor::scriptWaitForSound() {
 
 void Script::ScriptExecutor::scriptStopPcmSound() {
 	if (_soundEnabled) {
-		_engine->stopCurrentSound();
+		_engine->stopSample();
 	}
 }
 
@@ -2923,7 +2923,7 @@ bool Script::ScriptExecutor::scriptWaitForAdlib() {
 void Script::ScriptExecutor::scriptFreePcmSound() {
 	debugC(kDebugScript, "SCRIPT::freePcmSound()");
 	if (_soundEnabled)
-		_engine->stopCurrentSound();
+		_engine->stopSample();
 	_engine->clearCurrentSoundData();
 }
 
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index f29395847f4..9153dcf4fd1 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -2438,7 +2438,7 @@ bool View1::tick() {
 				}
 			} else if (executor->_waitForPcmSound) {
 				drawSceneUpdate();
-				if (!g_engine->isCurrentSoundPlaying()) {
+				if (!g_engine->isSamplePlaying()) {
 					debugC(kDebugScript, "waitForSound complete");
 					executor->debugLogActorWalkState("waitForSound complete");
 					executor->_waitForPcmSound = false;


Commit: ab78c9292a61f1ecc545df81f41930f6cbd6f3d7
    https://github.com/scummvm/scummvm/commit/ab78c9292a61f1ecc545df81f41930f6cbd6f3d7
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00

Commit Message:
MACS2: added amiga demo detection

Changed paths:
    engines/macs2/detection_tables.h


diff --git a/engines/macs2/detection_tables.h b/engines/macs2/detection_tables.h
index 5f35ddefe00..98884576a61 100644
--- a/engines/macs2/detection_tables.h
+++ b/engines/macs2/detection_tables.h
@@ -83,6 +83,16 @@ const ADGameDescription gameDescriptions[] = {
 	 GF_TRANSLATED | ADGF_DEMO | ADGF_UNSTABLE,
 	 GUIO3(GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_TTS, GAMEOPTION_ENHANCEMENTS)},
 
+	// Amiga demo - DataA (MXMF) + Mdir (MXDR). GMACS II / LINEL.
+	{"sis",
+	 "Demo",
+	 AD_ENTRY2s("DataA", "30ce5b1b30f7ef60f412c0475a25b0cb", 736670,
+	            "Mdir", "269f4c31a50395e3ec6ae69b86e426bd", 598),
+	 Common::DE_DEU,
+	 Common::kPlatformAmiga,
+	 ADGF_DEMO | ADGF_UNSTABLE,
+	 GUIO3(GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_TTS, GAMEOPTION_ENHANCEMENTS)},
+
 	AD_TABLE_END_MARKER};
 
 } // End of namespace Macs2


Commit: f33e897885eed8d3c0ea522dff9725c216c1ae00
    https://github.com/scummvm/scummvm/commit/f33e897885eed8d3c0ea522dff9725c216c1ae00
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00

Commit Message:
MACS2: fill some of the platform specific magic values for the amiga

Changed paths:
    engines/macs2/macs2.cpp
    engines/macs2/macs2.h
    engines/macs2/view1.cpp


diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 0a70026e2f2..6dff774d495 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -415,6 +415,7 @@ Macs2Engine::~Macs2Engine() {
 	_music->deinit();
 	delete _music;
 	delete _fileStream;
+	_fileStream = nullptr;
 	delete _scriptExecutor;
 	for (uint i = 0; i < GameObjects::instance()._objects.size(); i++) {
 		delete GameObjects::instance()._objects[i];
@@ -1539,8 +1540,8 @@ int Macs2Engine::measureString(const Common::String &s) {
 }
 
 int Macs2Engine::measureStringsVertically(const Common::StringArray &sa) {
-	// This is implemented around l0037_B318:
-	return sa.size() * (maxGlyphHeight + 2);
+	// DOS l0037_B318: maxGlyphHeight + 2. Amiga uses absolute MXFF line pitch.
+	return (int)sa.size() * dialogLineHeight();
 }
 
 int Macs2Engine::measureStrings(const Common::StringArray &sa) {
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index 1f41d9ae5af..d6b598ae18f 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -368,7 +368,10 @@ public:
 	Common::Array<BackgroundAnimation> _backgroundAnimations;
 	Common::Array<BackgroundAnimationBlob> _backgroundAnimationsBlobs;
 
-	Common::MemoryReadStream *_fileStream;
+	Common::MemoryReadStream *_fileStream = nullptr;
+
+	/** Amiga MXFF line pitch: measureTextWidth @ 00224420 uses (font[+8] - 1). */
+	uint16 amigaTextLinePitch = 0;
 
 	void setCursorMode(Script::MouseMode newMode);
 	void nextCursorMode();
@@ -504,6 +507,8 @@ public:
 	int computeStringIndex(Common::MemoryReadStream *stream, int targetOffset);
 
 	uint32 getFeatures() const;
+	Common::Platform getPlatform() const { return _gameDescription->platform; }
+	bool isAmiga() const { return getPlatform() == Common::kPlatformAmiga; }
 
 	bool isDemo() const { return getFeatures() & ADGF_DEMO; }
 
@@ -542,13 +547,23 @@ public:
 	int16 scaleScriptCoord(int16 coord) const { return coord; }
 
 	/** Dialogue / text-box chrome (DOS l0037_B368 / B462). */
-	int dialogPadW() const { return 0x12; }
-	int dialogPadH() const { return 0x10; }
-	int dialogTextInset() const { return 0x09; }
+	int dialogPadW() const { return isAmiga() ? 0x08 : 0x12; }
+	int dialogPadH() const { return isAmiga() ? 0x08 : 0x10; }
+	int dialogTextInset() const { return isAmiga() ? 0x04 : 0x09; }
 	int dialogLineGap() const { return 2; }
-	int portraitBorderPad() const { return 0x0D; }
-	int portraitContentInset() const { return 7; }
-	int portraitTextGap() const { return 0x12; }
+	int portraitBorderPad() const { return isAmiga() ? 2 : 0x0D; }
+	int portraitContentInset() const { return isAmiga() ? 1 : 7; }
+	int portraitTextGap() const { return isAmiga() ? 0x0A : 0x12; }
+	/**
+	 * Per-line step for dialogue layout.
+	 * DOS: maxGlyphHeight + dialogLineGap(). Amiga: absolute MXFF pitch
+	 * (amigaTextLinePitch) or maxGlyphHeight when pitch is unset.
+	 */
+	int dialogLineHeight() const {
+		if (isAmiga())
+			return amigaTextLinePitch ? (int)amigaTextLinePitch : (int)maxGlyphHeight;
+		return (int)maxGlyphHeight + dialogLineGap();
+	}
 
 	/** Depth-map compare Y for sprite occlusion (full Y on DOS). */
 	uint8 depthThresholdForY(int16 charY) const { return (uint8)charY; }
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 9153dcf4fd1..f928bb3e308 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -735,7 +735,7 @@ void View1::showStringBox(const Common::StringArray &sa) {
 	const int padW = g_engine->dialogPadW();
 	const int padH = g_engine->dialogPadH();
 	const int textInset = g_engine->dialogTextInset();
-	const int lineGap = g_engine->dialogLineGap();
+	const int lineHeight = g_engine->dialogLineHeight();
 	int totalWidth = g_engine->measureStrings(sa) + padW;
 	int totalHeight = g_engine->measureStringsVertically(sa) + padH;
 	g_engine->_textLog.push_back(Common::String::format(
@@ -750,7 +750,7 @@ void View1::showStringBox(const Common::StringArray &sa) {
 	for (auto iter = sa.begin(); iter < sa.end(); iter++) {
 		logRenderedText("TextBox", _stringBoxPosition.x + textInset, lineOffset, *iter);
 		renderString(_stringBoxPosition.x + textInset, lineOffset, *iter);
-		lineOffset += g_engine->maxGlyphHeight + lineGap;
+		lineOffset += lineHeight;
 	}
 }
 
@@ -947,7 +947,7 @@ bool View1::handleDialogueChoiceClick(int clickY, int clickX) {
 		return false;
 	}
 
-	int lineHeight = g_engine->maxGlyphHeight + g_engine->dialogLineGap();
+	int lineHeight = g_engine->dialogLineHeight();
 	int firstLineY = _stringBoxPosition.y + textInset;
 	int relY = clickY - firstLineY;
 	debug("handleDialogueChoiceClick: clickY=%d firstLineY=%d relY=%d lineHeight=%d clickedLine=%d",
@@ -2146,8 +2146,8 @@ void View1::draw() {
 	if (_isShowingTextBox || _isShowingDialoguePanel) {
 		showStringBox(_drawnStringBox);
 		if (_isDialogueChoiceInputActive && g_engine->enhancementEnabled(kEnhUIUX)) {
-			int lineHeight = g_engine->maxGlyphHeight + 2;
-			int firstLineY = _stringBoxPosition.y + 9;
+			int lineHeight = g_engine->dialogLineHeight();
+			int firstLineY = _stringBoxPosition.y + g_engine->dialogTextInset();
 			int relY = mousePos.y - firstLineY;
 			if (relY >= 0) {
 				int hoveredLine = relY / lineHeight;


Commit: b6556791e7432e1b2c79f37a940c3023eb9dd78f
    https://github.com/scummvm/scummvm/commit/b6556791e7432e1b2c79f37a940c3023eb9dd78f
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00

Commit Message:
MACS2: amiga string decoding is different

Changed paths:
    engines/macs2/detection_tables.h
    engines/macs2/macs2.cpp


diff --git a/engines/macs2/detection_tables.h b/engines/macs2/detection_tables.h
index 98884576a61..89a02b9d0c8 100644
--- a/engines/macs2/detection_tables.h
+++ b/engines/macs2/detection_tables.h
@@ -93,6 +93,17 @@ const ADGameDescription gameDescriptions[] = {
 	 ADGF_DEMO | ADGF_UNSTABLE,
 	 GUIO3(GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_TTS, GAMEOPTION_ENHANCEMENTS)},
 
+	// Amiga demo - English fan translation
+	{"sis",
+	 "Demo/w/translation",
+	 AD_ENTRY3s("DataA", "30ce5b1b30f7ef60f412c0475a25b0cb", 736670,
+	            "Mdir", "269f4c31a50395e3ec6ae69b86e426bd", 598,
+	            "macs2_translation.dat", nullptr, AD_NO_SIZE),
+	 Common::EN_ANY,
+	 Common::kPlatformAmiga,
+	 GF_TRANSLATED | ADGF_DEMO | ADGF_UNSTABLE,
+	 GUIO3(GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_TTS, GAMEOPTION_ENHANCEMENTS)},
+
 	AD_TABLE_END_MARKER};
 
 } // End of namespace Macs2
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 6dff774d495..bb838051a8c 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -1556,7 +1556,8 @@ int Macs2Engine::computeStringIndex(Common::MemoryReadStream *stream, int target
 	stream->seek(0);
 	int index = 0;
 	while (stream->pos() < targetOffset && !stream->eos()) {
-		uint16 len = stream->readUint16LE();
+		// DOS: u16LE + XOR ciphertext. Amiga: u16BE + plaintext.
+		const uint16 len = isAmiga() ? stream->readUint16BE() : stream->readUint16LE();
 		if (len == 0)
 			break;
 		stream->skip(len);
@@ -1681,22 +1682,28 @@ Common::StringArray Macs2Engine::decodeStrings(Common::MemoryReadStream *stream,
 	Common::StringArray result(numStrings);
 	stream->seek(offset);
 
-	byte x;
-	byte y;
-	byte r;
-
-	for (int i = 0; i < numStrings; i++) {
-		Common::String currentLine;
-		uint16 length = stream->readUint16LE();
-		byte currentByte;
-		for (int index = 1; index < length + 1; index++) {
-			currentByte = stream->readByte();
-			x = (byte)(index * index * 0x0c);
-			y = (byte)(currentByte ^ index);
-			r = (byte)(x ^ y);
-			currentLine += (char)r;
-		}
-		result[i] = currentLine;
+	if (isAmiga()) {
+		// Amiga strings: u16BE length + plaintext (Latin-1), no XOR cipher.
+		for (int i = 0; i < numStrings; i++) {
+			Common::String currentLine;
+			const uint16 length = stream->readUint16BE();
+			for (uint16 index = 0; index < length; index++)
+				currentLine += (char)stream->readByte();
+			result[i] = currentLine;
+		}
+	} else {
+		for (int i = 0; i < numStrings; i++) {
+			Common::String currentLine;
+			uint16 length = stream->readUint16LE();
+			for (int index = 1; index < length + 1; index++) {
+				const byte currentByte = stream->readByte();
+				const byte x = (byte)(index * index * 0x0c);
+				const byte y = (byte)(currentByte ^ index);
+				const byte r = (byte)(x ^ y);
+				currentLine += (char)r;
+			}
+			result[i] = currentLine;
+		}
 	}
 
 	// Apply translation if available


Commit: 14601bf9948d8ad0aa018368e0245dddd0c3f1d3
    https://github.com/scummvm/scummvm/commit/14601bf9948d8ad0aa018368e0245dddd0c3f1d3
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00

Commit Message:
MACS2: resize of the top of the function

Changed paths:
    engines/macs2/macs2.cpp


diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index bb838051a8c..54b23acfa68 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -293,6 +293,9 @@ void Macs2Engine::readResourceFile() {
 }
 
 void Macs2Engine::readExecutable() {
+	inventoryIconIndices.resize(6);
+	containerInventoryIconIndices.resize(6);
+
 	Common::ScopedPtr<Common::MemoryReadStream> exeFileStream;
 	{
 		// Extra scope in order to make sure no code tries to read from the file directly.
@@ -310,11 +313,9 @@ void Macs2Engine::readExecutable() {
 	_music->readDataFromExecutable(exeFileStream.get());
 
 	exeFileStream->seek(0x0001B610, SEEK_SET);
-	inventoryIconIndices.resize(6);
 	exeFileStream->read(inventoryIconIndices.data(), 12);
 
 	exeFileStream->seek(0x0001B61C, SEEK_SET);
-	containerInventoryIconIndices.resize(6);
 	exeFileStream->read(containerInventoryIconIndices.data(), 12);
 }
 


Commit: 2764981a74e1c9f8bc601d5d357331e091a48fdd
    https://github.com/scummvm/scummvm/commit/2764981a74e1c9f8bc601d5d357331e091a48fdd
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00

Commit Message:
MACS2: don't read data from exe for amiga

Changed paths:
    engines/macs2/macs2.cpp


diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 54b23acfa68..4e5630e5790 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -296,6 +296,14 @@ void Macs2Engine::readExecutable() {
 	inventoryIconIndices.resize(6);
 	containerInventoryIconIndices.resize(6);
 
+	if (isAmiga()) {
+		for (uint i = 0; i < 6; i++) {
+			inventoryIconIndices[i] = (uint16)(i + 1);
+			containerInventoryIconIndices[i] = (uint16)(i + 1);
+		}
+		return;
+	}
+
 	Common::ScopedPtr<Common::MemoryReadStream> exeFileStream;
 	{
 		// Extra scope in order to make sure no code tries to read from the file directly.


Commit: 7d36e8edeb3cb6c789e7ea040b6746f21a897765
    https://github.com/scummvm/scummvm/commit/7d36e8edeb3cb6c789e7ea040b6746f21a897765
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00

Commit Message:
MACS2: music is (or will be) a facade not just for adlib

Changed paths:
    engines/macs2/macs2.cpp


diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 4e5630e5790..f0edc2e33a8 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -2050,11 +2050,8 @@ Common::Error Macs2Engine::run() {
 
 	CursorMan.showMouse(false);
 
-	// Initialize Adlib
 	_music->init();
 	syncSoundSettings();
-
-	// Set the engine's debugger console
 	setDebugger(new GUI::Debugger());
 
 #ifdef USE_IMGUI


Commit: 0484fcbbf8ee0e548d1a5e2ee284a2b4d23da034
    https://github.com/scummvm/scummvm/commit/0484fcbbf8ee0e548d1a5e2ee284a2b4d23da034
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00

Commit Message:
MACS2: leave a comment for my future me regarding full version and demo version offsets

Changed paths:
    engines/macs2/macs2.cpp


diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index f0edc2e33a8..fbbc5feb388 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -318,6 +318,10 @@ void Macs2Engine::readExecutable() {
 		exeFileStream.reset(new Common::MemoryReadStream(fileData, size, DisposeAfterUse::YES));
 	}
 
+	// Full MCSEXEC.EXE and demo MCSEXEC.EXE are different binaries (different MD5, ~12k differing bytes),
+	// but the whole Data5 segment is identical (1020:0000...1020:3787)
+	// TODO: if there are ever other games using different versions of MCSEXEC.EXE, we should check the checksum here
+
 	_music->readDataFromExecutable(exeFileStream.get());
 
 	exeFileStream->seek(0x0001B610, SEEK_SET);


Commit: 1a6023f75481cdd09e2c008a11ed36dd63b58227
    https://github.com/scummvm/scummvm/commit/1a6023f75481cdd09e2c008a11ed36dd63b58227
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00

Commit Message:
MACS2: opcode table

to easier install different jump tables for different platforms or games

Changed paths:
    engines/macs2/debugtools.cpp
    engines/macs2/scriptexecutor.cpp
    engines/macs2/scriptexecutor.h


diff --git a/engines/macs2/debugtools.cpp b/engines/macs2/debugtools.cpp
index b9ea027eb96..50019205a44 100644
--- a/engines/macs2/debugtools.cpp
+++ b/engines/macs2/debugtools.cpp
@@ -87,166 +87,9 @@ static const char *getSpecialName(uint16 val) {
 }
 
 static const char *getOpcodeName(uint8 opcode) {
-	switch (opcode) {
-	case 0x01:
-		return "setVar";
-	case 0x02:
-		return "setVarOr";
-	case 0x03:
-		return "ifFalse";
-	case 0x04:
-		return "ifTrue";
-	case 0x05:
-		return "compare";
-	case 0x06:
-		return "ifInteraction";
-	case 0x07:
-		return "endIf";
-	case 0x08:
-		return "else";
-	case 0x09:
-		return "nop09";
-	case 0x0A:
-		return "printStringLeft";
-	case 0x0B:
-		return "moveObject";
-	case 0x0C:
-		return "changeScene";
-	case 0x0D:
-		return "showDialogue";
-	case 0x0E:
-		return "changeAnimation";
-	case 0x0F:
-		return "frameWait";
-	case 0x10:
-		return "walkToPosition";
-	case 0x11:
-		return "waitForWalk";
-	case 0x12:
-		return "setPathfinding";
-	case 0x13:
-		return "skipUntil14";
-	case 0x14:
-		return "skipWord";
-	case 0x15:
-		return "clearDialogueChoices";
-	case 0x16:
-		return "addDialogueChoice";
-	case 0x17:
-		return "showDialogueChoice";
-	case 0x18:
-		return "dismissPanel";
-	case 0x19:
-		return "walkToAndPickup";
-	case 0x1A:
-		return "setPickupFrames";
-	case 0x1B:
-		return "setupObject";
-	case 0x1C:
-		return "setSkippable";
-	case 0x1D:
-		return "clearSkippable";
-	case 0x1E:
-		return "playAnimation";
-	case 0x1F:
-		return "pathWalkable = testPathfinding";
-	case 0x20:
-		return "setYOffset";
-	case 0x21:
-		return "setMotion";
-	case 0x22:
-		return "setOrientation";
-	case 0x23:
-		return "moveToPosition";
-	case 0x24:
-		return "addValues";
-	case 0x25:
-		return "subValues";
-	case 0x26:
-		return "loadSpecialAnim";
-	case 0x27:
-		return "setDirection";
-	case 0x28:
-		return "stopAnimation";
-	case 0x29:
-		return "openInventory";
-	case 0x2A:
-		return "loadObjectAnim";
-	case 0x2B:
-		return "checkObjectData";
-	case 0x2C:
-		return "invCheck = checkInventory";
-	case 0x2D:
-		return "setSnapToTarget";
-	case 0x2E:
-		return "animRangeTest = testSceneAnimFrame";
-	case 0x2F:
-		return "animRangeTest = testObjectAnimFrame";
-	case 0x30:
-		return "printStringRight";
-	case 0x31:
-		return "setPaletteDarkness";
-	case 0x32:
-		return "setObjectShading";
-	case 0x33:
-		return "setObjectScaling";
-	case 0x34:
-		return "setHotspotOverride";
-	case 0x35:
-		return "setObjectBounds";
-	case 0x36:
-		return "dismissAllPanels";
-	case 0x37:
-		return "resetToSceneScript";
-	case 0x38:
-		return "loadOverlayFont";
-	case 0x39:
-		return "endOverlayText";
-	case 0x3A:
-		return "addOverlayTextEntry";
-	case 0x3B:
-		return "clearOverlayText";
-	case 0x3C:
-		return "fadeToBlack";
-	case 0x3D:
-		return "fadeFromBlack";
-	case 0x3E:
-		return "loadPcmSound";
-	case 0x3F:
-		return "freePcmSound";
-	case 0x40:
-		return "playPcmSound";
-	case 0x41:
-		return "waitForSound";
-	case 0x42:
-		return "stopPcmSound";
-	case 0x43:
-		return "loadMusicSlot";
-	case 0x44:
-		return "playMusicSlot";
-	case 0x45:
-		return "stopMusicSlot";
-	case 0x46:
-		return "freeMusicSlot";
-	case 0x47:
-		return "waitForMusic";
-	case 0x48:
-		return "getObjectX";
-	case 0x49:
-		return "getObjectY";
-	case 0x4A:
-		return "getObjectField8";
-	case 0x4B:
-		return "getObjectOrientation";
-	case 0x4C:
-		return "clearActorInventory";
-	case 0x4D:
-		return "setPathfindingRemap";
-	case 0x4E:
-		return "waitForAdlib";
-	default:
-		return "???";
-	}
+	if (g_engine && g_engine->_scriptExecutor)
+		return g_engine->_scriptExecutor->opcodeName(opcode);
+	return "?";
 }
 
 static const char *getCompareOpName(uint8 sub) {
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 98acad5af50..ae2fd77d217 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -58,6 +58,8 @@ ScriptExecutor::ScriptExecutor() {
 		_variables[i].a = 0;
 		_variables[i].b = 0;
 	}
+
+	setOpcodeTable(ScriptExecutor::kDosOpcodeTable, ScriptExecutor::kDosOpcodeTableSize);
 }
 
 ScriptExecutor::~ScriptExecutor() {
@@ -67,10 +69,8 @@ ScriptExecutor::~ScriptExecutor() {
 }
 
 Common::String ScriptExecutor::identifyScriptOpcode(uint8 opcode, uint8 opcode2) {
-	if (opcode == 0x5)
-		return Common::String::format("(%.2x)", opcode);
-
-	return Common::String::format("(%.2x %.2x)", opcode, opcode2);
+	(void)opcode2;
+	return Common::String::format("%s (%.2x)", opcodeName(opcode), opcode);
 }
 
 Common::String ScriptExecutor::identifyHelperOpcode(uint8 opcode, uint16 value) {
@@ -2927,6 +2927,516 @@ void Script::ScriptExecutor::scriptFreePcmSound() {
 	_engine->clearCurrentSoundData();
 }
 
+void ScriptExecutor::setOpcodeTable(const OpcodeEntry *table, uint size) {
+	_opcodeTable = table;
+	_opcodeTableSize = size;
+}
+
+const char *ScriptExecutor::opcodeName(uint8 opcode) const {
+	if (_opcodeTable == nullptr || opcode >= _opcodeTableSize)
+		return "?";
+	const char *name = _opcodeTable[opcode].name;
+	return name ? name : "?";
+}
+
+OpcodeResult ScriptExecutor::opSetVar() {
+	scriptSetVar();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetVarOr() {
+	scriptSetVarOr();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opIfFalse() {
+	scriptIfFalse();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opIfTrue() {
+	scriptIfTrue();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opCompare() {
+	if (!scriptCompare())
+		return OpcodeResult::FinishScript;
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opIfInteraction() {
+	scriptIfInteraction();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opEndIf() {
+	scriptEndIf();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opElse() {
+	scriptElse();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opNop09() {
+	scriptNop09();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opPrintStringLeft() {
+	scriptPrintStringLeft();
+	return OpcodeResult::WaitForCallback;
+}
+
+OpcodeResult ScriptExecutor::opMoveObject() {
+	scriptMoveObject();
+	if (hasScriptError())
+		return OpcodeResult::FinishScript;
+	// Binary: after scriptMoveObject(), exits if position >= end.
+	if (_stream->pos() >= _stream->size())
+		return OpcodeResult::FinishScript;
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opChangeScene() {
+	const ExecutionResult result = scriptChangeScene();
+	if (result == ExecutionResult::WaitingForCallback)
+		return OpcodeResult::WaitForCallback;
+	return OpcodeResult::ReturnFinished;
+}
+
+OpcodeResult ScriptExecutor::opShowDialogue() {
+	const ExecutionResult dialogueResult = scriptShowDialogue();
+	if (dialogueResult == ExecutionResult::WaitingForCallback)
+		return OpcodeResult::WaitForCallback;
+	return OpcodeResult::FinishScript;
+}
+
+OpcodeResult ScriptExecutor::opChangeAnimation() {
+	scriptChangeAnimation();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opFrameWait() {
+	scriptFrameWait();
+	return OpcodeResult::WaitForCallback;
+}
+
+OpcodeResult ScriptExecutor::opWalkToPosition() {
+	scriptWalkToPosition();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opWaitForWalk() {
+	const ExecutionResult result = scriptWaitForWalk();
+	if (result == ExecutionResult::WaitingForCallback)
+		return OpcodeResult::WaitForCallback;
+	return OpcodeResult::ReturnFinished;
+}
+
+OpcodeResult ScriptExecutor::opSetPathfinding() {
+	scriptSetPathfinding();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSkipUntil14() {
+	scriptSkipUntil14();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSkipWord() {
+	scriptSkipWord();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opClearDialogueChoices() {
+	scriptClearDialogueChoices();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opAddDialogueChoice() {
+	scriptAddDialogueChoice();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opShowDialogueChoice() {
+	const ExecutionResult result = scriptShowDialogueChoice();
+	if (result == ExecutionResult::WaitingForCallback)
+		return OpcodeResult::WaitForCallback;
+	return OpcodeResult::ReturnFinished;
+}
+
+OpcodeResult ScriptExecutor::opDismissPanel() {
+	const ExecutionResult result = scriptDismissPanel();
+	if (result == ExecutionResult::WaitingForCallback)
+		return OpcodeResult::WaitForCallback;
+	return OpcodeResult::ReturnFinished;
+}
+
+OpcodeResult ScriptExecutor::opWalkToAndPickup() {
+	scriptWalkToAndPickup();
+	return OpcodeResult::FinishScript;
+}
+
+OpcodeResult ScriptExecutor::opSetPickupFrames() {
+	scriptSetPickupFrames();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetupObject() {
+	scriptSetupObject();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetSkippable() {
+	scriptSetSkippable();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opClearSkippable() {
+	scriptClearSkippable();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opPlayAnimation() {
+	scriptPlayAnimation();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opTestPathfinding() {
+	scriptTestPathfinding();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetYOffset() {
+	scriptSetYOffset();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetMotion() {
+	scriptSetMotion();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetOrientation() {
+	scriptSetOrientation();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opMoveToPosition() {
+	scriptMoveToPosition();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opAddValues() {
+	scriptAddValues();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSubValues() {
+	scriptSubValues();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opLoadSpecialAnim() {
+	scriptLoadSpecialAnim();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetDirection() {
+	scriptSetDirection();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opStopAnimation() {
+	scriptStopAnimation();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opOpenInventory() {
+	// Binary (1008:e0f9): always terminates script + sets executingObjectId sentinel.
+	scriptOpenInventory();
+	_stream->seek(_stream->size(), SEEK_SET);
+	_executingObjectIndex = 0x201;
+	_executingScriptObjectId = 0x201;
+	return OpcodeResult::FinishScript;
+}
+
+OpcodeResult ScriptExecutor::opLoadObjectAnim() {
+	scriptLoadObjectAnim();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opCheckObjectData() {
+	scriptCheckObjectData();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opCheckInventory() {
+	scriptCheckInventory();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetSnapToTarget() {
+	scriptSetSnapToTarget();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opTestSceneAnimFrame() {
+	scriptTestSceneAnimFrame();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opTestObjectAnimFrame() {
+	scriptTestObjectAnimFrame();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opPrintStringRight() {
+	scriptPrintStringRight();
+	return OpcodeResult::WaitForCallback;
+}
+
+OpcodeResult ScriptExecutor::opSetPaletteDarkness() {
+	scriptSetPaletteDarkness();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetObjectShading() {
+	scriptSetObjectShading();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetObjectScaling() {
+	scriptSetObjectScaling();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetHotspotOverride() {
+	scriptSetHotspotOverride();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetObjectBounds() {
+	scriptSetObjectBounds();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opDismissAllPanels() {
+	scriptDismissAllPanels();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opResetToSceneScript() {
+	scriptResetToSceneScript();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opLoadOverlayFont() {
+	scriptLoadOverlayFont();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opEndOverlayText() {
+	scriptEndOverlayText();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opAddOverlayTextEntry() {
+	scriptAddOverlayTextEntry();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opClearOverlayText() {
+	scriptClearOverlayText();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opFadeToBlack() {
+	scriptFadeToBlack();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opFadeFromBlack() {
+	scriptFadeFromBlack();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opLoadPcmSound() {
+	scriptLoadPcmSound();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opFreePcmSound() {
+	scriptFreePcmSound();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opPlayPcmSound() {
+	scriptPlayPcmSound();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opWaitForSound() {
+	if (scriptWaitForSound())
+		return OpcodeResult::WaitForCallback;
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opStopPcmSound() {
+	scriptStopPcmSound();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opLoadMusicSlot() {
+	scriptLoadMusicSlot();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opPlayMusicSlot() {
+	scriptPlayMusicSlot();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opStopMusicSlot() {
+	scriptStopMusicSlot();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opFreeMusicSlot() {
+	scriptFreeMusicSlot();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opWaitForMusic() {
+	if (scriptWaitForMusic())
+		return OpcodeResult::WaitForCallback;
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opGetObjectX() {
+	scriptGetObjectX();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opGetObjectY() {
+	scriptGetObjectY();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opGetObjectField8() {
+	scriptGetObjectField8();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opGetObjectOrientation() {
+	scriptGetObjectOrientation();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opClearActorInventory() {
+	scriptClearActorInventory();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetPathfindingRemap() {
+	scriptSetPathfindingRemap();
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opWaitForAdlib() {
+	if (scriptWaitForAdlib())
+		return OpcodeResult::WaitForCallback;
+	return OpcodeResult::Continue;
+}
+
+// DOS script opcode table (indexed by primary opcode byte).
+// Platforms can install an alternate table via setOpcodeTable().
+const ScriptExecutor::OpcodeEntry ScriptExecutor::kDosOpcodeTable[] = {
+	{nullptr, nullptr},
+	{"setVar", &ScriptExecutor::opSetVar},
+	{"setVarOr", &ScriptExecutor::opSetVarOr},
+	{"ifFalse", &ScriptExecutor::opIfFalse},
+	{"ifTrue", &ScriptExecutor::opIfTrue},
+	{"compare", &ScriptExecutor::opCompare},
+	{"ifInteraction", &ScriptExecutor::opIfInteraction},
+	{"endIf", &ScriptExecutor::opEndIf},
+	{"else", &ScriptExecutor::opElse},
+	{"nop09", &ScriptExecutor::opNop09},
+	{"printStringLeft", &ScriptExecutor::opPrintStringLeft},
+	{"moveObject", &ScriptExecutor::opMoveObject},
+	{"changeScene", &ScriptExecutor::opChangeScene},
+	{"showDialogue", &ScriptExecutor::opShowDialogue},
+	{"changeAnimation", &ScriptExecutor::opChangeAnimation},
+	{"frameWait", &ScriptExecutor::opFrameWait},
+	{"walkToPosition", &ScriptExecutor::opWalkToPosition},
+	{"waitForWalk", &ScriptExecutor::opWaitForWalk},
+	{"setPathfinding", &ScriptExecutor::opSetPathfinding},
+	{"skipUntil14", &ScriptExecutor::opSkipUntil14},
+	{"skipWord", &ScriptExecutor::opSkipWord},
+	{"clearDialogueChoices", &ScriptExecutor::opClearDialogueChoices},
+	{"addDialogueChoice", &ScriptExecutor::opAddDialogueChoice},
+	{"showDialogueChoice", &ScriptExecutor::opShowDialogueChoice},
+	{"dismissPanel", &ScriptExecutor::opDismissPanel},
+	{"walkToAndPickup", &ScriptExecutor::opWalkToAndPickup},
+	{"setPickupFrames", &ScriptExecutor::opSetPickupFrames},
+	{"setupObject", &ScriptExecutor::opSetupObject},
+	{"setSkippable", &ScriptExecutor::opSetSkippable},
+	{"clearSkippable", &ScriptExecutor::opClearSkippable},
+	{"playAnimation", &ScriptExecutor::opPlayAnimation},
+	{"testPathfinding", &ScriptExecutor::opTestPathfinding},
+	{"setYOffset", &ScriptExecutor::opSetYOffset},
+	{"setMotion", &ScriptExecutor::opSetMotion},
+	{"setOrientation", &ScriptExecutor::opSetOrientation},
+	{"moveToPosition", &ScriptExecutor::opMoveToPosition},
+	{"addValues", &ScriptExecutor::opAddValues},
+	{"subValues", &ScriptExecutor::opSubValues},
+	{"loadSpecialAnim", &ScriptExecutor::opLoadSpecialAnim},
+	{"setDirection", &ScriptExecutor::opSetDirection},
+	{"stopAnimation", &ScriptExecutor::opStopAnimation},
+	{"openInventory", &ScriptExecutor::opOpenInventory},
+	{"loadObjectAnim", &ScriptExecutor::opLoadObjectAnim},
+	{"checkObjectData", &ScriptExecutor::opCheckObjectData},
+	{"checkInventory", &ScriptExecutor::opCheckInventory},
+	{"setSnapToTarget", &ScriptExecutor::opSetSnapToTarget},
+	{"testSceneAnimFrame", &ScriptExecutor::opTestSceneAnimFrame},
+	{"testObjectAnimFrame", &ScriptExecutor::opTestObjectAnimFrame},
+	{"printStringRight", &ScriptExecutor::opPrintStringRight},
+	{"setPaletteDarkness", &ScriptExecutor::opSetPaletteDarkness},
+	{"setObjectShading", &ScriptExecutor::opSetObjectShading},
+	{"setObjectScaling", &ScriptExecutor::opSetObjectScaling},
+	{"setHotspotOverride", &ScriptExecutor::opSetHotspotOverride},
+	{"setObjectBounds", &ScriptExecutor::opSetObjectBounds},
+	{"dismissAllPanels", &ScriptExecutor::opDismissAllPanels},
+	{"resetToSceneScript", &ScriptExecutor::opResetToSceneScript},
+	{"loadOverlayFont", &ScriptExecutor::opLoadOverlayFont},
+	{"endOverlayText", &ScriptExecutor::opEndOverlayText},
+	{"addOverlayTextEntry", &ScriptExecutor::opAddOverlayTextEntry},
+	{"clearOverlayText", &ScriptExecutor::opClearOverlayText},
+	{"fadeToBlack", &ScriptExecutor::opFadeToBlack},
+	{"fadeFromBlack", &ScriptExecutor::opFadeFromBlack},
+	{"loadPcmSound", &ScriptExecutor::opLoadPcmSound},
+	{"freePcmSound", &ScriptExecutor::opFreePcmSound},
+	{"playPcmSound", &ScriptExecutor::opPlayPcmSound},
+	{"waitForSound", &ScriptExecutor::opWaitForSound},
+	{"stopPcmSound", &ScriptExecutor::opStopPcmSound},
+	{"loadMusicSlot", &ScriptExecutor::opLoadMusicSlot},
+	{"playMusicSlot", &ScriptExecutor::opPlayMusicSlot},
+	{"stopMusicSlot", &ScriptExecutor::opStopMusicSlot},
+	{"freeMusicSlot", &ScriptExecutor::opFreeMusicSlot},
+	{"waitForMusic", &ScriptExecutor::opWaitForMusic},
+	{"getObjectX", &ScriptExecutor::opGetObjectX},
+	{"getObjectY", &ScriptExecutor::opGetObjectY},
+	{"getObjectField8", &ScriptExecutor::opGetObjectField8},
+	{"getObjectOrientation", &ScriptExecutor::opGetObjectOrientation},
+	{"clearActorInventory", &ScriptExecutor::opClearActorInventory},
+	{"setPathfindingRemap", &ScriptExecutor::opSetPathfindingRemap},
+	{"waitForAdlib", &ScriptExecutor::opWaitForAdlib}
+};
+const uint ScriptExecutor::kDosOpcodeTableSize = ARRAYSIZE(ScriptExecutor::kDosOpcodeTable);
+
 ExecutionResult Script::ScriptExecutor::executeOpcodes() {
 	debugC(kDebugScript, "----- Scripting function entered - scene: %.2x 1014: %.2x 1012: %.2x", Scenes::instance()._currentSceneIndex, _isSceneInitRun, _repeatRunFlag);
 	_isRunningScript = true;
@@ -2984,206 +3494,25 @@ ExecutionResult Script::ScriptExecutor::executeOpcodes() {
 		_lastOpcodeStreamPos = _stream->pos() - 2;
 		_expectedEndLocation += length + 2;
 
-		// TODO: convert this into a function lookup table and extract all opcode handling into separate functions, this is just for easier reading
-		if (opcode1 == 0x01) {
-			scriptSetVar();
-		} else if (opcode1 == 0x02) {
-			scriptSetVarOr();
-		} else if (opcode1 == 0x03) {
-			scriptIfFalse();
-		} else if (opcode1 == 0x04) {
-			scriptIfTrue();
-		} else if (opcode1 == 0x5) {
-			if (!scriptCompare()) {
-				endBuffering(_lastOpcodeTriggeredSkip);
-				break;
-			}
-		} else if (opcode1 == 0x06) {
-			scriptIfInteraction();
-		} else if (opcode1 == 0x07) {
-			scriptEndIf();
-		} else if (opcode1 == 0x08) {
-			scriptElse();
-		} else if (opcode1 == 0x09) {
-			scriptNop09();
-		} else if (opcode1 == 0x10) {
-			scriptWalkToPosition();
-		} else if (opcode1 == 0x11) {
-			return scriptWaitForWalk();
-		} else if (opcode1 == 0x13) {
-			scriptSkipUntil14();
-		} else if (opcode1 == 0x14) {
-			scriptSkipWord();
-		} else if (opcode1 == 0x0a) {
-			scriptPrintStringLeft();
-			return ExecutionResult::WaitingForCallback;
-		} else if (opcode1 == 0x15) {
-			scriptClearDialogueChoices();
-		} else if (opcode1 == 0x16) {
-			scriptAddDialogueChoice();
-		} else if (opcode1 == 0x17) {
-			return scriptShowDialogueChoice();
-		} else if (opcode1 == 0x18) {
-			return scriptDismissPanel();
-		} else if (opcode1 == 0x19) {
-			scriptWalkToAndPickup();
-			endBuffering(_lastOpcodeTriggeredSkip);
-			break;
-		} else if (opcode1 == 0x1a) {
-			scriptSetPickupFrames();
-		} else if (opcode1 == 0x1b) {
-			scriptSetupObject();
-		} else if (opcode1 == 0x1c) {
-			scriptSetSkippable();
-		} else if (opcode1 == 0x1d) {
-			scriptClearSkippable();
-		} else if (opcode1 == 0x1e) {
-			scriptPlayAnimation();
-		} else if (opcode1 == 0x1f) {
-			scriptTestPathfinding();
-		} else if (opcode1 == 0x20) {
-			scriptSetYOffset();
-		} else if (opcode1 == 0x21) {
-			scriptSetMotion();
-		} else if (opcode1 == 0x22) {
-			scriptSetOrientation();
-		} else if (opcode1 == 0x23) {
-			scriptMoveToPosition();
-		} else if (opcode1 == 0x24) {
-			scriptAddValues();
-		} else if (opcode1 == 0x25) {
-			scriptSubValues();
-		} else if (opcode1 == 0x26) {
-			scriptLoadSpecialAnim();
-		} else if (opcode1 == 0x27) {
-			scriptSetDirection();
-		} else if (opcode1 == 0x28) {
-			scriptStopAnimation();
-		} else if (opcode1 == 0x29) {
-			// Binary (1008:e0f9): always terminates script + sets executingObjectId sentinel.
-			scriptOpenInventory();
-			_stream->seek(_stream->size(), SEEK_SET);
-			_executingObjectIndex = 0x201;
-			_executingScriptObjectId = 0x201;
-			endBuffering(_lastOpcodeTriggeredSkip);
-			break;
-		} else if (opcode1 == 0x0b) {
-			scriptMoveObject();
-			if (hasScriptError()) {
-				endBuffering(_lastOpcodeTriggeredSkip);
-				break;
-			}
-			// Binary: after scriptMoveObject(), exits if position >= end.
-			// scriptMoveObject can advance the stream to end (e.g. if the moved
-			// object is the one whose script is executing).
-			if (_stream->pos() >= _stream->size()) {
-				endBuffering(_lastOpcodeTriggeredSkip);
-				break;
-			}
-		} else if (opcode1 == 0x0c) {
-			// Binary: scriptChangeScene (1008:ad6e) runs init+repeat synchronously and
-			// returns void; only pause the outer opcode loop when a wait is still active.
-			return scriptChangeScene();
-		} else if (opcode1 == 0x0d) {
-			const ExecutionResult dialogueResult = scriptShowDialogue();
-			if (dialogueResult == ExecutionResult::WaitingForCallback)
-				return dialogueResult;
-			break;
-		} else if (opcode1 == 0x0E) {
-			scriptChangeAnimation();
-		} else if (opcode1 == 0x0F) {
-			scriptFrameWait();
-			return ExecutionResult::WaitingForCallback;
-		} else if (opcode1 == 0x12) {
-			scriptSetPathfinding();
-		} else if (opcode1 == 0x2A) {
-			scriptLoadObjectAnim();
-		} else if (opcode1 == 0x2B) {
-			scriptCheckObjectData();
-		} else if (opcode1 == 0x2C) {
-			scriptCheckInventory();
-		} else if (opcode1 == 0x2D) {
-			scriptSetSnapToTarget();
-		} else if (opcode1 == 0x2E) {
-			scriptTestSceneAnimFrame();
-		} else if (opcode1 == 0x2F) {
-			scriptTestObjectAnimFrame();
-		} else if (opcode1 == 0x30) {
-			scriptPrintStringRight();
-			return ExecutionResult::WaitingForCallback;
-		} else if (opcode1 == 0x31) {
-			scriptSetPaletteDarkness();
-		} else if (opcode1 == 0x32) {
-			scriptSetObjectShading();
-		} else if (opcode1 == 0x33) {
-			scriptSetObjectScaling();
-		} else if (opcode1 == 0x34) {
-			scriptSetHotspotOverride();
-		} else if (opcode1 == 0x35) {
-			scriptSetObjectBounds();
-		} else if (opcode1 == 0x36) {
-			scriptDismissAllPanels();
-		} else if (opcode1 == 0x37) {
-			scriptResetToSceneScript();
-		} else if (opcode1 == 0x38) {
-			scriptLoadOverlayFont();
-		} else if (opcode1 == 0x39) {
-			// scriptEndOverlayText (1008:d80f). Clears the overlay text stage.
-			scriptEndOverlayText();
-		} else if (opcode1 == 0x3A) {
-			scriptAddOverlayTextEntry();
-		} else if (opcode1 == 0x3B) {
-			scriptClearOverlayText();
-		} else if (opcode1 == 0x3C) {
-			scriptFadeToBlack();
-		} else if (opcode1 == 0x3D) {
-			scriptFadeFromBlack();
-		} else if (opcode1 == 0x3E) {
-			scriptLoadPcmSound();
-		} else if (opcode1 == 0x3F) {
-			scriptFreePcmSound();
-		} else if (opcode1 == 0x40) {
-			scriptPlayPcmSound();
-		} else if (opcode1 == 0x41) {
-			if (scriptWaitForSound()) {
-				return ExecutionResult::WaitingForCallback;
-			}
-		} else if (opcode1 == 0x42) {
-			scriptStopPcmSound();
-		} else if (opcode1 == 0x43) {
-			scriptLoadMusicSlot();
-		} else if (opcode1 == 0x44) {
-			scriptPlayMusicSlot();
-		} else if (opcode1 == 0x45) {
-			scriptStopMusicSlot();
-		} else if (opcode1 == 0x47) {
-			if (scriptWaitForMusic()) {
-				return ExecutionResult::WaitingForCallback;
-			}
-		} else if (opcode1 == 0x46) {
-			scriptFreeMusicSlot();
-		} else if (opcode1 == 0x48) {
-			scriptGetObjectX();
-		} else if (opcode1 == 0x49) {
-			scriptGetObjectY();
-		} else if (opcode1 == 0x4A) {
-			scriptGetObjectField8();
-		} else if (opcode1 == 0x4B) {
-			scriptGetObjectOrientation();
-		} else if (opcode1 == 0x4C) {
-			scriptClearActorInventory();
-		} else if (opcode1 == 0x4D) {
-			scriptSetPathfindingRemap();
-		} else if (opcode1 == 0x4E) {
-			if (scriptWaitForAdlib()) {
-				return ExecutionResult::WaitingForCallback;
-			}
-		} else {
+		const OpcodeEntry *entry = nullptr;
+		if (_opcodeTable != nullptr && opcode1 < _opcodeTableSize)
+			entry = &_opcodeTable[opcode1];
+
+		if (entry == nullptr || entry->handler == nullptr) {
 			setScriptError(7);
 			endBuffering(_lastOpcodeTriggeredSkip);
 			break;
 		}
+
+		const OpcodeResult opcodeResult = (this->*(entry->handler))();
+		if (opcodeResult == OpcodeResult::WaitForCallback)
+			return ExecutionResult::WaitingForCallback;
+		if (opcodeResult == OpcodeResult::ReturnFinished)
+			return ExecutionResult::ScriptFinished;
+
 		endBuffering(_lastOpcodeTriggeredSkip);
+		if (opcodeResult == OpcodeResult::FinishScript)
+			break;
 	}
 	_isRunningScript = false;
 	if (hasScriptError())
diff --git a/engines/macs2/scriptexecutor.h b/engines/macs2/scriptexecutor.h
index 9f37e4ae2b1..463b952d7d5 100644
--- a/engines/macs2/scriptexecutor.h
+++ b/engines/macs2/scriptexecutor.h
@@ -75,6 +75,21 @@ enum class ExecutionResult {
 	WaitingForCallback
 };
 
+/** Per-opcode handler result for table-driven dispatch in executeOpcodes(). */
+enum class OpcodeResult {
+	/** Continue with the next opcode in the current script. */
+	Continue,
+	/** Break the opcode loop, then return ScriptFinished (clears running flag). */
+	FinishScript,
+	/** Return WaitingForCallback immediately (leaves running flag set). */
+	WaitForCallback,
+	/**
+	 * Return ScriptFinished immediately (leaves running flag set).
+	 * Matches legacy `return scriptXxx()` paths such as changeScene / waitForWalk.
+	 */
+	ReturnFinished
+};
+
 enum class ScriptExecutionState {
 	// We are executing the scene script
 	ExecutingSceneScript,
@@ -97,6 +112,23 @@ enum class ScriptExecutionState {
  *
  */
 class ScriptExecutor {
+public:
+	typedef OpcodeResult (ScriptExecutor::*OpcodeHandler)();
+
+	struct OpcodeEntry {
+		const char *name;
+		OpcodeHandler handler;
+	};
+
+	/** Install the active opcode table (DOS today; other platforms later). */
+	void setOpcodeTable(const OpcodeEntry *table, uint size);
+	/** Name for opcode, or "?" if unset/out of range. */
+	const char *opcodeName(uint8 opcode) const;
+
+	/** Default DOS opcode table (class member so private handlers are accessible). */
+	static const OpcodeEntry kDosOpcodeTable[];
+	static const uint kDosOpcodeTableSize;
+
 private:
 #ifdef DEMACS2
 public:
@@ -180,6 +212,86 @@ public:
 	void scriptFadeFromBlack();
 	void scriptFreePcmSound();
 
+	// Table-driven opcode wrappers (return OpcodeResult for executeOpcodes).
+	OpcodeResult opSetVar();
+	OpcodeResult opSetVarOr();
+	OpcodeResult opIfFalse();
+	OpcodeResult opIfTrue();
+	OpcodeResult opCompare();
+	OpcodeResult opIfInteraction();
+	OpcodeResult opEndIf();
+	OpcodeResult opElse();
+	OpcodeResult opNop09();
+	OpcodeResult opPrintStringLeft();
+	OpcodeResult opMoveObject();
+	OpcodeResult opChangeScene();
+	OpcodeResult opShowDialogue();
+	OpcodeResult opChangeAnimation();
+	OpcodeResult opFrameWait();
+	OpcodeResult opWalkToPosition();
+	OpcodeResult opWaitForWalk();
+	OpcodeResult opSetPathfinding();
+	OpcodeResult opSkipUntil14();
+	OpcodeResult opSkipWord();
+	OpcodeResult opClearDialogueChoices();
+	OpcodeResult opAddDialogueChoice();
+	OpcodeResult opShowDialogueChoice();
+	OpcodeResult opDismissPanel();
+	OpcodeResult opWalkToAndPickup();
+	OpcodeResult opSetPickupFrames();
+	OpcodeResult opSetupObject();
+	OpcodeResult opSetSkippable();
+	OpcodeResult opClearSkippable();
+	OpcodeResult opPlayAnimation();
+	OpcodeResult opTestPathfinding();
+	OpcodeResult opSetYOffset();
+	OpcodeResult opSetMotion();
+	OpcodeResult opSetOrientation();
+	OpcodeResult opMoveToPosition();
+	OpcodeResult opAddValues();
+	OpcodeResult opSubValues();
+	OpcodeResult opLoadSpecialAnim();
+	OpcodeResult opSetDirection();
+	OpcodeResult opStopAnimation();
+	OpcodeResult opOpenInventory();
+	OpcodeResult opLoadObjectAnim();
+	OpcodeResult opCheckObjectData();
+	OpcodeResult opCheckInventory();
+	OpcodeResult opSetSnapToTarget();
+	OpcodeResult opTestSceneAnimFrame();
+	OpcodeResult opTestObjectAnimFrame();
+	OpcodeResult opPrintStringRight();
+	OpcodeResult opSetPaletteDarkness();
+	OpcodeResult opSetObjectShading();
+	OpcodeResult opSetObjectScaling();
+	OpcodeResult opSetHotspotOverride();
+	OpcodeResult opSetObjectBounds();
+	OpcodeResult opDismissAllPanels();
+	OpcodeResult opResetToSceneScript();
+	OpcodeResult opLoadOverlayFont();
+	OpcodeResult opEndOverlayText();
+	OpcodeResult opAddOverlayTextEntry();
+	OpcodeResult opClearOverlayText();
+	OpcodeResult opFadeToBlack();
+	OpcodeResult opFadeFromBlack();
+	OpcodeResult opLoadPcmSound();
+	OpcodeResult opFreePcmSound();
+	OpcodeResult opPlayPcmSound();
+	OpcodeResult opWaitForSound();
+	OpcodeResult opStopPcmSound();
+	OpcodeResult opLoadMusicSlot();
+	OpcodeResult opPlayMusicSlot();
+	OpcodeResult opStopMusicSlot();
+	OpcodeResult opFreeMusicSlot();
+	OpcodeResult opWaitForMusic();
+	OpcodeResult opGetObjectX();
+	OpcodeResult opGetObjectY();
+	OpcodeResult opGetObjectField8();
+	OpcodeResult opGetObjectOrientation();
+	OpcodeResult opClearActorInventory();
+	OpcodeResult opSetPathfindingRemap();
+	OpcodeResult opWaitForAdlib();
+
 	inline void scriptUnimplementedOpcode(const char *source, uint16 opcode) {
 		debug("Unimplemented opcode (%s): %.2x.", source, opcode);
 	}
@@ -187,6 +299,10 @@ public:
 private:
 #endif
 
+	// Active opcode table
+	const OpcodeEntry *_opcodeTable = nullptr;
+	uint _opcodeTableSize = 0;
+
 	// State variables from here
 
 	// Overall state


Commit: fe4f8be3935468a83ac03a2b893c103ebf03c88b
    https://github.com/scummvm/scummvm/commit/fe4f8be3935468a83ac03a2b893c103ebf03c88b
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00

Commit Message:
MACS2: fixed opcode outer loop exit

Changed paths:
    engines/macs2/scriptexecutor.cpp


diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index ae2fd77d217..5669eefa209 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -651,6 +651,13 @@ bool ScriptExecutor::loadNextScript() {
 	// Confirmed from runScriptExecutor (1008:e3e7): after the scene script finishes,
 	// iterate executingObjectId from 1 to 0x200, skipping objects with no runtime data.
 	// Load each object's script from runtime+0x187/+0x189/+0x18B.
+	//
+	// Binary: when g_wExecutingScriptObjectId > 0x200 (e.g. opcode 0x29 sentinel),
+	// the outer loop stops without loading further object scripts. Check before the
+	// scene→object transition so a scene-script sentinel is not wiped by resetting
+	// _executingObjectIndex to 0.
+	if (_executingScriptObjectId > 0x200)
+		return false;
 
 	if (_scriptExecutionState == ScriptExecutionState::ExecutingSceneScript) {
 		// If we are finished with executing the scene, we need to go over all relevant objects
@@ -695,7 +702,6 @@ bool ScriptExecutor::loadNextScript() {
 			if (_terminateOuterScriptBeforeRepeat) {
 				if (_stream)
 					_stream->seek(_stream->size(), SEEK_SET);
-				_executingScriptObjectId = 0x201;
 				_terminateOuterScriptBeforeRepeat = false;
 			}
 			_repeatRunFlag = true;
@@ -708,6 +714,9 @@ bool ScriptExecutor::loadNextScript() {
 				return false;
 			}
 			_stream->seek(0, SEEK_SET);
+			// Fresh scene-script pass (same as runSceneScriptPass): clear any prior
+			// >0x200 sentinel so the early guard above does not skip object scripts.
+			_executingScriptObjectId = 0;
 			_scriptExecutionState = ScriptExecutionState::ExecutingSceneScript;
 			debugC(kDebugScript, "----- Deferred repeat pass for scene: %.4x", _executingObjectIndex);
 			return true;
@@ -3156,7 +3165,13 @@ OpcodeResult ScriptExecutor::opStopAnimation() {
 }
 
 OpcodeResult ScriptExecutor::opOpenInventory() {
-	// Binary (1008:e0f9): always terminates script + sets executingObjectId sentinel.
+	// Binary executeOpcodes @ 1008:e0f9 (full + demo):
+	//   scriptOpenInventory();
+	//   g_wScriptPosition = g_wScriptEndPosition;
+	//   g_wExecutingScriptObjectId = 0x201;
+	//   goto LAB_1008_e3bd;
+	// loadNextScript() honors ScriptObjectId > 0x200 and will not start/continue
+	// the object-script walk (including when 0x29 ran from the scene script).
 	scriptOpenInventory();
 	_stream->seek(_stream->size(), SEEK_SET);
 	_executingObjectIndex = 0x201;


Commit: ef2c114ffb16facf56052e458842bf6454c084f2
    https://github.com/scummvm/scummvm/commit/ef2c114ffb16facf56052e458842bf6454c084f2
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00

Commit Message:
MACS2: improved amiga demo support

Changed paths:
  A engines/macs2/amiga_archive.cpp
  A engines/macs2/amiga_archive.h
  A engines/macs2/amiga_decode.cpp
  A engines/macs2/amiga_decode.h
  A engines/macs2/amiga_resources.cpp
    engines/macs2/gameobjects.cpp
    engines/macs2/gameobjects.h
    engines/macs2/macs2.cpp
    engines/macs2/macs2.h
    engines/macs2/module.mk
    engines/macs2/scriptexecutor.cpp
    engines/macs2/scriptexecutor.h


diff --git a/engines/macs2/amiga_archive.cpp b/engines/macs2/amiga_archive.cpp
new file mode 100644
index 00000000000..e2346f69396
--- /dev/null
+++ b/engines/macs2/amiga_archive.cpp
@@ -0,0 +1,603 @@
+/* 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 "macs2/amiga_archive.h"
+
+#include "common/compression/powerpacker.h"
+#include "common/debug.h"
+#include "common/file.h"
+#include "common/memstream.h"
+#include "common/substream.h"
+#include "common/system.h"
+#include "common/textconsole.h"
+#include "common/array.h"
+
+#include <cstring>
+#include "macs2/detection.h"
+
+namespace Macs2 {
+
+namespace {
+
+static const uint32 kTagMXMF = MKTAG('M', 'X', 'M', 'F');
+static const uint32 kTagMXDR = MKTAG('M', 'X', 'D', 'R');
+static const uint32 kTagMXIN = MKTAG('M', 'X', 'I', 'N');
+static const uint32 kTagMXOO = MKTAG('M', 'X', 'O', 'O');
+static const uint32 kTagMXMM = MKTAG('M', 'X', 'M', 'M');
+static const uint32 kTagPP20 = MKTAG('P', 'P', '2', '0');
+
+static Common::Path volumePathForDisk(uint16 disk) {
+	// Demo uses DataA only (disk 0). Full game may use DataB/DataC/DataD.
+	char letter = (char)('A' + CLIP<int>(disk, 0, 25));
+	return Common::Path(Common::String::format("Data%c", letter));
+}
+
+} // End of anonymous namespace
+
+Macs2AmigaArchive::Macs2AmigaArchive() {
+}
+
+Macs2AmigaArchive::~Macs2AmigaArchive() {
+	close();
+}
+
+AmigaResourceType Macs2AmigaArchive::typeFromTag(uint16 tag) {
+	switch (tag) {
+	case kAmigaResOO:
+	case kAmigaResMM:
+	case kAmigaResOS:
+	case kAmigaResFF:
+		return (AmigaResourceType)tag;
+	default:
+		return kAmigaResUnknown;
+	}
+}
+
+const char *Macs2AmigaArchive::typeToString(AmigaResourceType type) {
+	switch (type) {
+	case kAmigaResOO:
+		return "OO";
+	case kAmigaResMM:
+		return "MM";
+	case kAmigaResOS:
+		return "OS";
+	case kAmigaResFF:
+		return "FF";
+	default:
+		return "??";
+	}
+}
+
+Common::String Macs2AmigaArchive::makeResourceName(AmigaResourceType type, uint16 id) {
+	return Common::String::format("%s_%04u", typeToString(type), id);
+}
+
+bool Macs2AmigaArchive::parseResourceName(const Common::String &name, AmigaResourceType &type, uint16 &id) {
+	if (name.equalsIgnoreCase("scene_table") || name.equalsIgnoreCase("Info"))
+		return false;
+	if (name.size() < 7 || name[2] != '_')
+		return false;
+
+	const uint16 tag = ((uint16)name[0] << 8) | (uint16)name[1];
+	type = typeFromTag(tag);
+	if (type == kAmigaResUnknown)
+		return false;
+
+	id = (uint16)atoi(name.c_str() + 3);
+	return true;
+}
+
+bool Macs2AmigaArchive::open() {
+	close();
+
+	if (!loadMdir()) {
+		close();
+		return false;
+	}
+
+	loadInfo();
+
+	debugC(1, kDebugFilePath, "Macs2 Amiga archive: %u resources, %u scenes, %u volumes indexed",
+		   (uint)_entries.size(), _sceneCount, (uint)_volumes.size());
+	return !_entries.empty();
+}
+
+void Macs2AmigaArchive::close() {
+	Common::StackLock lock(_mutex);
+	_entries.clear();
+	_volumes.clear();
+	_sceneCount = 0;
+	_totalResources = 0;
+	_firstBlockSize = 0;
+	_info = AmigaInfoData();
+}
+
+bool Macs2AmigaArchive::openVolume(uint16 disk) {
+	while (_volumes.size() <= disk)
+		_volumes.push_back(Volume());
+
+	Volume &volume = _volumes[disk];
+	if (volume.file)
+		return true;
+
+	volume.path = volumePathForDisk(disk);
+	volume.file.reset(new Common::File());
+	if (!volume.file->open(volume.path)) {
+		warning("Macs2 Amiga: cannot open volume %s", volume.path.toString().c_str());
+		volume.file.reset();
+		return false;
+	}
+	volume.size = (uint32)volume.file->size();
+	return true;
+}
+
+bool Macs2AmigaArchive::loadMdir() {
+	Common::File mdirFile;
+	if (!mdirFile.open("Mdir")) {
+		warning("Macs2 Amiga: Mdir not found");
+		return false;
+	}
+
+	const int64 mdirSize = mdirFile.size();
+	if (mdirSize < 18) {
+		warning("Macs2 Amiga: Mdir too small");
+		return false;
+	}
+
+	if (mdirFile.readUint32BE() != kTagMXDR) {
+		warning("Macs2 Amiga: Mdir is not MXDR");
+		return false;
+	}
+
+	mdirFile.readUint16BE(); // version
+	mdirFile.readUint16BE(); // entry count field (often count+1)
+	mdirFile.readUint16BE(); // type tag, usually "OO"
+	_totalResources = mdirFile.readUint16BE();
+	mdirFile.readUint32BE(); // reserved
+	_sceneCount = mdirFile.readUint16BE();
+
+	const uint16 dirEntryCount = (uint16)((mdirSize - 18) / 10);
+	Common::Array<AmigaDirEntry> dirEntries;
+	dirEntries.resize(dirEntryCount);
+
+	for (uint16 i = 0; i < dirEntryCount; i++) {
+		AmigaDirEntry &e = dirEntries[i];
+		const uint16 typeTag = mdirFile.readUint16BE();
+		e.type = typeFromTag(typeTag);
+		e.id = mdirFile.readUint16BE();
+		e.disk = mdirFile.readUint16BE();
+		e.offset = mdirFile.readUint32BE();
+	}
+
+	// Open volumes referenced by the directory and read each entry header.
+	for (const AmigaDirEntry &dir : dirEntries) {
+		if (dir.type == kAmigaResUnknown)
+			continue;
+		if (!openVolume(dir.disk))
+			return false;
+
+		Volume &volume = _volumes[dir.disk];
+		if (dir.offset + 8 > volume.size) {
+			warning("Macs2 Amiga: entry %s_%04u offset past end of volume", typeToString(dir.type), dir.id);
+			continue;
+		}
+
+		volume.file->seek(dir.offset, SEEK_SET);
+		const uint16 fileTypeTag = volume.file->readUint16BE();
+		const uint16 fileId = volume.file->readUint16BE();
+		const uint32 payloadSize = volume.file->readUint32BE();
+
+		if (typeFromTag(fileTypeTag) != dir.type || fileId != dir.id) {
+			warning("Macs2 Amiga: Mdir/Data mismatch for %s_%04u", typeToString(dir.type), dir.id);
+			continue;
+		}
+		if (payloadSize == 0 || dir.offset + 8 + payloadSize > volume.size) {
+			warning("Macs2 Amiga: bad payload size for %s_%04u", typeToString(dir.type), dir.id);
+			continue;
+		}
+
+		Entry entry;
+		entry.dir = dir;
+		entry.payloadSize = payloadSize;
+		entry.name = makeResourceName(dir.type, dir.id);
+		_entries[Common::Path(entry.name)] = entry;
+	}
+
+	// Index the first PP20 block (scene table) from DataA header.
+	if (!openVolume(0))
+		return false;
+
+	Volume &dataA = _volumes[0];
+	dataA.file->seek(0, SEEK_SET);
+	if (dataA.file->readUint32BE() != kTagMXMF) {
+		warning("Macs2 Amiga: DataA is not MXMF");
+		return false;
+	}
+	dataA.file->readUint16BE(); // version
+	dataA.file->readUint16BE(); // type
+	const uint16 hdrResourceCount = dataA.file->readUint16BE();
+	_firstBlockSize = dataA.file->readUint32BE();
+	_firstBlockOffset = 14;
+	if (hdrResourceCount != 0)
+		_totalResources = hdrResourceCount;
+
+	if (_firstBlockSize == 0 || _firstBlockOffset + _firstBlockSize > dataA.size) {
+		warning("Macs2 Amiga: invalid first PP20 block size");
+		return false;
+	}
+
+	Entry sceneTable;
+	sceneTable.dir.type = kAmigaResOO;
+	sceneTable.dir.id = 0xFFFF;
+	sceneTable.dir.disk = 0;
+	sceneTable.dir.offset = _firstBlockOffset;
+	sceneTable.payloadSize = _firstBlockSize;
+	sceneTable.name = "scene_table";
+	_entries[Common::Path(sceneTable.name)] = sceneTable;
+
+	return true;
+}
+
+bool Macs2AmigaArchive::loadInfo() {
+	Common::File infoFile;
+	if (!infoFile.open("Info"))
+		return false;
+
+	// Layout matches Ghidra load_info_mxin @ 00221494 on demo_unpacked.exe.0:
+	//  MXIN(4) + ver(u16)=1 + pad(u16)=0 + startMmId(u32BE)
+	//  + 5xu16 panel border color indices
+	//  + 8xu16 panel darken color indices
+	//  + 16xu16 UI RGB4 colors
+	//  + flag bytes (g_bUsePanelPatternFill at +0x50)
+	//  + mdirSize / volumeCount
+	if (infoFile.size() < 0x5A || infoFile.readUint32BE() != kTagMXIN)
+		return false;
+
+	const uint16 version = infoFile.readUint16BE();
+	const uint16 pad0 = infoFile.readUint16BE();
+	if (version != 1 || pad0 != 0)
+		return false;
+
+	const uint32 startSceneResourceId = infoFile.readUint32BE();
+
+	AmigaInfoData info;
+	if (startSceneResourceId > 0 && startSceneResourceId <= 0xFFFF)
+		info.startSceneResourceId = (uint16)startSceneResourceId;
+
+	// Ghidra load_info_mxin: bytes 0x0C..0x11 are other MXIN fields; panel tables
+	// start at file +0x12 (demo: six zero bytes after startMmId).
+	byte gap[6] = {0};
+	if (infoFile.read(gap, sizeof(gap)) != sizeof(gap))
+		return false;
+
+	// File offset 0x12: five border color indices (demo 18..22).
+	for (uint i = 0; i < 5; i++)
+		info.panelBorderColorIndices[i] = infoFile.readUint16BE();
+	// Also used historically as cursor mode probes in the demo (OO ids 18..22).
+	for (uint i = 0; i < 5; i++)
+		info.cursorResourceIds[i] = info.panelBorderColorIndices[i];
+
+	for (uint i = 0; i < 8; i++)
+		info.panelDarkenColorIndices[i] = infoFile.readUint16BE();
+
+	for (uint i = 0; i < 16; i++)
+		info.uiPaletteAmiga[i] = infoFile.readUint16BE();
+
+	// Flag block at file +0x4C (Ghidra after UI RGB copy).
+	// g_bUsePanelPatternFill is the byte at file +0x50 (demo: 0 = darken remap).
+	byte flags[10] = {0};
+	if (infoFile.read(flags, sizeof(flags)) != sizeof(flags))
+		return false;
+	info.usePanelPatternFill = flags[4] != 0; // file +0x50 (pre-title only)
+
+	if (infoFile.pos() + 4 <= infoFile.size()) {
+		info.mdirSize = infoFile.readUint16BE();
+		info.volumeCount = infoFile.readUint16BE();
+	}
+	// Demo Info ends with sceneCount-like word earlier in some dumps; keep 0 if absent.
+	info.useInventoryCursorId = 0x17; // Walk/inventory fallback used by gameplay modes
+	info.loaded = true;
+	_info = info;
+
+	loadPrefMenuMode();
+	// Gameplay uses PREF MENUMODE (copied over Info flag after title @ 00234b42).
+	_info.usePanelPatternFill = _info.prefMenuMode != 0;
+
+	if (_info.sceneCount != 0)
+		_sceneCount = _info.sceneCount;
+
+	Entry infoEntry;
+	infoEntry.name = "Info";
+	infoEntry.dir.disk = 0xFFFF; // special: load from loose Info file
+	infoEntry.payloadSize = (uint32)infoFile.size();
+	_entries[Common::Path("Info")] = infoEntry;
+
+	return true;
+}
+
+bool Macs2AmigaArchive::loadPrefMenuMode() {
+	// parsePrefResourceSettings @ 00236e88: * PREF then colon-separated ASCII digits.
+	// Order: SCREENMODE, MENUMODE, MUSIC, INTRO, PICSHADE, ...
+	// initAndRunTitleScreen copies MENUMODE into g_bUsePanelPatternFill.
+	Common::File prefFile;
+	if (!prefFile.open("PREF"))
+		return false;
+
+	const int32 size = prefFile.size();
+	if (size <= 0 || size > 0x10000) {
+		prefFile.close();
+		return false;
+	}
+	Common::Array<char> buf;
+	buf.resize((uint)size + 1);
+	if (prefFile.read(buf.data(), (uint32)size) != (uint32)size) {
+		prefFile.close();
+		return false;
+	}
+	prefFile.close();
+	buf[size] = '\0';
+
+	const char *p = strstr(buf.data(), "* PREF");
+	if (!p)
+		p = buf.data();
+
+	uint values[16] = {0};
+	uint count = 0;
+	while (*p && count < 16) {
+		while (*p && *p != '*')
+			p++;
+		if (!*p)
+			break;
+		p++;
+		while (*p == ' ' || *p == '\t')
+			p++;
+		while (*p && *p != ':' && *p != '\n' && *p != '\r')
+			p++;
+		if (*p != ':')
+			continue;
+		p++;
+		while (*p == ' ' || *p == '\t')
+			p++;
+		if (*p < '0' || *p > '9')
+			continue;
+		uint v = 0;
+		while (*p >= '0' && *p <= '9') {
+			v = v * 10 + (uint)(*p - '0');
+			p++;
+		}
+		values[count++] = v;
+	}
+
+	if (count >= 2) {
+		_info.prefMenuMode = (uint8)(values[1] & 0xFF);
+		debugC(1, kDebugFilePath, "Amiga PREF MENUMODE=%u (0=SHADE/1=GFX/2=SOLID/3=POINTS)",
+			   _info.prefMenuMode);
+		return true;
+	}
+	return false;
+}
+
+byte *Macs2AmigaArchive::decompressPP20(const byte *src, uint32 srcLen, uint32 &outLen) {
+	outLen = 0;
+	if (!src || srcLen < 12 || READ_BE_UINT32(src) != kTagPP20)
+		return nullptr;
+
+	// PowerPackerStream::unpackBuffer allocates with new[]; MemoryReadStream frees with free().
+	byte *unpacked = Common::PowerPackerStream::unpackBuffer(src, srcLen, outLen);
+	if (!unpacked || outLen == 0) {
+		delete[] unpacked;
+		outLen = 0;
+		return nullptr;
+	}
+
+	byte *copy = (byte *)malloc(outLen);
+	if (!copy) {
+		delete[] unpacked;
+		outLen = 0;
+		return nullptr;
+	}
+	memcpy(copy, unpacked, outLen);
+	delete[] unpacked;
+	return copy;
+}
+
+byte *Macs2AmigaArchive::decompressPayload(const byte *payload, uint32 payloadLen, uint32 &outLen) {
+	outLen = 0;
+	if (!payload || payloadLen < 4)
+		return nullptr;
+
+	const uint32 magic = READ_BE_UINT32(payload);
+	if (magic == kTagPP20)
+		return decompressPP20(payload, payloadLen, outLen);
+
+	// MXMM is a multi-chunk scene package (chunk0 = planar BG). Return the raw
+	// container so callers can decompress/parse individual chunks.
+	if (magic == kTagMXMM) {
+		byte *copy = (byte *)malloc(payloadLen);
+		if (!copy)
+			return nullptr;
+		memcpy(copy, payload, payloadLen);
+		outLen = payloadLen;
+		return copy;
+	}
+
+	// Uncompressed MXOO / raw payload: return a malloc'd copy for MemoryReadStream.
+	byte *copy = (byte *)malloc(payloadLen);
+	if (!copy)
+		return nullptr;
+	memcpy(copy, payload, payloadLen);
+	outLen = payloadLen;
+	return copy;
+}
+
+Common::SeekableReadStream *Macs2AmigaArchive::readAndDecompressEntry(const Entry &entry) const {
+	// Special case: loose Info file
+	if (entry.name.equalsIgnoreCase("Info")) {
+		Common::File infoFile;
+		if (!infoFile.open("Info"))
+			return nullptr;
+		const uint32 size = (uint32)infoFile.size();
+		byte *data = (byte *)malloc(size);
+		if (!data)
+			return nullptr;
+		if (infoFile.read(data, size) != size) {
+			free(data);
+			return nullptr;
+		}
+		return new Common::MemoryReadStream(data, size, DisposeAfterUse::YES);
+	}
+
+	if (entry.dir.disk == 0xFFFF || entry.dir.disk >= _volumes.size() || !_volumes[entry.dir.disk].file)
+		return nullptr;
+
+	Common::StackLock lock(_mutex);
+	Volume &volume = const_cast<Volume &>(_volumes[entry.dir.disk]);
+
+	uint32 readOffset = entry.dir.offset;
+	uint32 readSize = entry.payloadSize;
+
+	// scene_table points at the raw PP20 block (no 8-byte type header).
+	// Regular resources point at the 8-byte header; payload follows.
+	if (!entry.name.equalsIgnoreCase("scene_table")) {
+		readOffset += 8;
+	}
+
+	if (readOffset + readSize > volume.size)
+		return nullptr;
+
+	Common::Array<byte> payload;
+	payload.resize(readSize);
+	volume.file->seek(readOffset, SEEK_SET);
+	if (volume.file->read(payload.data(), readSize) != readSize)
+		return nullptr;
+
+	uint32 outLen = 0;
+	byte *decoded = decompressPayload(payload.data(), readSize, outLen);
+	if (!decoded)
+		return nullptr;
+
+	return new Common::MemoryReadStream(decoded, outLen, DisposeAfterUse::YES);
+}
+
+bool Macs2AmigaArchive::hasResource(AmigaResourceType type, uint16 id) const {
+	return hasFile(Common::Path(makeResourceName(type, id)));
+}
+
+Common::SeekableReadStream *Macs2AmigaArchive::createReadStreamForResource(AmigaResourceType type, uint16 id) const {
+	return createReadStreamForMember(Common::Path(makeResourceName(type, id)));
+}
+
+Common::SeekableReadStream *Macs2AmigaArchive::createSceneTableStream() const {
+	return createReadStreamForMember(Common::Path("scene_table"));
+}
+
+bool Macs2AmigaArchive::hasFile(const Common::Path &path) const {
+	return _entries.contains(path);
+}
+
+int Macs2AmigaArchive::listMembers(Common::ArchiveMemberList &list) const {
+	for (const auto &entry : _entries)
+		list.push_back(getMember(entry._key));
+	return _entries.size();
+}
+
+const Common::ArchiveMemberPtr Macs2AmigaArchive::getMember(const Common::Path &path) const {
+	if (!hasFile(path))
+		return Common::ArchiveMemberPtr();
+	return Common::ArchiveMemberPtr(new Common::GenericArchiveMember(path, *this));
+}
+
+Common::SeekableReadStream *Macs2AmigaArchive::createReadStreamForMember(const Common::Path &path) const {
+	if (!_entries.contains(path))
+		return nullptr;
+	return readAndDecompressEntry(_entries[path]);
+}
+
+bool Macs2AmigaArchive::decodePlanarSprite(const byte *mxoo, uint32 mxooSize, uint16 &width, uint16 &height, Common::Array<byte> &pixels) {
+	width = height = 0;
+	pixels.clear();
+
+	if (!mxoo || mxooSize < 32 || READ_BE_UINT32(mxoo) != kTagMXOO)
+		return false;
+
+	const uint32 scriptOff = READ_BE_UINT32(mxoo + 4);
+	if (scriptOff <= 12 || scriptOff > mxooSize)
+		return false;
+
+	const byte *body = mxoo + 12;
+	const uint32 bodyLen = scriptOff - 12;
+	if (bodyLen < 20 || body[10] != 1 || body[11] != 1)
+		return false;
+
+	const uint16 frames = READ_BE_UINT16(body + 14);
+	width = READ_BE_UINT16(body + 16);
+	height = READ_BE_UINT16(body + 18);
+	if (frames == 0 || width == 0 || width > 320 || height == 0 || height > 200)
+		return false;
+
+	const uint32 rowBytes = (width + 7) / 8;
+	const uint32 planeSize = rowBytes * height;
+	const uint32 pixelBytes = bodyLen - 20;
+	if (planeSize == 0 || pixelBytes < planeSize * 5)
+		return false;
+
+	const uint32 numPlanes = pixelBytes / planeSize;
+	if (numPlanes < 5 || numPlanes > 6 || pixelBytes != planeSize * numPlanes)
+		return false;
+
+	pixels.resize((uint)width * height);
+	Common::fill(pixels.begin(), pixels.end(), 0);
+
+	const byte *pixData = body + 20;
+	// Planes 0..4 are color; plane 5 (if present) is mask/transparency.
+	for (uint32 plane = 0; plane < 5 && plane < numPlanes; plane++) {
+		const uint32 pOff = plane * planeSize;
+		for (uint16 y = 0; y < height; y++) {
+			for (uint32 bx = 0; bx < rowBytes; bx++) {
+				const byte b = pixData[pOff + y * rowBytes + bx];
+				for (int bit = 0; bit < 8; bit++) {
+					const uint16 x = (uint16)(bx * 8 + bit);
+					if (x < width && (b & (0x80 >> bit)))
+						pixels[y * width + x] |= (byte)(1 << plane);
+				}
+			}
+		}
+	}
+
+	if (numPlanes >= 6) {
+		// Mask plane: clear transparent pixels (bit clear in mask).
+		const uint32 pOff = 5 * planeSize;
+		for (uint16 y = 0; y < height; y++) {
+			for (uint32 bx = 0; bx < rowBytes; bx++) {
+				const byte b = pixData[pOff + y * rowBytes + bx];
+				for (int bit = 0; bit < 8; bit++) {
+					const uint16 x = (uint16)(bx * 8 + bit);
+					if (x < width && (b & (0x80 >> bit)) == 0)
+						pixels[y * width + x] = 0;
+				}
+			}
+		}
+	}
+
+	return true;
+}
+
+} // End of namespace Macs2
diff --git a/engines/macs2/amiga_archive.h b/engines/macs2/amiga_archive.h
new file mode 100644
index 00000000000..07995fcb39a
--- /dev/null
+++ b/engines/macs2/amiga_archive.h
@@ -0,0 +1,179 @@
+/* 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 MACS2_AMIGA_ARCHIVE_H
+#define MACS2_AMIGA_ARCHIVE_H
+
+#include "common/archive.h"
+#include "common/array.h"
+#include "common/file.h"
+#include "common/hashmap.h"
+#include "common/mutex.h"
+#include "common/ptr.h"
+#include "common/str.h"
+#include "common/stream.h"
+
+namespace Macs2 {
+
+/**
+ * Resource type tags used in Amiga Mdir / DataA entry headers.
+ * Stored as big-endian ASCII pairs: "OO", "MM", "OS", "FF".
+ */
+enum AmigaResourceType : uint16 {
+	kAmigaResOO = 0x4F4F, // object / graphics
+	kAmigaResMM = 0x4D4D, // scene package (MXMM: planar BG + tables; not Protracker)
+	kAmigaResOS = 0x4F53, // sound effect
+	kAmigaResFF = 0x4646, // font
+	kAmigaResUnknown = 0
+};
+
+struct AmigaDirEntry {
+	AmigaResourceType type = kAmigaResUnknown;
+	uint16 id = 0;
+	uint16 disk = 0;
+	uint32 offset = 0; // absolute offset of the 8-byte entry header in Data*
+};
+
+struct AmigaInfoData {
+	uint16 cursorResourceIds[8] = {0}; // Talk..Disabled style slots from Info
+	uint16 useInventoryCursorId = 0;
+	uint16 sceneCount = 0;
+	uint16 volumeCount = 0;
+	uint16 mdirSize = 0;
+	/** 16x 12-bit 0x0RGB UI colors from MXIN (Ghidra DAT_002379dc). */
+	uint16 uiPaletteAmiga[16] = {0};
+	/**
+	 * Panel border line/corner color indices from MXIN (Ghidra g_awPanelBorderColorIndices).
+	 * Demo: 18,19,20,21,22.
+	 */
+	uint16 panelBorderColorIndices[5] = {0};
+	/**
+	 * Panel darken luminance->color table (Ghidra g_awPanelDarkenColorIndices).
+	 * Demo: 28,29,29,60,61,31,62,23. Indexed by (7 - (R4+G4+B4)/0x18).
+	 */
+	uint16 panelDarkenColorIndices[8] = {0};
+	/**
+	 * After title screen, g_bUsePanelPatternFill is overwritten from PREF MENUMODE
+	 * (initAndRunTitleScreen @ 00234b42): 0=SHADE (darken remap), 1=GFX / 2=SOLID /
+	 * 3=POINTS -> fill_ui_panel_pattern_tiles @ 00222332 (opaque 16x16 tiles).
+	 * Demo PREF defaults to MENUMODE 2 (SOLID). Info MXIN +0x50 is only the
+	 * pre-title default (0).
+	 */
+	uint8 prefMenuMode = 2;
+	bool usePanelPatternFill = true;
+	/**
+	 * Starting MM resource id from MXIN (u32BE at Info offset 8).
+	 * Demo Info stores 40 -> MM_0040 (intro). Script-visible scene id is mmId+1.
+	 */
+	uint16 startSceneResourceId = 0;
+	bool loaded = false;
+};
+
+/**
+ * Amiga MACS2 container: Mdir (MXDR) index + DataA/DataB/... (MXMF) volumes.
+ *
+ * Members are exposed as Common::Archive paths:
+ *   "scene_table"          - first PP20 block (decompressed MXOO)
+ *   "OO_0104", "MM_0004".. - typed resources (PP20 decompressed; MXMM raw)
+ *   "Info"                 - raw MXIN game metadata (if present on disk)
+ *
+ * Based on FORMAT.md / extract_macs2 Amiga mode in scummvm-tools.
+ */
+class Macs2AmigaArchive : public Common::Archive {
+public:
+	Macs2AmigaArchive();
+	~Macs2AmigaArchive() override;
+
+	/**
+	 * Open Mdir + DataA (and additional DataB.. volumes referenced by Mdir).
+	 * Looks up files via SearchMan / the game directory.
+	 */
+	bool open();
+	void close();
+	bool isOpen() const { return !_entries.empty(); }
+
+	uint16 getSceneCount() const { return _sceneCount; }
+	uint16 getResourceCount() const { return _totalResources; }
+	const AmigaInfoData &getInfo() const { return _info; }
+
+	/** Decompressed resource stream, or nullptr if missing. Caller owns the stream. */
+	Common::SeekableReadStream *createReadStreamForResource(AmigaResourceType type, uint16 id) const;
+
+	/** Decompressed first MXMF PP20 block (scene/base table). */
+	Common::SeekableReadStream *createSceneTableStream() const;
+
+	bool hasResource(AmigaResourceType type, uint16 id) const;
+
+	// Common::Archive
+	bool hasFile(const Common::Path &path) const override;
+	int listMembers(Common::ArchiveMemberList &list) const override;
+	const Common::ArchiveMemberPtr getMember(const Common::Path &path) const override;
+	Common::SeekableReadStream *createReadStreamForMember(const Common::Path &path) const override;
+
+	static Common::String makeResourceName(AmigaResourceType type, uint16 id);
+	static bool parseResourceName(const Common::String &name, AmigaResourceType &type, uint16 &id);
+	static AmigaResourceType typeFromTag(uint16 tag);
+	static const char *typeToString(AmigaResourceType type);
+
+	/**
+	 * Decode an Amiga planar MXOO sprite body into chunky 8bpp pixels.
+	 * Returns false if the resource is not a simple sprite.
+	 */
+	static bool decodePlanarSprite(const byte *mxoo, uint32 mxooSize, uint16 &width, uint16 &height, Common::Array<byte> &pixels);
+
+private:
+	struct Volume {
+		Common::Path path;
+		Common::ScopedPtr<Common::File> file;
+		uint32 size = 0;
+	};
+
+	struct Entry {
+		AmigaDirEntry dir;
+		uint32 payloadSize = 0; // size field from the 8-byte Data* header
+		Common::String name;
+	};
+
+	typedef Common::HashMap<Common::Path, Entry, Common::Path::IgnoreCase_Hash, Common::Path::IgnoreCase_EqualTo> EntryMap;
+
+	bool openVolume(uint16 disk);
+	bool loadMdir();
+	bool loadInfo();
+	/** Parse PREF MENUMODE into _info (Ghidra parsePrefResourceSettings -> title exit). */
+	bool loadPrefMenuMode();
+	Common::SeekableReadStream *readAndDecompressEntry(const Entry &entry) const;
+	static byte *decompressPP20(const byte *src, uint32 srcLen, uint32 &outLen);
+	static byte *decompressPayload(const byte *payload, uint32 payloadLen, uint32 &outLen);
+
+	EntryMap _entries;
+	Common::Array<Volume> _volumes;
+	mutable Common::Mutex _mutex;
+
+	uint16 _sceneCount = 0;
+	uint16 _totalResources = 0;
+	uint32 _firstBlockSize = 0;
+	uint32 _firstBlockOffset = 14;
+	AmigaInfoData _info;
+};
+
+} // End of namespace Macs2
+
+#endif // MACS2_AMIGA_ARCHIVE_H
diff --git a/engines/macs2/amiga_decode.cpp b/engines/macs2/amiga_decode.cpp
new file mode 100644
index 00000000000..4418e268c84
--- /dev/null
+++ b/engines/macs2/amiga_decode.cpp
@@ -0,0 +1,1038 @@
+/* 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 "macs2/amiga_decode.h"
+
+#include "common/compression/powerpacker.h"
+#include "common/endian.h"
+#include "common/hashmap.h"
+#include "common/util.h"
+
+namespace Macs2 {
+
+static const uint16 kPlanes = 6;
+
+bool parseAmigaMxoo(const byte *mxoo, uint32 mxooSize, AmigaMxooInfo &out) {
+	out = AmigaMxooInfo();
+	if (!mxoo || mxooSize < 24)
+		return false;
+	if (READ_BE_UINT32(mxoo) != MKTAG('M', 'X', 'O', 'O'))
+		return false;
+
+	out.scriptOffset = READ_BE_UINT32(mxoo + 4);
+	out.stringOffset = READ_BE_UINT32(mxoo + 8);
+	out.bodyOffset = 12;
+	if (out.scriptOffset > mxooSize || out.stringOffset > mxooSize)
+		return false;
+	out.bodySize = out.scriptOffset > out.bodyOffset ? out.scriptOffset - out.bodyOffset : 0;
+	if (out.bodySize < 0x66)
+		return false;
+
+	const byte *body = mxoo + out.bodyOffset;
+	if (READ_BE_UINT16(body + 0x0C) != 0x0101)
+		return false;
+
+	for (uint i = 0; i < 21; i++)
+		out.slotOffsets[i] = READ_BE_UINT32(body + 0x0E + i * 4);
+	out.extraOffset = READ_BE_UINT32(body + 0x62);
+	return true;
+}
+
+bool inspectAmigaAnimSlot(const byte *mxoo, uint32 mxooSize, uint32 bodyRelativeOffset, AmigaAnimSlotInfo &out) {
+	out = AmigaAnimSlotInfo();
+	if (!mxoo || bodyRelativeOffset == 0 || bodyRelativeOffset == 0xFFFFFFFF)
+		return false;
+
+	const uint32 absOff = 12 + bodyRelativeOffset;
+	if (absOff + 16 > mxooSize)
+		return false;
+
+	const byte *p = mxoo + absOff;
+	out.headerHint = READ_BE_UINT16(p + 0);
+	out.seqPos = READ_BE_UINT16(p + 2);
+	out.repeatCounter = READ_BE_UINT16(p + 4);
+	out.loopStart = READ_BE_UINT16(p + 6);
+
+	// Sequence length follows the same pattern as the DOS blob: headerHint is often
+	// the frame count; sequence payload is max(hint - 2, 0) bytes, padded to even.
+	const uint16 seqBytes = (out.headerHint >= 2) ? (uint16)(out.headerHint - 2) : 0;
+	const uint16 seqPadded = (seqBytes + 1) & ~1;
+	const uint32 metaOff = absOff + 8 + seqPadded;
+	if (metaOff + 6 > mxooSize)
+		return false;
+
+	out.frameCount = READ_BE_UINT16(mxoo + metaOff + 0);
+	out.width = READ_BE_UINT16(mxoo + metaOff + 2);
+	out.height = READ_BE_UINT16(mxoo + metaOff + 4);
+	out.pixelOffset = metaOff + 6;
+	out.headerSize = out.pixelOffset - absOff;
+
+	if (out.frameCount == 0 || out.width == 0 || out.height == 0)
+		return false;
+	if (out.width > 320 || out.height > 200 || out.frameCount > 256)
+		return false;
+
+	const uint32 rowBytes = (out.width + 7) / 8;
+	const uint32 planeBytes = rowBytes * out.height;
+	const uint32 pixelsNeeded = (uint32)out.frameCount * planeBytes * kPlanes;
+	if (out.pixelOffset + pixelsNeeded > mxooSize)
+		return false;
+
+	out.valid = true;
+	return true;
+}
+
+bool decodeAmigaPlanarFrame(const byte *planar, uint16 width, uint16 height, uint16 frameIndex,
+							uint16 frameCount, Common::Array<byte> &outPixels) {
+	if (!planar || width == 0 || height == 0 || frameIndex >= frameCount)
+		return false;
+
+	const uint32 rowBytes = (width + 7) / 8;
+	const uint32 planeBytes = rowBytes * height;
+	const uint32 frameBytes = planeBytes * kPlanes;
+	const byte *frameBase = planar + (uint32)frameIndex * frameBytes;
+
+	outPixels.clear();
+	outPixels.resize((uint32)width * height);
+	Common::fill(outPixels.begin(), outPixels.end(), 0);
+
+	// Anim slots store 6 bitplanes; colors use planes 0..4 (indices 0..31).
+	// Plane 5 is not applied as a sprite mask here - doing so clears almost all
+	// opaque pixels in the demo's character anims. Color 0 remains transparent
+	// for the existing DOS draw path.
+	for (uint16 y = 0; y < height; y++) {
+		for (uint16 x = 0; x < width; x++) {
+			byte color = 0;
+			const uint32 bitIndex = (uint32)x & 7;
+			const uint32 byteInRow = (uint32)x >> 3;
+			for (uint16 plane = 0; plane < 5; plane++) {
+				const byte *planeRow = frameBase + plane * planeBytes + (uint32)y * rowBytes;
+				if (planeRow[byteInRow] & (0x80 >> bitIndex))
+					color |= (byte)(1 << plane);
+			}
+			outPixels[(uint32)y * width + x] = color;
+		}
+	}
+	return true;
+}
+
+bool convertAmigaAnimSlotToDosBlob(const byte *mxoo, uint32 mxooSize, uint32 bodyRelativeOffset,
+								   Common::Array<byte> &outBlob) {
+	outBlob.clear();
+	AmigaAnimSlotInfo info;
+	if (!inspectAmigaAnimSlot(mxoo, mxooSize, bodyRelativeOffset, info) || !info.valid)
+		return false;
+
+	const byte *planar = mxoo + info.pixelOffset;
+
+	// DOS AnimBlobView layout (LE):
+	//   +0x00 unknown, +0x02 seqPos, +0x04 repeat, +0x06 loopStart, +0x08 delay,
+	//   +0x0A seqLenMinusOne; sequence at +0x0C (N bytes);
+	//   frames at +0x0B + (N+1) = +0x0C + N: uint16 frameCount, then per-frame records
+	// Frame: ox,oy,unk,w,h (LE) + chunky pixels
+	Common::Array<byte> sequence;
+	for (uint16 i = 0; i < info.frameCount; i++)
+		sequence.push_back((byte)(10 + i));
+
+	const uint32 seqLenMinusOne = sequence.size();
+	const uint32 frameDataOffset = 0x0C + seqLenMinusOne;
+	uint32 total = frameDataOffset + 2; // + frameCount word
+	for (uint16 f = 0; f < info.frameCount; f++)
+		total += 10 + (uint32)info.width * info.height;
+
+	outBlob.resize(total);
+	byte *dst = outBlob.data();
+	WRITE_LE_UINT16(dst + 0x00, info.headerHint);
+	WRITE_LE_UINT16(dst + 0x02, info.seqPos ? info.seqPos : 1);
+	WRITE_LE_UINT16(dst + 0x04, info.repeatCounter);
+	WRITE_LE_UINT16(dst + 0x06, info.loopStart);
+	WRITE_LE_UINT16(dst + 0x08, 0); // delay
+	WRITE_LE_UINT16(dst + 0x0A, (uint16)seqLenMinusOne);
+	if (!sequence.empty())
+		memcpy(dst + 0x0C, sequence.data(), sequence.size());
+
+	WRITE_LE_UINT16(dst + frameDataOffset, info.frameCount);
+	uint32 off = frameDataOffset + 2;
+	for (uint16 f = 0; f < info.frameCount; f++) {
+		Common::Array<byte> pixels;
+		if (!decodeAmigaPlanarFrame(planar, info.width, info.height, f, info.frameCount, pixels))
+			return false;
+		WRITE_LE_UINT16(dst + off + 0, 0); // ox
+		WRITE_LE_UINT16(dst + off + 2, 0); // oy
+		WRITE_LE_UINT16(dst + off + 4, 0); // unk
+		WRITE_LE_UINT16(dst + off + 6, info.width);
+		WRITE_LE_UINT16(dst + off + 8, info.height);
+		memcpy(dst + off + 10, pixels.data(), pixels.size());
+		off += 10 + pixels.size();
+	}
+	return true;
+}
+
+bool extractAmigaScript(const byte *mxoo, uint32 mxooSize, Common::Array<byte> &outScript) {
+	outScript.clear();
+	AmigaMxooInfo info;
+	if (!parseAmigaMxoo(mxoo, mxooSize, info))
+		return false;
+	if (info.scriptOffset + 4 > mxooSize)
+		return false;
+	// Script section: u16BE pad, u16BE size, then bytecode
+	const uint16 size = READ_BE_UINT16(mxoo + info.scriptOffset + 2);
+	const uint32 dataOff = info.scriptOffset + 4;
+	if (dataOff + size > mxooSize)
+		return false;
+	outScript.resize(size);
+	if (size)
+		memcpy(outScript.data(), mxoo + dataOff, size);
+	return true;
+}
+
+bool extractAmigaStringBlock(const byte *mxoo, uint32 mxooSize, Common::Array<byte> &outStrings) {
+	outStrings.clear();
+	AmigaMxooInfo info;
+	if (!parseAmigaMxoo(mxoo, mxooSize, info))
+		return false;
+	// String section: u16BE pad, u16BE size, then length-prefixed plaintext entries.
+	// Script string offsets are relative to the entries (after the 4-byte header).
+	if (info.stringOffset + 4 > mxooSize)
+		return false;
+	const uint16 size = READ_BE_UINT16(mxoo + info.stringOffset + 2);
+	const uint32 dataOff = info.stringOffset + 4;
+	if (dataOff + size > mxooSize)
+		return false;
+	outStrings.resize(size);
+	if (size)
+		memcpy(outStrings.data(), mxoo + dataOff, size);
+	return true;
+}
+
+bool convertAmigaSimpleSpriteToDosBlob(const byte *mxoo, uint32 mxooSize, Common::Array<byte> &outBlob) {
+	outBlob.clear();
+	if (!mxoo || mxooSize < 32 || READ_BE_UINT32(mxoo) != MKTAG('M', 'X', 'O', 'O'))
+		return false;
+
+	const uint32 scriptOff = READ_BE_UINT32(mxoo + 4);
+	if (scriptOff <= 12 || scriptOff > mxooSize)
+		return false;
+
+	const byte *body = mxoo + 12;
+	const uint32 bodyLen = scriptOff - 12;
+	if (bodyLen < 20 || body[10] != 1 || body[11] != 1)
+		return false;
+
+	const uint16 frames = READ_BE_UINT16(body + 14);
+	const uint16 width = READ_BE_UINT16(body + 16);
+	const uint16 height = READ_BE_UINT16(body + 18);
+	if (frames == 0 || width == 0 || width > 320 || height == 0 || height > 200)
+		return false;
+
+	const uint32 rowBytes = (width + 7) / 8;
+	const uint32 planeSize = rowBytes * height;
+	const uint32 pixelBytes = bodyLen - 20;
+	if (planeSize == 0 || pixelBytes < planeSize * 5)
+		return false;
+	const uint32 numPlanes = pixelBytes / planeSize;
+	if (numPlanes < 5 || numPlanes > 6 || pixelBytes != planeSize * numPlanes)
+		return false;
+
+	Common::Array<byte> pixels;
+	pixels.resize((uint32)width * height);
+	Common::fill(pixels.begin(), pixels.end(), 0);
+	const byte *pixData = body + 20;
+	for (uint32 plane = 0; plane < 5 && plane < numPlanes; plane++) {
+		const uint32 pOff = plane * planeSize;
+		for (uint16 y = 0; y < height; y++) {
+			for (uint32 bx = 0; bx < rowBytes; bx++) {
+				const byte b = pixData[pOff + y * rowBytes + bx];
+				for (int bit = 0; bit < 8; bit++) {
+					const uint16 x = (uint16)(bx * 8 + bit);
+					if (x < width && (b & (0x80 >> bit)))
+						pixels[y * width + x] |= (byte)(1 << plane);
+				}
+			}
+		}
+	}
+	if (numPlanes >= 6) {
+		const uint32 pOff = 5 * planeSize;
+		for (uint16 y = 0; y < height; y++) {
+			for (uint32 bx = 0; bx < rowBytes; bx++) {
+				const byte b = pixData[pOff + y * rowBytes + bx];
+				for (int bit = 0; bit < 8; bit++) {
+					const uint16 x = (uint16)(bx * 8 + bit);
+					if (x < width && (b & (0x80 >> bit)) == 0)
+						pixels[y * width + x] = 0;
+				}
+			}
+		}
+	}
+
+	// seqLenMinusOne=0 -> sequenceLength=1 -> frame data at 0x0C:
+	// uint16 frameCount, then ox,oy,unk,w,h + pixels.
+	const uint32 total = 0x0C + 2 + 10 + (uint32)width * height;
+	outBlob.resize(total);
+	byte *dst = outBlob.data();
+	WRITE_LE_UINT16(dst + 0x00, frames);
+	WRITE_LE_UINT16(dst + 0x02, 1);
+	WRITE_LE_UINT16(dst + 0x04, 0);
+	WRITE_LE_UINT16(dst + 0x06, 0);
+	WRITE_LE_UINT16(dst + 0x08, 0);
+	WRITE_LE_UINT16(dst + 0x0A, 0); // seqLenMinusOne
+	WRITE_LE_UINT16(dst + 0x0C, 1); // frameCount
+	WRITE_LE_UINT16(dst + 0x0E + 0, 0);
+	WRITE_LE_UINT16(dst + 0x0E + 2, 0);
+	WRITE_LE_UINT16(dst + 0x0E + 4, 0);
+	WRITE_LE_UINT16(dst + 0x0E + 6, width);
+	WRITE_LE_UINT16(dst + 0x0E + 8, height);
+	memcpy(dst + 0x0E + 10, pixels.data(), pixels.size());
+	return true;
+}
+
+bool convertAmigaPortraitAtlasToDosBlob(const byte *mxoo, uint32 mxooSize, uint32 bodyRelativeExtraOffset,
+										Common::Array<byte> &outBlob) {
+	outBlob.clear();
+	if (!mxoo || bodyRelativeExtraOffset == 0 || bodyRelativeExtraOffset == 0xFFFFFFFF)
+		return false;
+
+	const uint32 absExtra = 12 + bodyRelativeExtraOffset;
+	// animateDialoguePortrait @ 0022f79c: copy 14400 bytes; blit D3=D4=D6=0x50,
+	// D5=0xF0. drawSprite plane loop uses 6 separated planes -> 30*80*6=14400.
+	// Atlas is 240x80 (3x80x80 frames). Color from planes 0..4.
+	const uint16 atlasW = 240;
+	const uint16 atlasH = 80;
+	const uint16 frameW = 80;
+	const uint16 frameH = 80;
+	const uint16 frameCount = 3;
+	const uint32 rowBytes = (atlasW + 7) / 8;
+	const uint32 planeBytes = rowBytes * atlasH;
+	const uint32 portraitBytes = planeBytes * 6; // 14400
+	if (absExtra + portraitBytes > mxooSize)
+		return false;
+
+	const byte *src = mxoo + absExtra;
+	Common::Array<byte> atlas;
+	atlas.resize((uint32)atlasW * atlasH);
+	Common::fill(atlas.begin(), atlas.end(), 0);
+	for (uint16 y = 0; y < atlasH; y++) {
+		for (uint16 x = 0; x < atlasW; x++) {
+			byte color = 0;
+			const uint32 bitIndex = (uint32)x & 7;
+			const uint32 byteInRow = (uint32)x >> 3;
+			for (uint16 plane = 0; plane < 5; plane++) {
+				const byte *planeRow = src + plane * planeBytes + (uint32)y * rowBytes;
+				if (planeRow[byteInRow] & (0x80 >> bitIndex))
+					color |= (byte)(1 << plane);
+			}
+			atlas[(uint32)y * atlasW + x] = color;
+		}
+	}
+
+	// DOS anim blob: seqLenMinusOne = frameCount, sequence at +0x0C, frames follow.
+	const uint32 seqLenMinusOne = frameCount;
+	const uint32 frameDataOffset = 0x0C + seqLenMinusOne;
+	const uint32 frameBytes = 10 + (uint32)frameW * frameH;
+	const uint32 total = frameDataOffset + 2 + (uint32)frameCount * frameBytes;
+	outBlob.resize(total);
+	byte *dst = outBlob.data();
+	WRITE_LE_UINT16(dst + 0x00, frameCount);
+	WRITE_LE_UINT16(dst + 0x02, 1);
+	WRITE_LE_UINT16(dst + 0x04, 0);
+	WRITE_LE_UINT16(dst + 0x06, 0);
+	WRITE_LE_UINT16(dst + 0x08, 0);
+	WRITE_LE_UINT16(dst + 0x0A, (uint16)seqLenMinusOne);
+	for (uint16 i = 0; i < frameCount; i++)
+		dst[0x0C + i] = (byte)(10 + i);
+
+	WRITE_LE_UINT16(dst + frameDataOffset, frameCount);
+	uint32 off = frameDataOffset + 2;
+	for (uint16 f = 0; f < frameCount; f++) {
+		WRITE_LE_UINT16(dst + off + 0, 0);
+		WRITE_LE_UINT16(dst + off + 2, 0);
+		WRITE_LE_UINT16(dst + off + 4, 0);
+		WRITE_LE_UINT16(dst + off + 6, frameW);
+		WRITE_LE_UINT16(dst + off + 8, frameH);
+		byte *pixDst = dst + off + 10;
+		Common::fill(pixDst, pixDst + (uint32)frameW * frameH, 0);
+		for (uint16 y = 0; y < frameH; y++) {
+			memcpy(pixDst + (uint32)y * frameW,
+				   atlas.data() + (uint32)y * atlasW + (uint32)f * frameW,
+				   frameW);
+		}
+		// Keep Amiga COLOR indices 0..31. Demo portraits (OO_*) use only the
+		// copper high bank COLOR17..31 (chrome / skin / navy cap) - the same
+		// registers animateDialoguePortrait blits into on hardware. Remapping
+		// into a private 0xD0 bank drifted from live copper and caused the
+		// Freunde-dialogue palette mismatch vs FS-UAE.
+		// Color 0 stays transparent (drawSprite skips 0).
+		off += frameBytes;
+	}
+	return true;
+}
+
+static bool decompressPp20ToBuffer(const byte *src, uint32 srcLen, Common::Array<byte> &out) {
+	out.clear();
+	if (!src || srcLen < 12 || READ_BE_UINT32(src) != MKTAG('P', 'P', '2', '0'))
+		return false;
+
+	uint32 outLen = 0;
+	byte *unpacked = Common::PowerPackerStream::unpackBuffer(src, srcLen, outLen);
+	if (!unpacked || outLen == 0) {
+		delete[] unpacked;
+		return false;
+	}
+	out.resize(outLen);
+	memcpy(out.data(), unpacked, outLen);
+	delete[] unpacked;
+	return true;
+}
+
+bool decodeAmigaMxmmSceneBackground(const byte *mxmm, uint32 mxmmSize,
+									Common::Array<byte> &outPixels,
+									byte outPaletteRgb[768],
+									uint &outColorCount) {
+	outPixels.clear();
+	outColorCount = 0;
+	if (outPaletteRgb) {
+		for (uint i = 0; i < 768; i++)
+			outPaletteRgb[i] = 0;
+	}
+	if (!mxmm || mxmmSize < 14 || !outPaletteRgb)
+		return false;
+	if (READ_BE_UINT32(mxmm) != MKTAG('M', 'X', 'M', 'M'))
+		return false;
+
+	const uint32 chunk0Size = READ_BE_UINT32(mxmm + 10);
+	if (chunk0Size == 0 || 14 + chunk0Size > mxmmSize)
+		return false;
+
+	const byte *chunk0 = mxmm + 14;
+	Common::Array<byte> screen;
+	if (READ_BE_UINT32(chunk0) == MKTAG('P', 'P', '2', '0')) {
+		if (!decompressPp20ToBuffer(chunk0, chunk0Size, screen))
+			return false;
+	} else {
+		screen.resize(chunk0Size);
+		memcpy(screen.data(), chunk0, chunk0Size);
+	}
+
+	if (screen.size() < kAmigaSceneScreenSize)
+		return false;
+
+	Common::Array<byte> planarIndex;
+	planarIndex.resize((uint)kAmigaSceneWidth * kAmigaSceneHeight);
+	Common::fill(planarIndex.begin(), planarIndex.end(), 0);
+
+	const byte *planes = screen.data();
+	const uint32 rowBytes = kAmigaSceneWidth / 8; // 40
+	for (uint16 plane = 0; plane < kAmigaScenePlanes; plane++) {
+		const byte *planeBase = planes + (uint32)plane * kAmigaScenePlaneBytes;
+		for (uint16 y = 0; y < kAmigaSceneHeight; y++) {
+			for (uint32 bx = 0; bx < rowBytes; bx++) {
+				const byte b = planeBase[(uint32)y * rowBytes + bx];
+				for (int bit = 0; bit < 8; bit++) {
+					const uint16 x = (uint16)(bx * 8 + bit);
+					if (b & (0x80 >> bit))
+						planarIndex[(uint32)y * kAmigaSceneWidth + x] |= (byte)(1 << plane);
+				}
+			}
+		}
+	}
+
+	const byte *copper = screen.data() + kAmigaSceneCopperOffset;
+	uint16 base16[16];
+	for (uint i = 0; i < 16; i++)
+		base16[i] = READ_BE_UINT16(copper + i * 2);
+	const byte *lineColors = copper + 0x20; // 200 x 16 x u16BE
+
+	auto amiga12ToRgb8 = [](uint16 rgb, byte &r, byte &g, byte &b) {
+		const byte r4 = (rgb >> 8) & 0xF;
+		const byte g4 = (rgb >> 4) & 0xF;
+		const byte b4 = rgb & 0xF;
+		r = (byte)(r4 * 17);
+		g = (byte)(g4 * 17);
+		b = (byte)(b4 * 17);
+	};
+
+	auto buildPal32 = [&](uint16 y, byte pal32[32][3]) {
+		amiga12ToRgb8(base16[0], pal32[0][0], pal32[0][1], pal32[0][2]);
+		for (uint i = 0; i < 16; i++) {
+			const uint16 c = READ_BE_UINT16(lineColors + (uint32)y * 32 + i * 2);
+			amiga12ToRgb8(c, pal32[1 + i][0], pal32[1 + i][1], pal32[1 + i][2]);
+		}
+		for (uint i = 1; i < 16; i++)
+			amiga12ToRgb8(base16[i], pal32[16 + i][0], pal32[16 + i][1], pal32[16 + i][2]);
+	};
+
+	// Reserve 0..31 for Amiga COLOR registers (sprites) and 32..63 for EHB.
+	byte staticPal[32][3];
+	buildPal32(0, staticPal);
+	for (uint i = 0; i < 32; i++) {
+		outPaletteRgb[i * 3 + 0] = staticPal[i][0];
+		outPaletteRgb[i * 3 + 1] = staticPal[i][1];
+		outPaletteRgb[i * 3 + 2] = staticPal[i][2];
+	}
+	for (uint i = 0; i < 32; i++) {
+		outPaletteRgb[(32 + i) * 3 + 0] = (byte)(staticPal[i][0] / 2);
+		outPaletteRgb[(32 + i) * 3 + 1] = (byte)(staticPal[i][1] / 2);
+		outPaletteRgb[(32 + i) * 3 + 2] = (byte)(staticPal[i][2] / 2);
+	}
+	outColorCount = 64;
+
+	Common::HashMap<uint32, byte> colorToIndex;
+	for (uint i = 0; i < 64; i++) {
+		const uint32 key = ((uint32)outPaletteRgb[i * 3 + 0] << 16) |
+						   ((uint32)outPaletteRgb[i * 3 + 1] << 8) |
+						   outPaletteRgb[i * 3 + 2];
+		if (!colorToIndex.contains(key))
+			colorToIndex[key] = (byte)i;
+	}
+
+	outPixels.resize((uint)kAmigaSceneWidth * kAmigaSceneHeight);
+	for (uint16 y = 0; y < kAmigaSceneHeight; y++) {
+		byte pal32[32][3];
+		buildPal32(y, pal32);
+
+		for (uint16 x = 0; x < kAmigaSceneWidth; x++) {
+			byte idx = planarIndex[(uint32)y * kAmigaSceneWidth + x];
+			byte r, g, b;
+			if (idx >= 32) {
+				const byte base = (byte)(idx - 32);
+				r = (byte)(pal32[base][0] / 2);
+				g = (byte)(pal32[base][1] / 2);
+				b = (byte)(pal32[base][2] / 2);
+			} else {
+				r = pal32[idx][0];
+				g = pal32[idx][1];
+				b = pal32[idx][2];
+			}
+
+			const uint32 key = ((uint32)r << 16) | ((uint32)g << 8) | b;
+			byte outIdx;
+			if (colorToIndex.contains(key)) {
+				outIdx = colorToIndex[key];
+			} else if (outColorCount < 256) {
+				outIdx = (byte)outColorCount;
+				colorToIndex[key] = outIdx;
+				outPaletteRgb[outIdx * 3 + 0] = r;
+				outPaletteRgb[outIdx * 3 + 1] = g;
+				outPaletteRgb[outIdx * 3 + 2] = b;
+				outColorCount++;
+			} else {
+				outIdx = idx < 64 ? idx : (byte)(idx & 31);
+			}
+			outPixels[(uint32)y * kAmigaSceneWidth + x] = outIdx;
+		}
+	}
+
+	return outColorCount > 0;
+}
+
+static bool amigaMxmmIterChunks(const byte *mxmm, uint32 mxmmSize, uint32 &outTrailerOff,
+								Common::Array<const byte *> *outChunkPtrs,
+								Common::Array<uint32> *outChunkSizes) {
+	outTrailerOff = 0;
+	if (!mxmm || mxmmSize < 14 || READ_BE_UINT32(mxmm) != MKTAG('M', 'X', 'M', 'M'))
+		return false;
+
+	uint32 pos = 10;
+	while (pos + 4 <= mxmmSize) {
+		const uint32 chunkSize = READ_BE_UINT32(mxmm + pos);
+		if (chunkSize == 0)
+			break;
+		if (pos + 4 + chunkSize > mxmmSize)
+			break;
+		if (outChunkPtrs && outChunkSizes) {
+			outChunkPtrs->push_back(mxmm + pos + 4);
+			outChunkSizes->push_back(chunkSize);
+		}
+		pos += 4 + chunkSize;
+	}
+	outTrailerOff = pos;
+	return true;
+}
+
+static bool decompressAmigaChunkToBuffer(const byte *chunk, uint32 chunkSize, Common::Array<byte> &out) {
+	out.clear();
+	if (!chunk || chunkSize == 0)
+		return false;
+	if (READ_BE_UINT32(chunk) == MKTAG('P', 'P', '2', '0'))
+		return decompressPp20ToBuffer(chunk, chunkSize, out);
+	out.resize(chunkSize);
+	memcpy(out.data(), chunk, chunkSize);
+	return true;
+}
+
+/**
+ * Advance past MXMM size-prefixed chunks and the trailer script / optional extra
+ * blob so pos sits at the pathfinding table preamble (native load_scene_mxmm
+ * after bindLoadedScriptBlobToSlot @ 00221d90).
+ */
+static bool amigaMxmmSeekSceneTables(const byte *mxmm, uint32 mxmmSize, uint32 &outPos) {
+	outPos = 0;
+	uint32 pos = 0;
+	if (!amigaMxmmIterChunks(mxmm, mxmmSize, pos, nullptr, nullptr))
+		return false;
+
+	// Empty MXAA (and any other leading zero size words) before the script payload.
+	while (pos + 4 <= mxmmSize && READ_BE_UINT32(mxmm + pos) == 0)
+		pos += 4;
+	if (pos + 4 > mxmmSize)
+		return false;
+
+	const uint32 scriptPayloadSize = READ_BE_UINT32(mxmm + pos);
+	pos += 4;
+	if (scriptPayloadSize == 0 || pos + scriptPayloadSize > mxmmSize)
+		return false;
+	pos += scriptPayloadSize;
+
+	if (pos + 4 > mxmmSize)
+		return false;
+	const uint32 extraSize = READ_BE_UINT32(mxmm + pos);
+	pos += 4;
+	if (extraSize > 0) {
+		if (pos + extraSize > mxmmSize)
+			return false;
+		pos += extraSize;
+	}
+
+	outPos = pos;
+	return true;
+}
+
+bool extractAmigaMxmmSceneScript(const byte *mxmm, uint32 mxmmSize,
+								 Common::Array<byte> &outScript,
+								 Common::Array<byte> &outStrings) {
+	outScript.clear();
+	outStrings.clear();
+
+	uint32 trailerOff = 0;
+	if (!amigaMxmmIterChunks(mxmm, mxmmSize, trailerOff, nullptr, nullptr))
+		return false;
+	if (trailerOff >= mxmmSize)
+		return false;
+
+	const byte *trailer = mxmm + trailerOff;
+	uint32 trailerSize = mxmmSize - trailerOff;
+	uint32 pos = 0;
+
+	// Skip leading empty u32 markers (optional trailing chunk sizes of 0).
+	while (pos + 8 <= trailerSize && READ_BE_UINT32(trailer + pos) == 0)
+		pos += 4;
+
+	if (pos + 8 > trailerSize)
+		return false;
+
+	// Native reads one size-prefixed payload; the first u32 inside is often an
+	// inner bytecode length (e.g. 690) with the remaining bytes as script body.
+	const uint32 scriptPayloadSize = READ_BE_UINT32(trailer + pos);
+	pos += 4;
+	if (scriptPayloadSize == 0 || pos + scriptPayloadSize > trailerSize)
+		return false;
+
+	const byte *payload = trailer + pos;
+	pos += scriptPayloadSize;
+
+	uint32 scriptSize = scriptPayloadSize;
+	const byte *script = payload;
+	if (scriptPayloadSize >= 6) {
+		const uint32 innerLen = READ_BE_UINT32(payload);
+		if (innerLen >= 2 && innerLen + 4 <= scriptPayloadSize &&
+			(innerLen + 4 == scriptPayloadSize || innerLen + 4 + 1 == scriptPayloadSize)) {
+			script = payload + 4;
+			scriptSize = innerLen;
+		}
+	}
+
+	// Accept empty/tiny stubs (e.g. single 0x18) and real scene scripts.
+	if (scriptSize >= 2) {
+		const byte op = script[0];
+		const byte len = script[1];
+		if (len > 32 || (uint32)2 + len > scriptSize)
+			return false;
+		if (op != 0x04 && op != 0x05 && op != 0x01 && op != 0x0F && op != 0x0C && op != 0x18 && op != 0x07)
+			return false;
+	}
+
+	outScript.resize(scriptSize);
+	memcpy(outScript.data(), script, scriptSize);
+
+	// Optional extra blob: outer u32 size, payload often starts with an inner
+	// u32 string-bytes length (Ghidra load_scene_mxmm stage 0x19).
+	if (pos + 4 <= trailerSize) {
+		const uint32 extraSize = READ_BE_UINT32(trailer + pos);
+		if (extraSize > 0 && pos + 4 + extraSize <= trailerSize) {
+			const byte *extra = trailer + pos + 4;
+			uint32 stringBytes = extraSize;
+			const byte *stringData = extra;
+			if (extraSize >= 4) {
+				const uint32 inner = READ_BE_UINT32(extra);
+				if (inner > 0 && inner + 4 <= extraSize) {
+					stringData = extra + 4;
+					stringBytes = inner;
+				}
+			}
+			outStrings.resize(stringBytes);
+			memcpy(outStrings.data(), stringData, stringBytes);
+		}
+	}
+
+	return !outScript.empty();
+}
+
+static bool amigaMxmmGetTrailerTableBase(const byte *mxmm, uint32 mxmmSize, uint32 &outPos) {
+	outPos = 0;
+	if (!amigaMxmmSeekSceneTables(mxmm, mxmmSize, outPos))
+		return false;
+	// Native @ 00221d90: skip u16, then 0xA2 pathfinding, skip u16, 0x22 hotspot, 0xA walk.
+	const uint32 kTableBytes = 2 + 0xA2 + 2 + 0x22 + 10;
+	if (outPos + kTableBytes > mxmmSize)
+		return false;
+	outPos += 2; // preamble u16
+	return true;
+}
+
+bool extractAmigaMxmmScenePathfinding(const byte *mxmm, uint32 mxmmSize,
+									  uint16 &outNumPoints,
+									  Common::Array<AmigaPathfindingNode> &outNodes) {
+	outNumPoints = 0;
+	outNodes.clear();
+
+	uint32 pos = 0;
+	if (!amigaMxmmGetTrailerTableBase(mxmm, mxmmSize, pos))
+		return false;
+
+	const byte *table = mxmm + pos;
+	outNumPoints = READ_BE_UINT16(table);
+	if (outNumPoints > 16)
+		outNumPoints = 16;
+
+	outNodes.resize(16);
+	for (uint i = 0; i < 16; i++) {
+		const byte *node = table + 2 + i * 10;
+		AmigaPathfindingNode &dst = outNodes[i];
+		dst.x = READ_BE_UINT16(node + 0);
+		dst.y = READ_BE_UINT16(node + 2);
+		dst.adjacent[0] = node[4];
+		dst.adjacent[1] = node[5];
+		dst.adjacent[2] = node[6];
+		dst.adjacent[3] = node[7];
+		// DOS/Amiga: last word is connection count (not padding). BE on Amiga.
+		dst.numConnections = READ_BE_UINT16(node + 8);
+		if (dst.numConnections > 4)
+			dst.numConnections = 4;
+	}
+	return true;
+}
+
+bool extractAmigaMxmmSceneWalkParams(const byte *mxmm, uint32 mxmmSize,
+									 uint16 &outWalkDepthThresholdY,
+									 uint16 &outWalkDepthScaleFactor,
+									 uint16 &outWalkBaseSpeedPct,
+									 uint16 &outScenePaletteMode,
+									 uint16 &outPaletteDarkenPercent) {
+	outWalkDepthThresholdY = 0;
+	outWalkDepthScaleFactor = 0;
+	outWalkBaseSpeedPct = 0;
+	outScenePaletteMode = 0;
+	outPaletteDarkenPercent = 0;
+
+	uint32 pos = 0;
+	if (!amigaMxmmGetTrailerTableBase(mxmm, mxmmSize, pos))
+		return false;
+
+	// Skip pathfinding (0xA2) + preamble u16 + hotspot (0x22).
+	pos += 0xA2 + 2 + 0x22;
+
+	outWalkDepthThresholdY = READ_BE_UINT16(mxmm + pos + 0);
+	outWalkDepthScaleFactor = READ_BE_UINT16(mxmm + pos + 2);
+	outWalkBaseSpeedPct = READ_BE_UINT16(mxmm + pos + 4);
+	outScenePaletteMode = READ_BE_UINT16(mxmm + pos + 6);
+	outPaletteDarkenPercent = READ_BE_UINT16(mxmm + pos + 8);
+	return true;
+}
+
+bool extractAmigaMxmmSceneMaps(const byte *mxmm, uint32 mxmmSize,
+							   Common::Array<byte> &outPathfinding,
+							   Common::Array<byte> &outDepth,
+							   Common::Array<byte> &outShadow) {
+	outPathfinding.clear();
+	outDepth.clear();
+	outShadow.clear();
+
+	uint32 trailerOff = 0;
+	Common::Array<const byte *> chunkPtrs;
+	Common::Array<uint32> chunkSizes;
+	if (!amigaMxmmIterChunks(mxmm, mxmmSize, trailerOff, &chunkPtrs, &chunkSizes))
+		return false;
+	if (chunkPtrs.size() < 2)
+		return false;
+
+	const uint32 kMapBytes = (uint32)kAmigaSceneWidth * kAmigaSceneHeight; // 64000
+	Common::Array<byte> maps[3];
+	uint mapCount = 0;
+
+	// Chunk0 is the planar screen; subsequent 64000-byte buffers are maps.
+	for (uint i = 1; i < chunkPtrs.size() && mapCount < 3; i++) {
+		Common::Array<byte> decoded;
+		if (!decompressAmigaChunkToBuffer(chunkPtrs[i], chunkSizes[i], decoded))
+			continue;
+		if (decoded.size() != kMapBytes)
+			continue;
+		maps[mapCount++] = Common::move(decoded);
+	}
+
+	if (mapCount == 0)
+		return false;
+
+	// Native load_scene_mxmm order (NOT DOS MCS order):
+	//   Map0 -> depth / draw mask (g_pSceneMap0Buffer; buildDepthMaskFromMap0)
+	//   Map1 -> walkability (g_pSceneMap1Buffer; getWalkabilityAt)
+	//   Map2 -> MXCC hotspot RLE (not a 64000 surface; see extractAmigaMxmmMxccHotspotMap)
+	// DOS MCS is depth, pathfinding, shadow, hotspot. Swapping Map0/Map1 into
+	// ScummVM's pathfinding slot made MM_0004 treat the depth mask as walk
+	// blockers (255 walls), so off-screen walkTo cancelled and waitForWalk
+	// completed early. Map1 on that demo is all zeros (= open, like DOS scene 5).
+	if (mapCount >= 1)
+		outDepth = Common::move(maps[0]);
+	if (mapCount >= 2)
+		outPathfinding = Common::move(maps[1]);
+	if (mapCount >= 3)
+		outShadow = Common::move(maps[2]);
+	return true;
+}
+
+bool extractAmigaMxmmSceneHotspotColors(const byte *mxmm, uint32 mxmmSize,
+										uint16 &outNumHotspots,
+										Common::Array<uint16> &outColorTable) {
+	outNumHotspots = 0;
+	outColorTable.clear();
+
+	uint32 pos = 0;
+	if (!amigaMxmmGetTrailerTableBase(mxmm, mxmmSize, pos))
+		return false;
+
+	// Skip pathfinding (0xA2) + preamble u16 before hotspot table.
+	pos += 0xA2 + 2;
+	if (pos + 0x22 > mxmmSize)
+		return false;
+
+	outNumHotspots = READ_BE_UINT16(mxmm + pos);
+	if (outNumHotspots > 16)
+		outNumHotspots = 16;
+
+	// Same 0x20 byte layout as DOS (f9 00 fe 00 ...). On LE hosts, memcpy into
+	// uint16 yields color in the low byte - matches getHotspotAtPoint.
+	outColorTable.resize(0x20 / sizeof(uint16));
+	memcpy(outColorTable.data(), mxmm + pos + 2, 0x20);
+	return true;
+}
+
+bool extractAmigaMxmmMxccHotspotMap(const byte *mxmm, uint32 mxmmSize,
+									Common::Array<byte> &outHotspotMap) {
+	outHotspotMap.clear();
+
+	uint32 trailerOff = 0;
+	Common::Array<const byte *> chunkPtrs;
+	Common::Array<uint32> chunkSizes;
+	if (!amigaMxmmIterChunks(mxmm, mxmmSize, trailerOff, &chunkPtrs, &chunkSizes))
+		return false;
+
+	const byte *mxcc = nullptr;
+	uint32 mxccSize = 0;
+	for (uint i = 0; i < chunkPtrs.size(); i++) {
+		if (chunkSizes[i] >= 0x19A + 320 &&
+			READ_BE_UINT32(chunkPtrs[i]) == MKTAG('M', 'X', 'C', 'C')) {
+			mxcc = chunkPtrs[i];
+			mxccSize = chunkSizes[i];
+			break;
+		}
+	}
+	if (!mxcc)
+		return false;
+
+	// Header: MXCC + ver u16BE + pad u16BE + payloadSize u16BE (Ghidra Map2).
+	if (READ_BE_UINT16(mxcc + 4) != 1)
+		return false;
+
+	const byte marker = mxcc[5];
+	const uint32 kRowTableOff = 0x0A;
+	const uint32 kRowDataBase = 0x19A;
+	const uint kRows = 200;
+	const uint kWidth = 320;
+	if (kRowTableOff + kRows * 2 > mxccSize || kRowDataBase > mxccSize)
+		return false;
+
+	outHotspotMap.resize(kWidth * kRows);
+	Common::fill(outHotspotMap.begin(), outHotspotMap.end(), 0);
+
+	for (uint y = 0; y < kRows; y++) {
+		// decodeMxccRunLengthAt: y==0 uses offset 0; else BE word at +0x0A+(y-1)*2.
+		uint32 rowOff = 0;
+		if (y > 0)
+			rowOff = READ_BE_UINT16(mxcc + kRowTableOff + (y - 1) * 2);
+
+		uint32 p = kRowDataBase + rowOff;
+		uint x = 0;
+		while (x < kWidth && p < mxccSize) {
+			const byte b = mxcc[p++];
+			if (b == marker) {
+				if (p + 1 >= mxccSize)
+					break;
+				const byte color = mxcc[p++];
+				const byte run = mxcc[p++];
+				for (uint r = 0; r < run && x < kWidth; r++)
+					outHotspotMap[y * kWidth + x++] = color;
+			} else {
+				outHotspotMap[y * kWidth + x++] = b;
+			}
+		}
+	}
+	return true;
+}
+
+bool amigaMxmmHasMxaaOverlay(const byte *mxmm, uint32 mxmmSize) {
+	uint32 pos = 0;
+	if (!amigaMxmmIterChunks(mxmm, mxmmSize, pos, nullptr, nullptr))
+		return false;
+	// Native: immediately after Map2 comes the MXAA size word (0 = absent).
+	if (pos + 4 > mxmmSize)
+		return false;
+	const uint32 mxaaSize = READ_BE_UINT32(mxmm + pos);
+	if (mxaaSize == 0 || pos + 4 + mxaaSize > mxmmSize)
+		return false;
+	const byte *blob = mxmm + pos + 4;
+	if (mxaaSize >= 4 && READ_BE_UINT32(blob) == MKTAG('M', 'X', 'A', 'A'))
+		return true;
+	if (mxaaSize >= 4 && READ_BE_UINT32(blob) == MKTAG('P', 'P', '2', '0')) {
+		Common::Array<byte> decoded;
+		if (decompressAmigaChunkToBuffer(blob, mxaaSize, decoded) &&
+			decoded.size() >= 4 && READ_BE_UINT32(decoded.data()) == MKTAG('M', 'X', 'A', 'A'))
+			return true;
+	}
+	return false;
+}
+
+bool decodeAmigaMxffFont(const byte *mxff, uint32 mxffSize, Common::Array<AmigaMxffGlyph> &outGlyphs) {
+	outGlyphs.clear();
+	if (!mxff || mxffSize < 0x10A + 160)
+		return false;
+	if (READ_BE_UINT32(mxff) != MKTAG('M', 'X', 'F', 'F'))
+		return false;
+	if (READ_BE_UINT16(mxff + 4) != 1)
+		return false;
+
+	const uint16 blitHeight = READ_BE_UINT16(mxff + 6);
+	const uint16 atlasRows = READ_BE_UINT16(mxff + 8);
+	const uint16 atlasWidthPixels = READ_BE_UINT16(mxff + 0xA);
+	const uint16 planes = READ_BE_UINT16(mxff + 0xC);
+	if (blitHeight == 0 || atlasRows == 0 || atlasWidthPixels < 8 || planes != 6)
+		return false;
+
+	const uint32 rowBytes = (uint32)(atlasWidthPixels >> 3);
+	if (rowBytes == 0)
+		return false;
+	const uint32 planeBytes = rowBytes * atlasRows;
+	const uint32 atlasBytes = planeBytes * 6;
+	const uint32 atlasOff = 0x10A;
+	if (atlasOff + atlasBytes > mxffSize)
+		return false;
+
+	const byte *charmap = mxff + 0x0E;
+	const byte *widths = mxff + 0x8C;
+	const byte *atlas = mxff + atlasOff;
+	const uint16 glyphHeight = MIN(blitHeight, atlasRows);
+	const uint16 cellWidth = 16; // drawText: glyphIndex * (height>>3) byte offset -> 16px cells
+
+	// chars 0x20..0x20+0x7D (0x7E entries)
+	for (uint i = 0; i < 0x7E; i++) {
+		const byte ascii = (byte)(0x20 + i);
+		const byte glyphIndex = charmap[i];
+		const byte advance = widths[i];
+		// 0x4E in the map is unused/empty (Ghidra charmap filler).
+		if (glyphIndex == 0x4E || advance == 0)
+			continue;
+
+		const uint32 cellX0 = (uint32)glyphIndex * cellWidth;
+		if (cellX0 + advance > atlasWidthPixels)
+			continue;
+
+		AmigaMxffGlyph glyph;
+		glyph.ascii = (char)ascii;
+		glyph.width = advance;
+		glyph.height = glyphHeight;
+		glyph.pixels.resize((uint32)advance * glyphHeight);
+		Common::fill(glyph.pixels.begin(), glyph.pixels.end(), 0);
+
+		for (uint16 y = 0; y < glyphHeight; y++) {
+			for (uint16 x = 0; x < advance; x++) {
+				const uint32 absX = cellX0 + x;
+				byte color = 0;
+				const uint32 bit = absX & 7;
+				const uint32 byteInRow = absX >> 3;
+				for (uint16 plane = 0; plane < 6; plane++) {
+					const byte *planeRow = atlas + plane * planeBytes + (uint32)y * rowBytes;
+					if (planeRow[byteInRow] & (0x80 >> bit))
+						color |= (byte)(1 << plane);
+				}
+				glyph.pixels[(uint32)y * advance + x] = color;
+			}
+		}
+		outGlyphs.push_back(Common::move(glyph));
+	}
+
+	return !outGlyphs.empty();
+}
+
+bool extractAmigaMxosPcm(const byte *mxos, uint32 mxosSize, Common::Array<byte> &outPcm, uint16 &outRateHz) {
+	outPcm.clear();
+	outRateHz = 8000;
+	if (!mxos || mxosSize < 0x20)
+		return false;
+	if (READ_BE_UINT32(mxos) != MKTAG('M', 'X', 'O', 'S'))
+		return false;
+
+	// Demo MXOS: word at +0x10 is the byte offset of the first sample block.
+	const uint16 sampleOff = READ_BE_UINT16(mxos + 0x10);
+	if (sampleOff < 0x14 || sampleOff >= mxosSize)
+		return false;
+
+	// Optional Paula period near the sample header (word after offset on several demos).
+	// period 0 -> keep 8000 Hz. NTSC color clock / period ~= Paula playback rate.
+	if ((uint32)sampleOff + 4 <= mxosSize) {
+		const uint16 period = READ_BE_UINT16(mxos + 0x12);
+		if (period >= 0x50 && period <= 0x400) {
+			const uint32 rate = 3579545u / (uint32)period;
+			if (rate >= 4000 && rate <= 28867)
+				outRateHz = (uint16)rate;
+		}
+	}
+
+	const uint32 pcmBytes = mxosSize - (uint32)sampleOff;
+	if (pcmBytes < 16)
+		return false;
+
+	outPcm.resize(pcmBytes);
+	// Paula samples are signed 8-bit; MacsAudioStream expects unsigned (value-128)*256.
+	for (uint32 i = 0; i < pcmBytes; i++)
+		outPcm[i] = (byte)((int8)mxos[sampleOff + i] + 128);
+
+	return true;
+}
+
+} // End of namespace Macs2
diff --git a/engines/macs2/amiga_decode.h b/engines/macs2/amiga_decode.h
new file mode 100644
index 00000000000..4dbc633e7a0
--- /dev/null
+++ b/engines/macs2/amiga_decode.h
@@ -0,0 +1,270 @@
+/* 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 MACS2_AMIGA_DECODE_H
+#define MACS2_AMIGA_DECODE_H
+
+#include "common/array.h"
+#include "common/scummsys.h"
+
+namespace Macs2 {
+
+/**
+ * Amiga MXOO object body layout (after the 12-byte MXOO header):
+ *   +0x00..0x0B  padding
+ *   +0x0C        signature 0x0101
+ *   +0x0E        21 x uint32BE slot offsets (0 / 0xFFFFFFFF = empty)
+ *   +0x62        uint32BE offset of extra/portrait section (often end of anims)
+ *
+ * Each anim slot is planar 6-plane frame data with a short BE header.
+ * Script bytecode is identical to DOS (LE operands). Strings are plaintext
+ * with uint16BE length prefixes (no XOR cipher).
+ *
+ * Game object index = Amiga OO resource id + 1 (OO_0000 -> object 1).
+ */
+struct AmigaMxooInfo {
+	uint32 scriptOffset = 0;
+	uint32 stringOffset = 0;
+	uint32 slotOffsets[21];
+	uint32 extraOffset = 0;
+	uint32 bodyOffset = 12; // start of body within MXOO
+	uint32 bodySize = 0;
+
+	AmigaMxooInfo() {
+		for (uint i = 0; i < 21; i++)
+			slotOffsets[i] = 0xFFFFFFFF;
+	}
+};
+
+struct AmigaAnimSlotInfo {
+	uint16 frameCount = 0;
+	uint16 width = 0;
+	uint16 height = 0;
+	uint16 seqPos = 0;
+	uint16 repeatCounter = 0;
+	uint16 loopStart = 0;
+	uint16 headerHint = 0;
+	uint32 pixelOffset = 0; // absolute offset in MXOO of first planar byte
+	uint32 headerSize = 0;  // bytes from slot start to pixel data
+	bool valid = false;
+};
+
+bool parseAmigaMxoo(const byte *mxoo, uint32 mxooSize, AmigaMxooInfo &out);
+bool inspectAmigaAnimSlot(const byte *mxoo, uint32 mxooSize, uint32 bodyRelativeOffset, AmigaAnimSlotInfo &out);
+
+/** Decode one frame of planar Amiga anim data to chunky 8bpp (color planes 0..4). */
+bool decodeAmigaPlanarFrame(const byte *planar, uint16 width, uint16 height, uint16 frameIndex,
+							uint16 frameCount, Common::Array<byte> &outPixels);
+
+/**
+ * Convert an Amiga anim slot into a DOS-compatible animation blob so the
+ * existing AnimBlobView / renderer path can consume it.
+ */
+bool convertAmigaAnimSlotToDosBlob(const byte *mxoo, uint32 mxooSize, uint32 bodyRelativeOffset,
+								   Common::Array<byte> &outBlob);
+
+/** Extract script bytecode (without the MXOO script section header). */
+bool extractAmigaScript(const byte *mxoo, uint32 mxooSize, Common::Array<byte> &outScript);
+
+/**
+ * Extract Amiga string entries (u16BE length + plaintext), skipping the
+ * MXOO string-section header. Offsets used by scripts are relative to this block.
+ */
+bool extractAmigaStringBlock(const byte *mxoo, uint32 mxooSize, Common::Array<byte> &outStrings);
+
+/** Convert a simple planar MXOO sprite (cursor/icon) into a 1-frame DOS anim blob. */
+bool convertAmigaSimpleSpriteToDosBlob(const byte *mxoo, uint32 mxooSize, Common::Array<byte> &outBlob);
+
+/**
+ * Convert Amiga dialogue portrait atlas (body+0x62) into a DOS anim blob.
+ * Layout: 240x80x6 separated planar = three 80x80 faces (D5=0xF0, D3/D4/D6=0x50,
+ * 6 plane blit in animateDialoguePortrait @ 0022f79c). Color from planes 0..4.
+ * Pixels keep Amiga COLOR indices 0..31 (playfield copper); demo portraits use
+ * only COLOR17..31 from the copper high bank / MXIN chrome ramp.
+ */
+bool convertAmigaPortraitAtlasToDosBlob(const byte *mxoo, uint32 mxooSize, uint32 bodyRelativeExtraOffset,
+										Common::Array<byte> &outBlob);
+
+/** Decompressed MXMM chunk0 screen buffer size (6x8000 planes + copper block). */
+enum : uint32 {
+	kAmigaSceneScreenSize = 54432, // 0xD4A0
+	kAmigaSceneCopperOffset = 0xBB80,
+	kAmigaSceneCopperSize = 0x1920, // 16 base colors + 200x16 line colors
+	kAmigaScenePlaneBytes = 8000 // 40x200
+};
+enum : uint16 {
+	kAmigaSceneWidth = 320,
+	kAmigaSceneHeight = 200,
+	kAmigaScenePlanes = 6 // BPLCON0 = 0x6200 -> EHB
+};
+
+/**
+ * Decode MXMM scene package chunk0 into chunky 8bpp 320x200 pixels and an RGB8
+ * palette (up to 256 entries).
+ *
+ * Palette layout (matches Amiga copper / sprite drawing):
+ * - indices 0..31: COLOR00..31 from the copper base block + first scanline
+ * - indices 32..63: Extra HalfBrite of 0..31 (BPLCON0 0x6200)
+ * - indices 64..*: extra colors needed for per-scanline copper differences
+ *
+ * Character/OO sprites use planes 0..4 against COLOR00..31, so 0..31 must stay
+ * stable Amiga hardware colors.
+ */
+bool decodeAmigaMxmmSceneBackground(const byte *mxmm, uint32 mxmmSize,
+									Common::Array<byte> &outPixels,
+									byte outPaletteRgb[768],
+									uint &outColorCount);
+
+/**
+ * Extract scene script + string block from an MXMM package trailer.
+ *
+ * After the size-prefixed chunks (BG / maps / MXCC / ...), the trailer is:
+ *   [optional u32 zero markers]
+ *   u32BE scriptSize
+ *   u32BE unknown (ignored)
+ *   script[scriptSize]          - DOS-identical LE bytecode
+ *   [optional] u32BE stringBytes + u16BE-length plaintext entries
+ *
+ * Ghidra: load_scene_mxmm reads this after planar BG / map chunks.
+ * Script-visible scene ids are MM_resource_id + 1 (changeScene subtracts 1
+ * before MM lookup; curScene is set to mmId+1 after load).
+ */
+bool extractAmigaMxmmSceneScript(const byte *mxmm, uint32 mxmmSize,
+								 Common::Array<byte> &outScript,
+								 Common::Array<byte> &outStrings);
+
+/**
+ * Extract scene walk / palette scalars from the MXMM trailer tables.
+ *
+ * Ghidra load_scene_mxmm @ 00221de8 reads 10 bytes after pathfinding (0xA2) and
+ * hotspot (0x22) tables into DAT_002376da..e2 (five BE u16s), matching DOS
+ * scene+0x51FD..0x5205:
+ *   [0] walk depth threshold Y
+ *   [1] walk depth scale factor
+ *   [2] walk base speed percent
+ *   [3] scene palette mode
+ *   [4] palette darken percent
+ *
+ * Without these, ScummVM's DOS walkAlongPath formula collapses to 1 px/frame
+ * when the Amiga native path never loads MCS scene metadata.
+ */
+bool extractAmigaMxmmSceneWalkParams(const byte *mxmm, uint32 mxmmSize,
+									 uint16 &outWalkDepthThresholdY,
+									 uint16 &outWalkDepthScaleFactor,
+									 uint16 &outWalkBaseSpeedPct,
+									 uint16 &outScenePaletteMode,
+									 uint16 &outPaletteDarkenPercent);
+
+/**
+ * One pathfinding graph node from the MXMM trailer 0xA2 table
+ * (Ghidra load_scene_mxmm @ 00221d9a -> 00248794).
+ *
+ * Layout (BE), matching DOS MCS 16x10-byte nodes:
+ *   u16 x, u16 y, u8 adj[4], u16 unused
+ * Prefixed by u16BE active node count (DOS scene+0x51F7).
+ */
+struct AmigaPathfindingNode {
+	uint16 x = 0;
+	uint16 y = 0;
+	byte adjacent[4] = {0, 0, 0, 0};
+	uint16 numConnections = 0;
+};
+
+/**
+ * Extract pathfinding node graph from the MXMM trailer (0xA2 bytes after a
+ * skipped u16). Always yields 16 node slots; outNumPoints is the active count.
+ * Without this, Amiga scenes keep _numPathfindingPoints==0, calculatePath fails,
+ * walks cancel early, and waitForWalk completes at the stuck position.
+ */
+bool extractAmigaMxmmScenePathfinding(const byte *mxmm, uint32 mxmmSize,
+									  uint16 &outNumPoints,
+									  Common::Array<AmigaPathfindingNode> &outNodes);
+
+/**
+ * Decode MXMM map chunks (320x200) after chunk0.
+ * Native order: Map0=depth (g_pSceneMap0Buffer), Map1=walkability
+ * (g_pSceneMap1Buffer). Map2 holds MXCC (hotspot RLE), not a 64000 shadow -
+ * use extractAmigaMxmmMxccHotspotMap for that. A third 64000 PP20 (rare) is
+ * returned as outShadow when present.
+ */
+bool extractAmigaMxmmSceneMaps(const byte *mxmm, uint32 mxmmSize,
+							   Common::Array<byte> &outPathfinding,
+							   Common::Array<byte> &outDepth,
+							   Common::Array<byte> &outShadow);
+
+/**
+ * Extract hotspot color table from the MXMM trailer (0x22 bytes after pathfinding).
+ * Layout matches DOS scene+0x50C3: u16BE numHotspots + 0x20 bytes color words
+ * (color in the first byte of each pair / low byte once stored as uint16 on LE).
+ * MM_0004: num=7, colors 249,254,250,251,253,252,248 - same as DOS scene 5.
+ */
+bool extractAmigaMxmmSceneHotspotColors(const byte *mxmm, uint32 mxmmSize,
+										uint16 &outNumHotspots,
+										Common::Array<uint16> &outColorTable);
+
+/**
+ * Decode MXCC chunk (Map2 / g_pSceneMap2Buffer) to a 320x200 hotspot pixel map.
+ * Ghidra decodeMxccRunLengthAt @ 00233590: per-row RLE with marker at MXCC+5,
+ * row offsets at +0x0A (200xu16BE), row data at +0x19A + offset.
+ * MM_0004 decodes pixel-identical to DOS scene 5 hotspot RLE.
+ * No separate 4th 64000 map chunk exists in the Amiga demo format.
+ */
+bool extractAmigaMxmmMxccHotspotMap(const byte *mxmm, uint32 mxmmSize,
+									Common::Array<byte> &outHotspotMap);
+
+/**
+ * True if MXMM has a non-empty MXAA overlay-anim blob (native tickMxaaOverlayAnims).
+ * Demo MM_0004/MM_0040 have size 0 - no consumer wired yet.
+ */
+bool amigaMxmmHasMxaaOverlay(const byte *mxmm, uint32 mxmmSize);
+
+/**
+ * One decoded MXFF glyph (chunky 8bpp, color 0 = transparent).
+ * Ghidra drawText @ 00224492: charmap at +0x0E, widths at +0x8C, atlas at +0x10A.
+ */
+struct AmigaMxffGlyph {
+	byte ascii = 0;
+	uint16 width = 0;
+	uint16 height = 0;
+	Common::Array<byte> pixels; // width * height
+};
+
+/**
+ * Decode native Amiga MXFF font (FF_0000) into chunky glyphs for GlyphData.
+ *
+ * Header (BE): magic MXFF, ver=1 at +4, blit height at +6, atlas rows at +8,
+ * atlas width-pixels at +0xA (row bytes = value>>3), planes=6 at +0xC.
+ * Glyph index map: +0x0E (0x7E bytes for chars 0x20..).
+ * Advance widths: +0x8C (0x7E bytes).
+ * Planar atlas: +0x10A, rowBytes * atlasRows * 6 planes; cell X = glyphIndex * 16.
+ */
+bool decodeAmigaMxffFont(const byte *mxff, uint32 mxffSize, Common::Array<AmigaMxffGlyph> &outGlyphs);
+
+/**
+ * Extract unsigned 8-bit PCM (+ optional rate) from an MXOS container for MacsAudioStream.
+ * Returns false if magic/version/layout is not recognized.
+ * outRateHz is set from Paula period when present (NTSC clock / period), else 8000.
+ */
+bool extractAmigaMxosPcm(const byte *mxos, uint32 mxosSize, Common::Array<byte> &outPcm, uint16 &outRateHz);
+
+} // End of namespace Macs2
+
+#endif // MACS2_AMIGA_DECODE_H
diff --git a/engines/macs2/amiga_resources.cpp b/engines/macs2/amiga_resources.cpp
new file mode 100644
index 00000000000..04105dcb47b
--- /dev/null
+++ b/engines/macs2/amiga_resources.cpp
@@ -0,0 +1,794 @@
+/* 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 "macs2/macs2.h"
+
+#include "common/archive.h"
+#include "common/debug.h"
+#include "common/memstream.h"
+#include "common/system.h"
+#include "common/util.h"
+#include "graphics/managed_surface.h"
+
+#include "macs2/amiga_decode.h"
+#include "macs2/detection.h"
+#include "macs2/gameobjects.h"
+#include "macs2/macs2_constants.h"
+#include "macs2/view1.h"
+
+namespace Macs2 {
+
+
+bool Macs2Engine::loadAmigaSceneBackground(uint32 sceneResourceId) {
+	if (!_amigaArchive || sceneResourceId == 0 || sceneResourceId > 0xFFFF)
+		return false;
+	if (!_amigaArchive->hasResource(kAmigaResMM, (uint16)sceneResourceId))
+		return false;
+
+	Common::ScopedPtr<Common::SeekableReadStream> stream(
+		_amigaArchive->createReadStreamForResource(kAmigaResMM, (uint16)sceneResourceId));
+	if (!stream)
+		return false;
+
+	const uint32 size = (uint32)stream->size();
+	Common::Array<byte> mxmm;
+	mxmm.resize(size);
+	if (stream->read(mxmm.data(), size) != size)
+		return false;
+
+	Common::Array<byte> pixels;
+	byte paletteRgb[768];
+	uint colorCount = 0;
+	if (!decodeAmigaMxmmSceneBackground(mxmm.data(), size, pixels, paletteRgb, colorCount))
+		return false;
+	if (pixels.size() != (uint)kScreenWidth * kGameHeight || colorCount == 0)
+		return false;
+
+	for (int y = 0; y < kGameHeight; y++) {
+		for (int x = 0; x < kScreenWidth; x++)
+			_sceneBackground.setPixel(x, y, pixels[(uint)y * kScreenWidth + x]);
+	}
+
+	// _palVanilla holds raw 6-bit VGA values (fade math subtracts from these).
+	// _pal is the 8-bit display palette - must be expanded via applyPaletteDarkening().
+	memset(_pal, 0, sizeof(_pal));
+	memset(_palVanilla, 0, sizeof(_palVanilla));
+	for (uint i = 0; i < colorCount && i < 256; i++) {
+		const byte r8 = paletteRgb[i * 3 + 0];
+		const byte g8 = paletteRgb[i * 3 + 1];
+		const byte b8 = paletteRgb[i * 3 + 2];
+		_palVanilla[i * 3 + 0] = (byte)((r8 * 63) / 255);
+		_palVanilla[i * 3 + 1] = (byte)((g8 * 63) / 255);
+		_palVanilla[i * 3 + 2] = (byte)((b8 * 63) / 255);
+	}
+
+	// Keep Info UI chrome colors in the high VGA indices used by panel drawing.
+	if (_amigaArchive->getInfo().loaded) {
+		const AmigaInfoData &info = _amigaArchive->getInfo();
+		auto amiga12ToVga6 = [](uint16 rgb, byte &r6, byte &g6, byte &b6) {
+			const byte r4 = (rgb >> 8) & 0xF;
+			const byte g4 = (rgb >> 4) & 0xF;
+			const byte b4 = rgb & 0xF;
+			r6 = (byte)((r4 * 63) / 15);
+			g6 = (byte)((g4 * 63) / 15);
+			b6 = (byte)((b4 * 63) / 15);
+		};
+		for (uint i = 0; i < 16; i++) {
+			byte r6, g6, b6;
+			amiga12ToVga6(info.uiPaletteAmiga[i], r6, g6, b6);
+			const uint idx = 0xF0 + i;
+			if (idx >= 256)
+				break;
+			_palVanilla[idx * 3 + 0] = r6;
+			_palVanilla[idx * 3 + 1] = g6;
+			_palVanilla[idx * 3 + 2] = b6;
+		}
+	}
+	_amigaNativePlayfieldPalette = true;
+	// Portraits share playfield COLOR17..31 (copper high bank). Native copper
+	// already filled those slots; installAmigaPortraitPalette is a no-op here.
+	installAmigaPortraitPalette(true);
+	buildAmigaPanelRemapTable();
+	applyPaletteDarkening();
+
+	_depthMap.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
+	_pathfindingMap.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
+	_shadowMap.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
+	_hotspotMap.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
+
+	Common::Array<byte> pathMap, depthMap, shadowMap;
+	if (extractAmigaMxmmSceneMaps(mxmm.data(), size, pathMap, depthMap, shadowMap)) {
+		auto blitMap = [](Graphics::ManagedSurface &dest, const Common::Array<byte> &src) {
+			if (src.size() != (uint)kScreenWidth * kGameHeight)
+				return;
+			for (int y = 0; y < kGameHeight; y++) {
+				for (int x = 0; x < kScreenWidth; x++)
+					dest.setPixel(x, y, src[(uint)y * kScreenWidth + x]);
+			}
+		};
+		blitMap(_pathfindingMap, pathMap);
+		blitMap(_depthMap, depthMap);
+		blitMap(_shadowMap, shadowMap);
+	}
+
+	// Hotspot colors (trailer 0x22) + MXCC RLE map (Map2). No 4th 64000 hotspot
+	// chunk on Amiga - decodeMxccRunLengthAt expands MXCC to the DOS hotspot map.
+	_numHotspots = 0;
+	_hotspotColorTable.clear();
+	uint16 numHotspots = 0;
+	Common::Array<uint16> hotspotColors;
+	if (extractAmigaMxmmSceneHotspotColors(mxmm.data(), size, numHotspots, hotspotColors)) {
+		_numHotspots = numHotspots;
+		_hotspotColorTable = Common::move(hotspotColors);
+	}
+	Common::Array<byte> hotspotMap;
+	if (extractAmigaMxmmMxccHotspotMap(mxmm.data(), size, hotspotMap) &&
+		hotspotMap.size() == (uint)kScreenWidth * kGameHeight) {
+		for (int y = 0; y < kGameHeight; y++) {
+			for (int x = 0; x < kScreenWidth; x++)
+				_hotspotMap.setPixel(x, y, hotspotMap[(uint)y * kScreenWidth + x]);
+		}
+	}
+
+	_backgroundAnimations.clear();
+	_backgroundAnimationsBlobs.clear();
+	_mapImageFileOffset = 0;
+	_mapSubSceneTableFilePos = 0;
+	// Trailer 0x40+0x80 after walk params: native copies to 0024888c/00248954.
+	// Demo scenes are all zeros; no other xrefs in this binary - leave
+	// _sceneResourceOffsets alone (Amiga loads by archive type/id).
+	// MXAA: size word after Map2 is 0 on MM_0004/MM_0040; tickMxaaOverlayAnims
+	// has nothing to run until a scene ships a real MXAA blob.
+	if (amigaMxmmHasMxaaOverlay(mxmm.data(), size)) {
+		debugC(1, kDebugFilePath, "Amiga: MM_%04u has MXAA overlay data (not loaded yet)",
+			   (uint)sceneResourceId);
+	}
+
+	// Pathfinding graph + walk depth/speed live in the MXMM trailer (Ghidra
+	// load_scene_mxmm @ 00221d90). Without nodes, calculatePath always fails and
+	// walkAlongPath cancels with finalDest=current - waitForWalk then completes
+	// immediately while the actor is still short of the script target.
+	pathfindingPoints.clear();
+	_numPathfindingPoints = 0;
+	uint16 numPfPoints = 0;
+	Common::Array<AmigaPathfindingNode> pfNodes;
+	if (extractAmigaMxmmScenePathfinding(mxmm.data(), size, numPfPoints, pfNodes)) {
+		_numPathfindingPoints = numPfPoints;
+		for (uint i = 0; i < pfNodes.size(); i++) {
+			PathfindingPoint current;
+			current._index = (uint16)i;
+			current._position.x = (int16)pfNodes[i].x;
+			current._position.y = (int16)pfNodes[i].y;
+			current._adjacentPoints.clear();
+			const uint16 nConn = MIN<uint16>(pfNodes[i].numConnections, 4);
+			for (uint j = 0; j < nConn; j++) {
+				if (pfNodes[i].adjacent[j] != 0)
+					current._adjacentPoints.push_back(pfNodes[i].adjacent[j]);
+			}
+			pathfindingPoints.push_back(current);
+		}
+	}
+
+	// Walk depth/speed percent (Ghidra g_abSceneWalkPaletteParams / DOS 0x51FD..).
+	// Leaving base at 0 makes walkAlongPath clamp to 1 px/frame and cancel early.
+	uint16 walkThreshY = 0, walkScale = 0, walkBasePct = 0, palMode = 0, darken = 0;
+	if (extractAmigaMxmmSceneWalkParams(mxmm.data(), size, walkThreshY, walkScale, walkBasePct, palMode,
+										darken)) {
+		_walkDepthThresholdY = walkThreshY;
+		_walkDepthScaleFactor = walkScale;
+		_walkBaseSpeedPct = walkBasePct;
+		_scenePaletteMode = palMode != 0 ? palMode : 1;
+		_paletteDarkenPercent = darken;
+	} else {
+		// Empty stubs (e.g. MM_0040) and DOS flat rooms use 100/100/100.
+		_walkDepthThresholdY = 100;
+		_walkDepthScaleFactor = 100;
+		_walkBaseSpeedPct = 100;
+		_scenePaletteMode = 1;
+		_paletteDarkenPercent = 0;
+	}
+
+	// Scene script/strings live in the MXMM trailer (not the global scene_table stub).
+	_amigaPendingSceneScript.clear();
+	_amigaPendingSceneStrings.clear();
+	extractAmigaMxmmSceneScript(mxmm.data(), size, _amigaPendingSceneScript, _amigaPendingSceneStrings);
+
+	debugC(1, kDebugFilePath,
+		   "Amiga: loaded native MM_%04u (script %u bytes, strings %u bytes, pfNodes %u, "
+		   "hotspots %u, walk %u/%u/%u)",
+		   (uint)sceneResourceId, (uint)_amigaPendingSceneScript.size(),
+		   (uint)_amigaPendingSceneStrings.size(), (uint)_numPathfindingPoints, (uint)_numHotspots,
+		   (uint)_walkDepthThresholdY, (uint)_walkDepthScaleFactor, (uint)_walkBaseSpeedPct);
+	return true;
+}
+
+// --- loadAmigaMxffFont (macs2.cpp:2807-2866) ---
+bool Macs2Engine::loadAmigaMxffFont() {
+	if (!_amigaArchive || !_amigaArchive->hasResource(kAmigaResFF, 0))
+		return false;
+
+	Common::ScopedPtr<Common::SeekableReadStream> stream(
+		_amigaArchive->createReadStreamForResource(kAmigaResFF, 0));
+	if (!stream)
+		return false;
+
+	const uint32 size = (uint32)stream->size();
+	Common::Array<byte> mxff;
+	mxff.resize(size);
+	if (stream->read(mxff.data(), size) != size)
+		return false;
+
+	Common::Array<AmigaMxffGlyph> glyphs;
+	if (!decodeAmigaMxffFont(mxff.data(), size, glyphs) || glyphs.empty())
+		return false;
+
+	numGlyphs = 0;
+	maxGlyphHeight = 0;
+	amigaTextLinePitch = 0;
+	if (size >= 0x0A) {
+		const uint16 atlasRows = READ_BE_UINT16(mxff.data() + 8);
+		if (atlasRows > 1)
+			amigaTextLinePitch = (uint16)(atlasRows - 1);
+	}
+	for (uint i = 0; i < glyphs.size() && i < 256; i++) {
+		_glyphs[i]._ascii = glyphs[i].ascii;
+		_glyphs[i]._width = glyphs[i].width;
+		_glyphs[i]._height = glyphs[i].height;
+		_glyphs[i]._data = Common::move(glyphs[i].pixels);
+		// MXFF dialogue glyphs use copper COLOR23 (black) + COLOR27 (near-white
+		// outline). Remap into private UI bank 0xF0.. so outdoor copper cannot
+		// recolor text. COLOR17+i -> 0xF1+i (MXIN ui[1+i]); COLOR23->0xF7=ui[7]=0,
+		// COLOR27->0xFB=ui[11]=EEE. drawText @ 00224492 blits via drawSprite.
+		for (uint p = 0; p < _glyphs[i]._data.size(); p++) {
+			const byte c = _glyphs[i]._data[p];
+			if (c == 0)
+				continue;
+			if (c >= 17 && c <= 31)
+				_glyphs[i]._data[p] = (byte)(0xF0 + (c - 16));
+			else if (c < 16)
+				_glyphs[i]._data[p] = (byte)(0xF0 + c);
+		}
+		maxGlyphHeight = MAX(maxGlyphHeight, _glyphs[i]._height);
+		numGlyphs++;
+	}
+	if (amigaTextLinePitch == 0 && maxGlyphHeight > 1)
+		amigaTextLinePitch = (uint16)(maxGlyphHeight - 1);
+	// Reuse dialogue font for panel/save UI until a second MXFF exists.
+	numPanelGlyphs = numGlyphs;
+	maxPanelGlyphHeight = maxGlyphHeight;
+	for (uint i = 0; i < numGlyphs; i++)
+		_panelGlyphs[i] = _glyphs[i];
+
+	debugC(1, kDebugFilePath, "Amiga: loaded MXFF font FF_0000 (%u glyphs, height %u, linePitch %u)",
+		   numGlyphs, maxGlyphHeight, amigaTextLinePitch);
+	return numGlyphs > 0;
+}
+
+bool Macs2Engine::loadAmigaOverlayFontResource(uint16 ffId) {
+	if (_amigaArchive == nullptr || !_amigaArchive->hasResource(kAmigaResFF, ffId))
+		return false;
+
+	Common::ScopedPtr<Common::SeekableReadStream> stream(
+		_amigaArchive->createReadStreamForResource(kAmigaResFF, ffId));
+	if (!stream)
+		return false;
+
+	const uint32 size = (uint32)stream->size();
+	Common::Array<byte> mxff;
+	mxff.resize(size);
+	if (stream->read(mxff.data(), size) != size)
+		return false;
+
+	Common::Array<AmigaMxffGlyph> glyphs;
+	if (!decodeAmigaMxffFont(mxff.data(), size, glyphs) || glyphs.empty())
+		return false;
+
+	numOverlayGlyphs = 0;
+	maxOverlayGlyphHeight = 0;
+	for (uint i = 0; i < glyphs.size() && i < 256; i++) {
+		_overlayGlyphs[i]._ascii = glyphs[i].ascii;
+		_overlayGlyphs[i]._width = glyphs[i].width;
+		_overlayGlyphs[i]._height = glyphs[i].height;
+		_overlayGlyphs[i]._data = Common::move(glyphs[i].pixels);
+		// Same copper->UI-bank remap as loadAmigaMxffFont.
+		for (uint p = 0; p < _overlayGlyphs[i]._data.size(); p++) {
+			const byte c = _overlayGlyphs[i]._data[p];
+			if (c == 0)
+				continue;
+			if (c >= 17 && c <= 31)
+				_overlayGlyphs[i]._data[p] = (byte)(0xF0 + (c - 16));
+			else if (c < 16)
+				_overlayGlyphs[i]._data[p] = (byte)(0xF0 + c);
+		}
+		maxOverlayGlyphHeight = MAX(maxOverlayGlyphHeight, _overlayGlyphs[i]._height);
+		numOverlayGlyphs++;
+	}
+	return numOverlayGlyphs > 0;
+}
+
+bool Macs2Engine::loadAmigaOverlayFont(uint8 resourceIndex) {
+	if (resourceIndex != 0 && _amigaArchive != nullptr) {
+		uint16 ffId = resourceIndex;
+		if (!_amigaArchive->hasResource(kAmigaResFF, ffId) && resourceIndex > 0)
+			ffId = (uint16)(resourceIndex - 1);
+		if (loadAmigaOverlayFontResource(ffId))
+			return true;
+	}
+
+	// Fall back to the already-loaded main MXFF dialogue font.
+	if (numGlyphs == 0)
+		return false;
+
+	numOverlayGlyphs = numGlyphs;
+	maxOverlayGlyphHeight = maxGlyphHeight;
+	for (uint i = 0; i < numGlyphs; i++)
+		_overlayGlyphs[i] = _glyphs[i];
+	return true;
+}
+
+// --- loadAmigaCursorResource (macs2.cpp:2868-2894) ---
+bool Macs2Engine::loadAmigaCursorResource(uint16 resourceId, AnimFrame &out) {
+	out = AnimFrame();
+	if (!_amigaArchive || resourceId == 0)
+		return false;
+
+	Common::ScopedPtr<Common::SeekableReadStream> stream(_amigaArchive->createReadStreamForResource(kAmigaResOO, resourceId));
+	if (!stream)
+		return false;
+
+	const uint32 size = (uint32)stream->size();
+	Common::Array<byte> data;
+	data.resize(size);
+	if (stream->read(data.data(), size) != size)
+		return false;
+
+	uint16 width = 0, height = 0;
+	Common::Array<byte> pixels;
+	if (!Macs2AmigaArchive::decodePlanarSprite(data.data(), size, width, height, pixels))
+		return false;
+
+	out._width = width;
+	out._height = height;
+	out._offsetX = 0;
+	out._offsetY = 0;
+	out._data = Common::move(pixels);
+	return true;
+}
+
+// --- installAmigaPortraitPalette (macs2.cpp:2896-2937) ---
+void Macs2Engine::installAmigaPortraitPalette(bool copyFromPlayfield) {
+	// Portraits now keep Amiga COLOR indices and share the playfield copper
+	// high bank (COLOR17..31), matching animateDialoguePortrait on hardware.
+	// Demo OO_* atlases never touch COLOR01..16.
+	//
+	// When native copper is not resident (pre-scene), seed
+	// COLOR17..31 from MXIN chrome so portraits are not left on the provisional
+	// ramp from applyAmigaUiPalette. copyFromPlayfield is ignored - live copper
+	// already owns those slots after loadAmigaSceneBackground.
+	(void)copyFromPlayfield;
+
+	if (_amigaNativePlayfieldPalette)
+		return;
+
+	auto amiga12ToVga6 = [](uint16 rgb, byte &r6, byte &g6, byte &b6) {
+		const byte r4 = (rgb >> 8) & 0xF;
+		const byte g4 = (rgb >> 4) & 0xF;
+		const byte b4 = rgb & 0xF;
+		r6 = (byte)((r4 * 63) / 15);
+		g6 = (byte)((g4 * 63) / 15);
+		b6 = (byte)((b4 * 63) / 15);
+	};
+
+	static const uint16 kHighBankFallback[15] = {
+		0x0BBA, 0x0EB8, 0x0C96, 0x0A74, 0x0963, 0x0741, 0x0000, 0x049E,
+		0x0C00, 0x0DDC, 0x0EEE, 0x0887, 0x0776, 0x0006, 0x0520
+	};
+
+	// Copper base16: COLOR00 + COLOR17..31 == MXIN ui[0..15].
+	const uint16 *highSrc = kHighBankFallback;
+	if (_amigaArchive && _amigaArchive->getInfo().loaded)
+		highSrc = &_amigaArchive->getInfo().uiPaletteAmiga[1];
+
+	for (uint i = 0; i < 15; i++) {
+		byte r6, g6, b6;
+		amiga12ToVga6(highSrc[i], r6, g6, b6);
+		const uint idx = 17 + i;
+		_palVanilla[idx * 3 + 0] = r6;
+		_palVanilla[idx * 3 + 1] = g6;
+		_palVanilla[idx * 3 + 2] = b6;
+	}
+}
+
+// --- applyAmigaUiPalette (macs2.cpp:2939-2990) ---
+void Macs2Engine::applyAmigaUiPalette() {
+	if (!_amigaArchive || !_amigaArchive->getInfo().loaded)
+		return;
+
+	const AmigaInfoData &info = _amigaArchive->getInfo();
+
+	auto amiga12ToVga6 = [](uint16 rgb, byte &r6, byte &g6, byte &b6) {
+		const byte r4 = (rgb >> 8) & 0xF;
+		const byte g4 = (rgb >> 4) & 0xF;
+		const byte b4 = rgb & 0xF;
+		r6 = (byte)((r4 * 63) / 15);
+		g6 = (byte)((g4 * 63) / 15);
+		b6 = (byte)((b4 * 63) / 15);
+	};
+
+	// Provisional playfield until an MM_* copper list is loaded.
+	// Copper base16 layout: COLOR00 + COLOR17..31 = MXIN ui[0..15]. COLOR01..16
+	// stay a visible ramp (overwritten per-line by scene copper).
+	byte r6, g6, b6;
+	amiga12ToVga6(info.uiPaletteAmiga[0], r6, g6, b6);
+	_palVanilla[0] = 0;
+	_palVanilla[1] = 0;
+	_palVanilla[2] = 0;
+	for (uint i = 1; i < 16; i++) {
+		const byte v = (byte)((i * 63) / 15);
+		_palVanilla[i * 3 + 0] = v;
+		_palVanilla[i * 3 + 1] = (byte)((v * 3) / 4);
+		_palVanilla[i * 3 + 2] = (byte)(v / 2);
+	}
+	for (uint i = 0; i < 15; i++) {
+		amiga12ToVga6(info.uiPaletteAmiga[i + 1], r6, g6, b6);
+		const uint idx = 17 + i;
+		_palVanilla[idx * 3 + 0] = r6;
+		_palVanilla[idx * 3 + 1] = g6;
+		_palVanilla[idx * 3 + 2] = b6;
+	}
+
+	// Amiga UI colors also live in the high indices used by panel/chrome drawing.
+	for (uint i = 0; i < 16; i++) {
+		amiga12ToVga6(info.uiPaletteAmiga[i], r6, g6, b6);
+		const uint idx = 0xF0 + i;
+		if (idx >= 256)
+			break;
+		_palVanilla[idx * 3 + 0] = r6;
+		_palVanilla[idx * 3 + 1] = g6;
+		_palVanilla[idx * 3 + 2] = b6;
+	}
+	_amigaNativePlayfieldPalette = false;
+	installAmigaPortraitPalette(false);
+	buildAmigaPanelRemapTable();
+	applyPaletteDarkening();
+}
+
+// --- buildAmigaPanelRemapTable (macs2.cpp:2992-3018) ---
+void Macs2Engine::buildAmigaPanelRemapTable() {
+	// Ghidra fill_ui_panel_darken_remap @ 002221fe:
+	// bucket = 7 - (R4+G4+B4)/0x18, then index g_awPanelDarkenColorIndices[bucket].
+	// Those indices are playfield copper slots on Amiga. ScummVM must not paint wood
+	// RGB into those slots (breaks intro/scene art). Instead map buckets onto the
+	// private MXIN UI bank already installed at 0xF0..0xFF (opaque brown ramp).
+	if (_panelRemapTable.size() != 0x100)
+		_panelRemapTable.resize(0x100);
+
+	// MXIN UI[0..5] = wood ramp (BBA..741). UI[6] is 0x000 - never use it for fill.
+	static const byte kUiWood[8] = {0, 1, 2, 3, 4, 5, 2, 3};
+	for (uint i = 0; i < 0x100; i++) {
+		const byte r6 = _palVanilla[i * 3 + 0];
+		const byte g6 = _palVanilla[i * 3 + 1];
+		const byte b6 = _palVanilla[i * 3 + 2];
+		const uint r4 = (r6 * 15) / 63;
+		const uint g4 = (g6 * 15) / 63;
+		const uint b4 = (b6 * 15) / 63;
+		const uint sum = r4 + g4 + b4;
+		int bucket = 7 - (int)(sum / 0x18);
+		if (bucket < 0)
+			bucket = 0;
+		if (bucket > 7)
+			bucket = 7;
+		_panelRemapTable[i] = (byte)(0xF0 + kUiWood[bucket]);
+	}
+}
+
+// --- readAmigaResources (macs2.cpp:3020-3317) ---
+void Macs2Engine::readAmigaResources() {
+	_amigaArchive = new Macs2AmigaArchive();
+	if (!_amigaArchive->open())
+		error("readAmigaResources(): Failed to open Amiga DataA/Mdir archive");
+
+	SearchMan.add("macs2amiga", _amigaArchive, 0, false);
+
+	Common::ScopedPtr<Common::SeekableReadStream> sceneTable(_amigaArchive->createSceneTableStream());
+	if (!sceneTable)
+		error("readAmigaResources(): Failed to decompress Amiga scene table");
+
+	// Keep the scene table resident for later Amiga scene loading work.
+	{
+		const uint32 size = (uint32)sceneTable->size();
+		byte *data = (byte *)malloc(size);
+		if (!data)
+			error("readAmigaResources(): Out of memory for scene table");
+		if (sceneTable->read(data, size) != size) {
+			free(data);
+			error("readAmigaResources(): Failed reading scene table");
+		}
+		_fileStream = new Common::MemoryReadStream(data, size, DisposeAfterUse::YES);
+	}
+
+	debugC(1, kDebugFilePath, "Amiga scene table loaded (%d bytes), %u scenes, %u archive members",
+		   (int)_fileStream->size(), _amigaArchive->getSceneCount(), (uint)_amigaArchive->getResourceCount());
+
+	// Cursor / UI icon slots: DOS uses 33 entries; modes 0x13.. map to index mode-1.
+	_imageResources.clear();
+	_imageResources.resize(33);
+
+	const AmigaInfoData &info = _amigaArchive->getInfo();
+	auto tryLoadCursor = [this](uint16 resourceId) {
+		if (resourceId == 0 || resourceId > _imageResources.size())
+			return;
+		AnimFrame frame;
+		if (loadAmigaCursorResource(resourceId, frame))
+			_imageResources[resourceId - 1] = Common::move(frame);
+	};
+
+	if (info.loaded) {
+		for (uint i = 0; i < 5; i++)
+			tryLoadCursor(info.cursorResourceIds[i]);
+		tryLoadCursor(info.useInventoryCursorId);
+		// Panel / map cursor modes used by the engine (0x18, 0x19).
+		tryLoadCursor(0x18);
+		tryLoadCursor(0x19);
+		tryLoadCursor(0x1A);
+	} else {
+		// Fallback: load every small OO sprite whose id fits the cursor table.
+		for (uint16 id = 1; id <= 33; id++) {
+			if (_amigaArchive->hasResource(kAmigaResOO, id))
+				tryLoadCursor(id);
+		}
+	}
+
+	// Some cursor mode ids in Info are not standalone OO sprites in the demo.
+	// Duplicate the first loaded cursor into empty gameplay slots so the UI works.
+	int fallbackIndex = -1;
+	for (uint i = 0; i < _imageResources.size(); i++) {
+		if (!_imageResources[i]._data.empty() && _imageResources[i]._width > 0 &&
+			_imageResources[i]._height > 0) {
+			fallbackIndex = (int)i;
+			break;
+		}
+	}
+	if (fallbackIndex >= 0) {
+		static const uint16 kRequiredModes[] = {0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A};
+		for (uint16 mode : kRequiredModes) {
+			const uint idx = mode - 1;
+			if (idx < _imageResources.size() && _imageResources[idx]._data.empty())
+				_imageResources[idx] = _imageResources[fallbackIndex];
+		}
+	}
+
+	applyAmigaUiPalette();
+
+	// Native MXFF dialogue font (Ghidra drawText @ 00224492). Prefer over DOS MCS glyphs.
+	const bool loadedMxff = loadAmigaMxffFont();
+
+	// Load every OO resource as a GameObject. Object index = resource id + 1.
+	GameObjects::instance()._objects.resize(0x200, nullptr);
+	uint loadedObjects = 0;
+	uint loadedAnims = 0;
+
+	Common::ArchiveMemberList members;
+	_amigaArchive->listMembers(members);
+	for (const Common::ArchiveMemberPtr &member : members) {
+		AmigaResourceType type = kAmigaResUnknown;
+		uint16 resId = 0;
+		if (!Macs2AmigaArchive::parseResourceName(member->getName(), type, resId) || type != kAmigaResOO)
+			continue;
+
+		Common::ScopedPtr<Common::SeekableReadStream> stream(_amigaArchive->createReadStreamForResource(type, resId));
+		if (!stream)
+			continue;
+
+		const uint32 size = (uint32)stream->size();
+		Common::Array<byte> mxoo;
+		mxoo.resize(size);
+		if (stream->read(mxoo.data(), size) != size)
+			continue;
+
+		const uint16 objectIndex = (uint16)(resId + 1);
+		if (objectIndex == 0 || objectIndex > 0x200)
+			continue;
+
+		GameObject *gameObject = GameObjects::instance()._objects[objectIndex - 1];
+		if (gameObject == nullptr) {
+			gameObject = new GameObject();
+			gameObject->_index = objectIndex;
+			GameObjects::instance()._objects[objectIndex - 1] = gameObject;
+		}
+		// Non-zero marks the object as having resident Amiga data (skip DOS file reload).
+		gameObject->_dataOffset = 1;
+		gameObject->_position = Common::Point(0, 0);
+		gameObject->_sceneIndex = 0;
+		gameObject->_orientation = 11;
+		gameObject->_verticalOffsetScale = 0;
+		// MXOO has no DOS +0x185/+0x186 flag bytes. Defaults stay false until we
+		// infer character-style rendering below (or a script opcode sets them).
+
+		while (gameObject->_blobs.size() < 0x15)
+			gameObject->_blobs.push_back(Common::Array<uint8>());
+		while (gameObject->_blobSourceKeys.size() < 0x15)
+			gameObject->_blobSourceKeys.push_back(0);
+		while (gameObject->_blobWalkSpeeds.size() < 0x15)
+			gameObject->_blobWalkSpeeds.push_back(0);
+		while (gameObject->_blobMirrorFlags.size() < 0x15)
+			gameObject->_blobMirrorFlags.push_back(false);
+
+		AmigaMxooInfo mxooInfo;
+		if (parseAmigaMxoo(mxoo.data(), size, mxooInfo)) {
+			for (uint slot = 0; slot < 21; slot++) {
+				Common::Array<byte> blob;
+				if (!convertAmigaAnimSlotToDosBlob(mxoo.data(), size, mxooInfo.slotOffsets[slot], blob) || blob.empty())
+					continue;
+				if (slot < gameObject->_blobs.size())
+					gameObject->_blobs[slot] = Common::move(blob);
+				else
+					gameObject->_blobs.push_back(Common::move(blob));
+				loadedAnims++;
+			}
+
+			// Amiga MXOO omits mirrored walk/stand slots (Ghidra/demo OO_0000: slots
+			// 6-8 and 14-16 empty). DOS stores those slots with bMirrorFlag=1 and
+			// mirrorAnimBlob at load (MCSEXEC loadObjectData). Synthesize the same
+			// post-load state so walkAlongPath DirAvail and drawAllCharacters work.
+			// Walk speeds match DOS protagonist RESOURCE.MCS object 1:
+			//   orients 1..8 -> 2,4,6,4,2,4,6,4
+			static const uint16 kAmigaWalkSpeeds[8] = {2, 4, 6, 4, 2, 4, 6, 4};
+			static const uint8 kMirrorOrientToSource[6][2] = {
+				{6, 4}, {7, 3}, {8, 2}, {14, 12}, {15, 11}, {16, 10}};
+			for (uint i = 0; i < 8; i++) {
+				if (i < gameObject->_blobWalkSpeeds.size())
+					gameObject->_blobWalkSpeeds[i] = kAmigaWalkSpeeds[i];
+			}
+			for (uint m = 0; m < 6; m++) {
+				const uint16 dstOrient = kMirrorOrientToSource[m][0];
+				const uint16 srcOrient = kMirrorOrientToSource[m][1];
+				const uint dstIdx = dstOrient - 1;
+				const uint srcIdx = srcOrient - 1;
+				if (dstIdx >= gameObject->_blobs.size() || srcIdx >= gameObject->_blobs.size())
+					continue;
+				if (!gameObject->_blobs[dstIdx].empty() || gameObject->_blobs[srcIdx].empty())
+					continue;
+				gameObject->_blobs[dstIdx] = gameObject->_blobs[srcIdx];
+				BackgroundAnimationBlob::mirrorAnimBlob(gameObject->_blobs[dstIdx]);
+				gameObject->_blobMirrorFlags[dstIdx] = true;
+				if (dstOrient <= 8 && srcOrient <= 8)
+					gameObject->_blobWalkSpeeds[dstIdx] = kAmigaWalkSpeeds[dstOrient - 1];
+				loadedAnims++;
+			}
+
+			extractAmigaScript(mxoo.data(), size, gameObject->_script);
+			extractAmigaStringBlock(mxoo.data(), size, gameObject->_stringData);
+
+			// Dialogue portraits: 240x80x6 atlas (3x80x80) -> blobs[17]/[18].
+			while (gameObject->_blobs.size() < 19)
+				gameObject->_blobs.push_back(Common::Array<uint8>());
+			Common::Array<byte> portraitBlob;
+			if (convertAmigaPortraitAtlasToDosBlob(mxoo.data(), size, mxooInfo.extraOffset, portraitBlob) &&
+				!portraitBlob.empty()) {
+				gameObject->_blobs[17] = portraitBlob;
+				gameObject->_blobs[18] = portraitBlob;
+				loadedAnims++;
+			}
+
+			// DOS MCS stores hasShading/hasScaling after the 21 anim slots (obj1/6/12/...
+			// = 1). Amiga MXOO omits those bytes; without them drawAllCharacters used
+			// the non-depth blit and actors drew over foreground depth (brown rocks).
+			// Match DOS character objects: walk anims and/or dialogue portrait.
+			uint walkSlots = 0;
+			for (uint s = 0; s < 8 && s < gameObject->_blobs.size(); s++) {
+				if (!gameObject->_blobs[s].empty())
+					walkSlots++;
+			}
+			const bool hasPortrait = !gameObject->_blobs[17].empty();
+			if (walkSlots >= 2 || hasPortrait) {
+				gameObject->_hasScaling = true;
+				gameObject->_hasShading = true;
+			}
+		} else {
+			// Simple planar sprite (cursor / inventory icon): expose as walk + inventory slots.
+			Common::Array<byte> blob;
+			if (convertAmigaSimpleSpriteToDosBlob(mxoo.data(), size, blob) && !blob.empty()) {
+				gameObject->_blobs[0] = blob;
+				gameObject->_blobs[0x13] = blob;
+				loadedAnims++;
+			}
+		}
+
+		loadedObjects++;
+	}
+
+	// Protagonist object slot (OO_0000 -> object 1). Do NOT place him in the room here -
+	// the intro/scene MXMM scripts call moveObject on scene init (same as DOS).
+	GameObject *protagonist = GameObjects::instance()._objects[0];
+	if (protagonist == nullptr) {
+		protagonist = new GameObject();
+		protagonist->_index = 1;
+		protagonist->_dataOffset = 1;
+		GameObjects::instance()._objects[0] = protagonist;
+	}
+	protagonist->_sceneIndex = 0;
+	protagonist->_position = Common::Point(0, 0);
+	protagonist->_orientation = 11;
+
+	Scenes::instance()._currentActorIndex = 1;
+
+	// Info MXIN u32 @ offset 8 = starting MM resource id (demo: 40 -> intro MM_0040).
+	// Script-visible scene id = MM id + 1 (Ghidra load_scene_mxmm / FUN_002215fa).
+	uint16 startResourceId = _amigaArchive->getInfo().startSceneResourceId;
+	if (startResourceId == 0 || !_amigaArchive->hasResource(kAmigaResMM, startResourceId)) {
+		if (_amigaArchive->hasResource(kAmigaResMM, 40))
+			startResourceId = 40;
+		else if (_amigaArchive->hasResource(kAmigaResMM, 4))
+			startResourceId = 4;
+		else
+			startResourceId = 1;
+	}
+	const uint16 startScriptScene = (uint16)(startResourceId + 1);
+	Scenes::instance()._currentSceneIndex = startScriptScene;
+
+	_sceneBackground.create(kScreenWidth, kGameHeight, Graphics::PixelFormat::createFormatCLUT8());
+	_depthMap.create(kScreenWidth, kGameHeight, Graphics::PixelFormat::createFormatCLUT8());
+	_pathfindingMap.create(kScreenWidth, kGameHeight, Graphics::PixelFormat::createFormatCLUT8());
+	_shadowMap.create(kScreenWidth, kGameHeight, Graphics::PixelFormat::createFormatCLUT8());
+	_hotspotMap.create(kScreenWidth, kGameHeight, Graphics::PixelFormat::createFormatCLUT8());
+	_shadingTable.resize(0x800);
+	Common::fill(_shadingTable.begin(), _shadingTable.end(), 0);
+	_panelRemapTable.resize(0x100);
+	buildAmigaPanelRemapTable();
+
+	_numHotspots = 0;
+	_numPathfindingPoints = 0;
+	_scenePaletteMode = 1;
+	_paletteDarkenPercent = 0;
+
+	// Fonts: MXFF dialogue font from DataA (standalone Amiga demo - no DOS MCS).
+	if (!loadedMxff)
+		warning("Amiga: no MXFF font FF_0000 in DataA - text may be missing");
+
+	// Border chrome uses fixed slots 30/31/32. Empty/zero-size tiles make
+	// drawBorderSide spin forever (no events, frameWait appears stuck).
+	auto ensureBorderTile = [this](uint index, byte color, uint16 w, uint16 h) {
+		if (index >= _imageResources.size())
+			return;
+		if (!_imageResources[index]._data.empty() && _imageResources[index]._width > 0 &&
+			_imageResources[index]._height > 0)
+			return;
+		AnimFrame tile;
+		tile._width = w;
+		tile._height = h;
+		tile._data.resize((uint)w * h);
+		Common::fill(tile._data.begin(), tile._data.end(), color);
+		_imageResources[index] = Common::move(tile);
+	};
+	if (_imageResources.size() < 33)
+		_imageResources.resize(33);
+	ensureBorderTile(30, 0xF2, 8, 1);
+	ensureBorderTile(31, 0xF1, 8, 8);
+	ensureBorderTile(32, 0xF3, 8, 1);
+
+	debugC(1, kDebugFilePath,
+		   "Amiga objects loaded: %u resources, %u anim slots; start MM_%04u (script scene %u)",
+		   loadedObjects, loadedAnims, startResourceId, startScriptScene);
+	changeScene(Scenes::instance()._currentSceneIndex);
+}
+
+} // namespace Macs2
diff --git a/engines/macs2/gameobjects.cpp b/engines/macs2/gameobjects.cpp
index 28e900f95c8..80eacbd8d48 100644
--- a/engines/macs2/gameobjects.cpp
+++ b/engines/macs2/gameobjects.cpp
@@ -385,6 +385,17 @@ Macs2::GameObject *Macs2::GameObjects::getObjectByIndex(uint16 index) {
 }
 
 Common::MemoryReadStream *Macs2::GameObjects::readGameObjectStrings(uint16 index, Common::SeekableReadStream *fileStream) {
+	// Amiga: strings live on the GameObject itself (plaintext, u16BE lengths).
+	if (g_engine->isAmiga()) {
+		GameObject *obj = getObjectByIndex(index);
+		if (obj == nullptr)
+			return new Common::MemoryReadStream(nullptr, 0);
+		byte *copy = (byte *)malloc(obj->_stringData.size());
+		if (!obj->_stringData.empty())
+			memcpy(copy, obj->_stringData.data(), obj->_stringData.size());
+		return new Common::MemoryReadStream(copy, obj->_stringData.size(), DisposeAfterUse::YES);
+	}
+
 	const uint32 directoryOffset = g_engine->getMcsDirectoryOffset();
 	// Object string table pointer at directory + index*0xC + 0x17FC.
 	fileStream->seek(directoryOffset + index * 0xC + 0x17FC);
diff --git a/engines/macs2/gameobjects.h b/engines/macs2/gameobjects.h
index 2fc91936bb8..9864c6d7392 100644
--- a/engines/macs2/gameobjects.h
+++ b/engines/macs2/gameobjects.h
@@ -176,6 +176,10 @@ public:
 	// The object-specific script
 	Common::Array<uint8> _script;
 
+	// Amiga: plaintext string entries for this object (script offsets are relative to this).
+	// Empty on DOS; DOS strings are read from RESOURCE.MCS via the scene/object tables.
+	Common::Array<uint8> _stringData;
+
 	// Per-object resource offset table (runtime +0x18D, 128 bytes = 32 dword file offsets).
 	// Loaded from file during loadObjectData. Used by scriptLoadObjectAnim/scriptLoadSpecialAnim
 	// to look up animation resource file addresses for this object.
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index fbbc5feb388..f3144d62429 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -41,6 +41,7 @@
 #include "graphics/pixelformat.h"
 #include "graphics/surface.h"
 #include "gui/debugger.h"
+#include "macs2/amiga_decode.h"
 #include "macs2/debugtools.h"
 #include "macs2/detection.h"
 #include "macs2/hotspot_names.h"
@@ -331,6 +332,13 @@ void Macs2Engine::readExecutable() {
 	exeFileStream->read(containerInventoryIconIndices.data(), 12);
 }
 
+void Macs2Engine::loadBootstrapResources() {
+	if (isAmiga())
+		readAmigaResources();
+	else
+		readResourceFile();
+}
+
 void Macs2Engine::readBackgroundAnimations(Common::MemoryReadStream *stream) {
 	// changeScene (1008:2574): background animation loading at scene+0x50F5.
 	// Per-entry runtime struct (0x10 bytes stride):
@@ -427,6 +435,11 @@ Macs2Engine::~Macs2Engine() {
 	clearCurrentSoundData();
 	_music->deinit();
 	delete _music;
+	if (_amigaArchive) {
+		SearchMan.remove("macs2amiga");
+		delete _amigaArchive;
+		_amigaArchive = nullptr;
+	}
 	delete _fileStream;
 	_fileStream = nullptr;
 	delete _scriptExecutor;
@@ -476,6 +489,32 @@ uint16 Macs2Engine::scaledMusicVolume(uint16 gameAttenuation) const {
 }
 
 bool Macs2Engine::loadSceneGraphics(uint32 sceneIndex) {
+	if (isAmiga()) {
+		uint32 sceneResourceId = 0;
+		if (sceneIndex > 0)
+			sceneResourceId = sceneIndex - 1;
+
+		if (sceneResourceId != 0 && loadAmigaSceneBackground(sceneResourceId))
+			return true;
+
+		_sceneBackground.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
+		_depthMap.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
+		_pathfindingMap.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
+		_shadowMap.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
+		_hotspotMap.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
+		_numHotspots = 0;
+		_numPathfindingPoints = 0;
+		_walkDepthThresholdY = 100;
+		_walkDepthScaleFactor = 100;
+		_walkBaseSpeedPct = 100;
+		_scenePaletteMode = 1;
+		_paletteDarkenPercent = 0;
+		applyAmigaUiPalette();
+		warning("Amiga: no MM_%04u in DataA (script scene %u)",
+				(uint)sceneResourceId, (uint)sceneIndex);
+		return true;
+	}
+
 	const uint32 newSceneIndex = sceneIndex;
 
 	// Background image
@@ -652,6 +691,134 @@ void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
 	_backgroundAnimationsBlobs.clear();
 	memset(_areaOverrides, 0, sizeof(_areaOverrides));
 
+	if (isAmiga()) {
+		// Amiga scripts use scene ids = MM_resource_id + 1 (Ghidra FUN_002215fa
+		// subtracts 1 before load_scene_mxmm; load_scene sets curScene = mmId+1).
+		_amigaPendingSceneScript.clear();
+		_amigaPendingSceneStrings.clear();
+
+		if (!loadSceneGraphics(newSceneIndex))
+			error("changeScene(): Failed to load scene graphics for scene %u", newSceneIndex);
+
+		View1 *currentView = (View1 *)findView("View1");
+		if (currentView != nullptr) {
+			// Do not push _pal here: scriptChangeScene fades the previous
+			// palette to black, then fades the new scene in.
+			for (auto currentCharacter : currentView->_characters) {
+				if (currentCharacter->_gameObject != nullptr)
+					_scriptExecutor->saveWalkRuntime(currentCharacter, currentCharacter->_gameObject);
+				delete currentCharacter;
+			}
+			currentView->_characters.clear();
+			currentView->flushPendingCharacterDeletes();
+
+			GameObject *actorObject = GameObjects::getObjectByIndex(Scenes::instance()._currentActorIndex);
+			if (actorObject != nullptr && actorObject->_sceneIndex == newSceneIndex) {
+				Character *actorChar = new Character();
+				actorChar->_gameObject = actorObject;
+				currentView->_characters.push_back(actorChar);
+				_scriptExecutor->restoreWalkRuntime(actorChar, actorObject);
+				resetCharacterWalkPath(actorChar);
+				_scriptExecutor->saveWalkRuntime(actorChar, actorObject);
+			}
+			for (auto currentObject : GameObjects::instance()._objects) {
+				if (currentObject == nullptr)
+					continue;
+				if (currentObject->_sceneIndex == newSceneIndex &&
+					currentObject->_index != Scenes::instance()._currentActorIndex &&
+					currentObject->_dataOffset != 0) {
+					Character *c = new Character();
+					c->_gameObject = currentObject;
+					currentView->_characters.push_back(c);
+					_scriptExecutor->restoreWalkRuntime(c, currentObject);
+					resetCharacterWalkPath(c);
+					_scriptExecutor->saveWalkRuntime(c, currentObject);
+				}
+			}
+			currentView->rebuildCharacterLookupTable();
+			currentView->_backgroundSurface.copyFrom(_sceneBackground);
+			if (executeScript)
+				currentView->_paletteDirty = true;
+		}
+
+		Scenes::instance()._lastSceneIndex = Scenes::instance()._currentSceneIndex;
+		Scenes::instance()._currentSceneIndex = newSceneIndex;
+		_scriptExecutor->releaseObjectStream();
+		delete Scenes::instance()._currentSceneScript;
+		delete Scenes::instance()._currentSceneStrings;
+
+		byte *scriptCopy = nullptr;
+		uint32 scriptSize = 0;
+		byte *stringCopy = nullptr;
+		uint32 stringSize = 0;
+
+		if (!_amigaPendingSceneScript.empty()) {
+			scriptSize = _amigaPendingSceneScript.size();
+			scriptCopy = (byte *)malloc(scriptSize);
+			if (scriptCopy)
+				memcpy(scriptCopy, _amigaPendingSceneScript.data(), scriptSize);
+			else
+				scriptSize = 0;
+		}
+		if (!_amigaPendingSceneStrings.empty()) {
+			stringSize = _amigaPendingSceneStrings.size();
+			stringCopy = (byte *)malloc(stringSize);
+			if (stringCopy)
+				memcpy(stringCopy, _amigaPendingSceneStrings.data(), stringSize);
+			else
+				stringSize = 0;
+		}
+		_amigaPendingSceneScript.clear();
+		_amigaPendingSceneStrings.clear();
+
+		// Fallback: global scene_table MXOO stub (usually just opcode 0x18 in the demo).
+		if (scriptSize == 0 && _fileStream != nullptr) {
+			AmigaMxooInfo sceneInfo;
+			const uint32 tableSize = (uint32)_fileStream->size();
+			Common::Array<byte> table;
+			table.resize(tableSize);
+			_fileStream->seek(0);
+			if (_fileStream->read(table.data(), tableSize) == tableSize &&
+				parseAmigaMxoo(table.data(), tableSize, sceneInfo)) {
+				Common::Array<byte> script;
+				Common::Array<byte> strings;
+				if (extractAmigaScript(table.data(), tableSize, script) && !script.empty()) {
+					scriptSize = script.size();
+					scriptCopy = (byte *)malloc(scriptSize);
+					if (scriptCopy)
+						memcpy(scriptCopy, script.data(), scriptSize);
+					else
+						scriptSize = 0;
+				}
+				if (stringSize == 0 && extractAmigaStringBlock(table.data(), tableSize, strings)) {
+					stringSize = strings.size();
+					stringCopy = (byte *)malloc(stringSize);
+					if (stringCopy && stringSize)
+						memcpy(stringCopy, strings.data(), stringSize);
+					else
+						stringSize = 0;
+				}
+			}
+		}
+
+		Scenes::instance()._currentSceneScript = new Common::MemoryReadStream(scriptCopy, scriptSize, DisposeAfterUse::YES);
+		Scenes::instance()._currentSceneStrings = new Common::MemoryReadStream(stringCopy, stringSize, DisposeAfterUse::YES);
+		Scenes::instance()._currentSceneSpecialAnimOffsets.clear();
+		_scriptExecutor->setScript(Scenes::instance()._currentSceneScript);
+
+		_pathfindingOverrides.clear();
+		for (uint i = 0; i < _hotspotOverrides.size(); i++)
+			_hotspotOverrides[i] = 0xFFFF;
+
+		// Match DOS changeScene: when View1 is not up yet (first call from
+		// readAmigaResources), only load scene data. Entry init/repeat — including
+		// intro frameWait/changeScene — must run from View1::tick so waits can
+		// complete in the game loop.
+		if (executeScript && currentView != nullptr)
+			_scriptExecutor->runSceneEntryScriptPasses();
+		return;
+	}
+
 	if (!loadSceneGraphics(newSceneIndex))
 		error("changeScene(): Failed to load scene graphics for scene %u", newSceneIndex);
 
@@ -744,6 +911,9 @@ void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
 }
 
 bool Macs2Engine::loadOverlayFont(uint8 resourceIndex, uint16 executingObjectID) {
+	if (isAmiga())
+		return loadAmigaOverlayFont(resourceIndex);
+
 	// Original (1008:d749): looks up file offset from scene/object resource table
 	// at scene+0x5209+index*4 (same table as loadIndexedResource/_sceneResourceOffsets),
 	// seeks to offset+0x10, then calls loadFontData.
@@ -776,7 +946,7 @@ bool Macs2Engine::loadOverlayFont(uint8 resourceIndex, uint16 executingObjectID)
 
 	// Seek to address + 0x10 (original skips 16-byte resource header)
 	_fileStream->seek(address + 0x10, SEEK_SET);
-	uint16 glyphCount = _fileStream->readUint16LE();
+	const uint16 glyphCount = _fileStream->readUint16LE();
 	if (glyphCount == 0 || glyphCount > 256) {
 		_fileStream->seek(oldPos, SEEK_SET);
 		return false;
@@ -1861,6 +2031,17 @@ bool Macs2Engine::loadObjectData(GameObject *obj) {
 		_scriptExecutor->setScriptError(0x19);
 		return false;
 	}
+	// Amiga: OO_* animation/script payloads stay resident after readAmigaResources().
+	// _dataOffset is a non-zero sentinel (not an MCS file offset). Opcode 0x0b
+	// (moveObject) and checkObjectData call this when an object enters the scene;
+	// seeking into RESOURCE.MCS would set script error 0x01.
+	if (isAmiga()) {
+		if (obj->_dataOffset == 0) {
+			_scriptExecutor->setScriptError(0x19);
+			return false;
+		}
+		return true;
+	}
 	if (obj->_dataOffset == 0) {
 		_scriptExecutor->setScriptError(0x19);
 		return false;
@@ -1988,14 +2169,18 @@ bool Macs2Engine::loadObjectData(GameObject *obj) {
 	return true;
 }
 
-void Macs2Engine::setCurrentSoundData(const Common::Array<uint8> &data) {
+void Macs2Engine::setCurrentSoundData(const Common::Array<uint8> &data, int rateHz, int headerSkip) {
 	stopSample();
 	_currentSoundData = data;
+	_currentSoundRate = rateHz > 0 ? rateHz : 0x1F40;
+	_currentSoundHeaderSkip = headerSkip >= 0 ? headerSkip : 0;
 }
 
 void Macs2Engine::clearCurrentSoundData() {
 	stopSample();
 	_currentSoundData.clear();
+	_currentSoundRate = 0x1F40;
+	_currentSoundHeaderSkip = 2;
 }
 
 void Macs2Engine::playSample() {
@@ -2004,7 +2189,11 @@ void Macs2Engine::playSample() {
 
 	stopSample();
 	MacsAudioStream *audioStream = new MacsAudioStream();
-	audioStream->_pos = 2; // Skip 2-byte header (original: size = stored_size - 2)
+	audioStream->_rate = _currentSoundRate;
+	// DOS: skip 2-byte size header. Amiga MXOS extract is raw PCM (skip 0).
+	audioStream->_pos = _currentSoundHeaderSkip;
+	if (audioStream->_pos > (int64)_currentSoundData.size())
+		audioStream->_pos = (int64)_currentSoundData.size();
 	audioStream->_data = _currentSoundData;
 	g_system->getMixer()->playStream(Audio::Mixer::kSFXSoundType, &_currentSoundHandle, audioStream);
 }
@@ -2357,7 +2546,7 @@ bool MacsAudioStream::isStereo() const {
 }
 
 int MacsAudioStream::getRate() const {
-	return 0x1F40;
+	return _rate > 0 ? _rate : 0x1F40;
 }
 
 bool MacsAudioStream::endOfData() const {
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index d6b598ae18f..39d8d2e352d 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -38,6 +38,7 @@
 #include "common/text-to-speech.h"
 #include "common/util.h"
 #include "engines/engine.h"
+#include "macs2/amiga_archive.h"
 #include "macs2/events.h"
 #include "macs2/macs2_constants.h"
 #include "macs2/scriptexecutor.h"
@@ -47,7 +48,9 @@ namespace Macs2 {
 class MacsAudioStream : public Audio::SeekableAudioStream {
 public:
 	Common::Array<byte> _data;
-	int64 _pos;
+	int64 _pos = 0;
+	/** Playback rate in Hz (DOS SB samples are 8000; Amiga MXOS uses Paula period). */
+	int _rate = 0x1F40;
 
 	virtual ~MacsAudioStream() {}
 
@@ -253,6 +256,35 @@ public:
 	Graphics::ManagedSurface readRLEImage(int64 offs, Common::MemoryReadStream *stream);
 
 	void readResourceFile();
+	/** Amiga: open DataA/Mdir, load OO objects as GameObjects, cursors, and scene stubs. */
+	void readAmigaResources();
+	void applyAmigaUiPalette();
+	/**
+	 * Ensure Amiga dialogue portraits can use playfield COLOR17..31.
+	 * After native MM copper load those slots are already correct; otherwise
+	 * seed them from MXIN chrome (copper base16 layout).
+	 */
+	void installAmigaPortraitPalette(bool copyFromPlayfield);
+	/**
+	 * Build _panelRemapTable from luminance buckets (Ghidra fill_ui_panel_darken_remap
+	 * @ 002221fe). Outputs into private UI bank 0xF0.. so playfield/intro colors
+	 * at MXIN darken indices are never overwritten.
+	 */
+	void buildAmigaPanelRemapTable();
+	bool loadAmigaCursorResource(uint16 resourceId, AnimFrame &out);
+	/** Load FF_0000 MXFF into `_glyphs` (Ghidra drawText / g_pFont1Data). */
+	bool loadAmigaMxffFont();
+	/** Opcode 0x38 overlay font: FF_* from DataA, else copy the main MXFF glyphs. */
+	bool loadAmigaOverlayFont(uint8 resourceIndex);
+	/** Load one FF_* MXFF into `_overlayGlyphs`. Returns false if missing/undecodable. */
+	bool loadAmigaOverlayFontResource(uint16 ffId);
+	/**
+	 * Amiga: load native MM_* MXMM package by resource id (not script scene id).
+	 * Script-visible scene ids are resourceId+1 (Ghidra FUN_002215fa / load_scene_mxmm).
+	 * Also extracts trailer script/strings into _amigaPendingScene* for changeScene.
+	 * Palette indices 0..31 stay Amiga COLOR registers for OO sprite compatibility.
+	 */
+	bool loadAmigaSceneBackground(uint32 sceneResourceId);
 
 	// We also need some data from the executable, specifically embedded
 	// Adlib data
@@ -372,6 +404,13 @@ public:
 
 	/** Amiga MXFF line pitch: measureTextWidth @ 00224420 uses (font[+8] - 1). */
 	uint16 amigaTextLinePitch = 0;
+	/** True after loadAmigaSceneBackground installed copper colors in 0..31. */
+	bool _amigaNativePlayfieldPalette = false;
+	/** Amiga DataA/Mdir archive (owned). Null on DOS. */
+	Macs2AmigaArchive *_amigaArchive = nullptr;
+	/** Filled by loadAmigaSceneBackground; consumed by Amiga changeScene. */
+	Common::Array<byte> _amigaPendingSceneScript;
+	Common::Array<byte> _amigaPendingSceneStrings;
 
 	void setCursorMode(Script::MouseMode newMode);
 	void nextCursorMode();
@@ -420,7 +459,13 @@ public:
 	Music *getMusic() const { return _music; }
 	// Returns the Music volume (0-63) scaled by the user's music_volume setting
 	uint16 scaledMusicVolume(uint16 gameAttenuation) const;
-	void setCurrentSoundData(const Common::Array<uint8> &data);
+	/**
+	 * Install PCM for opcode 0x3E / playSample.
+	 * @param rateHz sample rate (DOS Sound Blaster path uses 8000)
+	 * @param headerSkip bytes to skip at start of buffer (DOS resources have a 2-byte size header;
+	 *                   Amiga MXOS extract is raw PCM and uses 0)
+	 */
+	void setCurrentSoundData(const Common::Array<uint8> &data, int rateHz = 0x1F40, int headerSkip = 2);
 	void clearCurrentSoundData();
 	bool hasCurrentSoundData() const { return !_currentSoundData.empty(); }
 	void playSample();
@@ -480,6 +525,8 @@ public:
 	bool readInputFrame(uint16 &mouseX, uint16 &mouseY, uint16 &buttons);
 
 	Common::Array<uint8> _currentSoundData;
+	int _currentSoundRate = 0x1F40;
+	int _currentSoundHeaderSkip = 2;
 	Audio::SoundHandle _currentSoundHandle;
 
 	// Schedules a run of the script the next time the executor is ticked
@@ -509,6 +556,7 @@ public:
 	uint32 getFeatures() const;
 	Common::Platform getPlatform() const { return _gameDescription->platform; }
 	bool isAmiga() const { return getPlatform() == Common::kPlatformAmiga; }
+	Macs2AmigaArchive *getAmigaArchive() const { return _amigaArchive; }
 
 	bool isDemo() const { return getFeatures() & ADGF_DEMO; }
 
@@ -568,11 +616,11 @@ public:
 	/** Depth-map compare Y for sprite occlusion (full Y on DOS). */
 	uint8 depthThresholdForY(int16 charY) const { return (uint8)charY; }
 
-	/** Resource bootstrap (MCS fonts/cursors/shading + first scene). */
-	void loadBootstrapResources() { readResourceFile(); }
+	/** Resource bootstrap (DOS MCS or Amiga DataA/Mdir). */
+	void loadBootstrapResources();
 	/**
 	 * Load scene background, maps, pathfinding, and related scene tables.
-	 * Called from changeScene; platforms can replace this body later.
+	 * Called from changeScene; Amiga uses native MXMM from DataA.
 	 */
 	bool loadSceneGraphics(uint32 sceneIndex);
 
diff --git a/engines/macs2/module.mk b/engines/macs2/module.mk
index 7611e9f2071..84a299ad07a 100644
--- a/engines/macs2/module.mk
+++ b/engines/macs2/module.mk
@@ -1,6 +1,9 @@
 MODULE := engines/macs2
 
 MODULE_OBJS = \
+	amiga_archive.o \
+	amiga_decode.o \
+	amiga_resources.o \
 	midiparser_macs2.o \
 	music.o \
 	dialogs.o \
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 5669eefa209..24404491fb3 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -22,6 +22,8 @@
 #include "macs2/scriptexecutor.h"
 #include "common/debug.h"
 #include "common/memstream.h"
+#include "macs2/amiga_archive.h"
+#include "macs2/amiga_decode.h"
 #include "macs2/debugtools.h"
 #include "macs2/detection.h"
 #include "macs2/gameobjects.h"
@@ -59,7 +61,7 @@ ScriptExecutor::ScriptExecutor() {
 		_variables[i].b = 0;
 	}
 
-	setOpcodeTable(ScriptExecutor::kDosOpcodeTable, ScriptExecutor::kDosOpcodeTableSize);
+	setOpcodeTable(ScriptExecutor::kV1OpcodeTable, ScriptExecutor::kV1OpcodeTableSize);
 }
 
 ScriptExecutor::~ScriptExecutor() {
@@ -356,11 +358,53 @@ bool ScriptExecutor::loadIndexedResource(Common::Array<uint8> &outData, uint8 re
 	return !outData.empty();
 }
 
-bool ScriptExecutor::loadSoundResource(Common::Array<uint8> &outData, uint8 resourceIndex) {
+bool ScriptExecutor::loadSoundResource(Common::Array<uint8> &outData, uint8 resourceIndex,
+									   int &rateHz, int &headerSkip) {
+	rateHz = 0x1F40;
+	headerSkip = 2;
+
+	if (_engine != nullptr && _engine->isAmiga()) {
+		outData.clear();
+		headerSkip = 0;
+		if (resourceIndex == 0 || _engine->getAmigaArchive() == nullptr)
+			return false;
+
+		Macs2AmigaArchive *archive = _engine->getAmigaArchive();
+		// Scripts index OS_* by the same 1-based resource id used in Mdir.
+		uint16 osId = resourceIndex;
+		if (!archive->hasResource(kAmigaResOS, osId)) {
+			if (resourceIndex > 0 && archive->hasResource(kAmigaResOS, (uint16)(resourceIndex - 1)))
+				osId = (uint16)(resourceIndex - 1);
+			else
+				return false;
+		}
+
+		Common::ScopedPtr<Common::SeekableReadStream> stream(
+			archive->createReadStreamForResource(kAmigaResOS, osId));
+		if (!stream)
+			return false;
+		const uint32 size = (uint32)stream->size();
+		Common::Array<byte> mxos;
+		mxos.resize(size);
+		if (stream->read(mxos.data(), size) != size)
+			return false;
+
+		uint16 rate = 8000;
+		if (!extractAmigaMxosPcm(mxos.data(), size, outData, rate) || outData.empty())
+			return false;
+		rateHz = (int)rate;
+		return true;
+	}
+
 	return loadIndexedResource(outData, resourceIndex);
 }
 
 bool ScriptExecutor::loadMusicResource(Common::Array<uint8> &outData, uint8 resourceIndex) {
+	// Amiga DataA has no AdLib/Protracker song blobs (MM_* are scene packages).
+	if (_engine != nullptr && _engine->isAmiga()) {
+		outData.clear();
+		return true;
+	}
 	return loadIndexedResource(outData, resourceIndex);
 }
 
@@ -2647,13 +2691,15 @@ void Script::ScriptExecutor::scriptFadeFromBlack() {
 void Script::ScriptExecutor::scriptLoadPcmSound() {
 	const uint8 resourceIndex = readByte();
 	Common::Array<uint8> soundData;
-	if (!loadSoundResource(soundData, resourceIndex)) {
+	int rateHz = 0x1F40;
+	int headerSkip = 2;
+	if (!loadSoundResource(soundData, resourceIndex, rateHz, headerSkip)) {
 		warning("Opcode 0x3E: failed to load PCM sound resource %u", resourceIndex);
 		return;
 	}
 
 	// Opcode 0x3E only installs the buffer; playback is opcode 0x40.
-	_engine->setCurrentSoundData(soundData);
+	_engine->setCurrentSoundData(soundData, rateHz, headerSkip);
 }
 
 void Script::ScriptExecutor::scriptPlayPcmSound() {
@@ -3367,9 +3413,9 @@ OpcodeResult ScriptExecutor::opWaitForAdlib() {
 	return OpcodeResult::Continue;
 }
 
-// DOS script opcode table (indexed by primary opcode byte).
-// Platforms can install an alternate table via setOpcodeTable().
-const ScriptExecutor::OpcodeEntry ScriptExecutor::kDosOpcodeTable[] = {
+// Script dialect v1 opcode table (indexed by primary opcode byte).
+// Later dialects can install an alternate table via setOpcodeTable().
+const ScriptExecutor::OpcodeEntry ScriptExecutor::kV1OpcodeTable[] = {
 	{nullptr, nullptr},
 	{"setVar", &ScriptExecutor::opSetVar},
 	{"setVarOr", &ScriptExecutor::opSetVarOr},
@@ -3450,7 +3496,7 @@ const ScriptExecutor::OpcodeEntry ScriptExecutor::kDosOpcodeTable[] = {
 	{"setPathfindingRemap", &ScriptExecutor::opSetPathfindingRemap},
 	{"waitForAdlib", &ScriptExecutor::opWaitForAdlib}
 };
-const uint ScriptExecutor::kDosOpcodeTableSize = ARRAYSIZE(ScriptExecutor::kDosOpcodeTable);
+const uint ScriptExecutor::kV1OpcodeTableSize = ARRAYSIZE(ScriptExecutor::kV1OpcodeTable);
 
 ExecutionResult Script::ScriptExecutor::executeOpcodes() {
 	debugC(kDebugScript, "----- Scripting function entered - scene: %.2x 1014: %.2x 1012: %.2x", Scenes::instance()._currentSceneIndex, _isSceneInitRun, _repeatRunFlag);
diff --git a/engines/macs2/scriptexecutor.h b/engines/macs2/scriptexecutor.h
index 463b952d7d5..59af9b705bf 100644
--- a/engines/macs2/scriptexecutor.h
+++ b/engines/macs2/scriptexecutor.h
@@ -120,14 +120,14 @@ public:
 		OpcodeHandler handler;
 	};
 
-	/** Install the active opcode table (DOS today; other platforms later). */
+	/** Install the active opcode table (use when a platform/version remaps opcodes). */
 	void setOpcodeTable(const OpcodeEntry *table, uint size);
 	/** Name for opcode, or "?" if unset/out of range. */
 	const char *opcodeName(uint8 opcode) const;
 
-	/** Default DOS opcode table (class member so private handlers are accessible). */
-	static const OpcodeEntry kDosOpcodeTable[];
-	static const uint kDosOpcodeTableSize;
+	/** Script dialect v1 opcode table (MCS / Amiga demo bytecode). */
+	static const OpcodeEntry kV1OpcodeTable[];
+	static const uint kV1OpcodeTableSize;
 
 private:
 #ifdef DEMACS2
@@ -369,7 +369,13 @@ private:
 	void scriptSkipAlternate();
 
 	bool loadIndexedResource(Common::Array<uint8> &outData, uint8 resourceIndex, uint16 objectTableOffset = 0x189);
-	bool loadSoundResource(Common::Array<uint8> &outData, uint8 resourceIndex);
+	/**
+	 * Load PCM for opcode 0x3E.
+	 * DOS: indexed MCS resource (2-byte header). Amiga: OS_/MXOS from DataA (raw PCM).
+	 * On success, rateHz/headerSkip describe how playSample should consume the buffer.
+	 */
+	bool loadSoundResource(Common::Array<uint8> &outData, uint8 resourceIndex,
+						   int &rateHz, int &headerSkip);
 	bool loadMusicResource(Common::Array<uint8> &outData, uint8 resourceIndex);
 
 	// scriptReadValue: reads a typed value from the script stream


Commit: 4330a2ada73d3c6e93f70588bbbf6d05dcfee72b
    https://github.com/scummvm/scummvm/commit/4330a2ada73d3c6e93f70588bbbf6d05dcfee72b
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00

Commit Message:
MACS2: improved mcs v1 magic number situation (named a few things)

Changed paths:
    engines/macs2/gameobjects.cpp
    engines/macs2/macs2.cpp
    engines/macs2/macs2.h
    engines/macs2/macs2_constants.h
    engines/macs2/midiparser_macs2.cpp
    engines/macs2/scriptexecutor.cpp
    engines/macs2/view1.cpp


diff --git a/engines/macs2/gameobjects.cpp b/engines/macs2/gameobjects.cpp
index 80eacbd8d48..41e6a87ad93 100644
--- a/engines/macs2/gameobjects.cpp
+++ b/engines/macs2/gameobjects.cpp
@@ -66,7 +66,7 @@ Common::Array<uint32> Macs2::Scenes::readSpecialAnimsOffsets(uint16 sceneIndex,
 
 Common::MemoryReadStream *Macs2::Scenes::readSceneStrings(uint16 sceneIndex, Common::SeekableReadStream *fileStream) {
 	const uint32 directoryOffset = g_engine->getMcsDirectoryOffset();
-	// Third dword of entry (sceneIndex-1) / strings blob — DOS formula directory+scene*0xC-4.
+	// Third dword of entry (sceneIndex-1) / strings blob - DOS formula directory+scene*0xC-4.
 	fileStream->seek(directoryOffset + sceneIndex * 0xC - 0x4);
 	uint32 sceneDataOffset2 = fileStream->readUint32LE();
 	fileStream->seek(sceneDataOffset2, SEEK_SET);
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index f3144d62429..645eba875aa 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -113,6 +113,24 @@ Graphics::ManagedSurface Macs2Engine::readRLEImage(int64 offs, Common::MemoryRea
 	return result;
 }
 
+Macs2Engine::McsFileVersion Macs2Engine::detectMcsFileVersion(Common::SeekableReadStream &stream) const {
+	if (stream.size() < (int64)kMcsMagicSize)
+		return McsFileVersion::Unknown;
+
+	const int64 pos = stream.pos();
+	byte magic[kMcsMagicSize];
+	stream.seek(0, SEEK_SET);
+	if (stream.read(magic, kMcsMagicSize) != kMcsMagicSize) {
+		stream.seek(pos, SEEK_SET);
+		return McsFileVersion::Unknown;
+	}
+	stream.seek(pos, SEEK_SET);
+
+	if (memcmp(magic, kMcsMagicV1, kMcsMagicSize) == 0)
+		return McsFileVersion::V1;
+	return McsFileVersion::Unknown;
+}
+
 void Macs2Engine::readResourceFile() {
 	{
 		// Extra scope in order to make sure no code tries to read from the file directly.
@@ -127,23 +145,25 @@ void Macs2Engine::readResourceFile() {
 		_fileStream = new Common::MemoryReadStream(fileData, size, DisposeAfterUse::YES);
 	}
 
-	// Full implementation here
-
-	// File layout (from loadResourceFile at 1008:2e8d):
-	//   0x00: 12-byte magic header "AHFFMCSR0100"
-	//   0x0C: 2 bytes actor index + 2 bytes initial scene index (loaded below)
-	//   0x10: 0x3000 bytes scene table (512 entries * 12 bytes, accessed via seek per scene)
-	//   0x3010: 0x300 bytes vanilla palette (global default, overwritten per-scene by changeScene)
-	//   0x3310: 0x800 bytes shading table -> sceneData+0x53D3 (64 palette entries x 32 levels)
-	//   0x3B10: 33 cursor/icon image entries (4-byte size + blob data each)
-	//   Then: Font 1 (4-byte size + glyph data)
-	//   Then: Font 2 (4-byte size + glyph data)
-	//   Then: 0x400 bytes map scene offsets -> sceneData+0x5DDB
-	// We skip to the cursor images; palette/shading are loaded per-scene in changeScene.
-	_fileStream->seek(0xC + 0x4 + 0x3000 + 0x300);
-	_shadingTable.resize(0x800);
-	_fileStream->read(_shadingTable.data(), 0x800);
-	_fileStream->seek(0xC + 0x4 + 0x3000 + 0x300 + 0x800);
+	_mcsFileVersion = detectMcsFileVersion(*_fileStream);
+	if (_mcsFileVersion != McsFileVersion::V1)
+		error("readResourceFile(): unrecognized MCS magic (expected %s)", kMcsMagicV1);
+
+	loadResourceFileV1();
+}
+
+void Macs2Engine::loadResourceFileV1() {
+	// File layout (loadResourceFile @ 1008:2e8d, magic AHFFMACS0100):
+	//   +0x00  12-byte magic
+	//   +0x0C  actor index, +0x0E initial scene index
+	//   +0x10  directory 0x3000 (512 x 12); object DATA/SCRIPT ptrs at +0x17F4/+0x17F8
+	//   +0x3010 vanilla palette 0x300
+	//   +0x3310 shading table 0x800 -> scene+0x53D3
+	//   then 0x21 cursor/icon images, Font1, Font2, 0x400 map scene offsets -> scene+0x5DDB
+	// Global vanilla palette is overwritten per-scene in changeScene; skip to shading.
+	_fileStream->seek(kMcsV1ShadingTableOffset, SEEK_SET);
+	_shadingTable.resize(kMcsV1ShadingTableSize);
+	_fileStream->read(_shadingTable.data(), kMcsV1ShadingTableSize);
 	readImageResources(_fileStream);
 	// Font 1 follows immediately after the 33 image resource entries.
 	// Original: 4-byte size field (skipped) + 2-byte glyph count + glyph data.
@@ -168,13 +188,12 @@ void Macs2Engine::readResourceFile() {
 	}
 	numPanelGlyphs = font2GlyphCount;
 
-	// Map scene offsets: 0x400 bytes (256 entries x 4 bytes) -> scene+0x5DDB
-	// First entry is the help screen image offset.
-	for (int i = 0; i < 256; i++) {
+	// Map scene offsets -> scene+0x5DDB. First entry is the help screen image offset.
+	for (uint i = 0; i < kMcsV1MapSceneOffsetCount; i++) {
 		_mapSceneOffsets[i] = _fileStream->readUint32LE();
 	}
 
-	_fileStream->seek(0xC, SEEK_SET);
+	_fileStream->seek(kMcsV1ActorIndexOffset, SEEK_SET);
 	Scenes::instance()._currentActorIndex = _fileStream->readUint16LE();
 	uint16 firstSceneIndex = _fileStream->readUint16LE();
 	Scenes::instance()._currentSceneIndex = firstSceneIndex;
@@ -187,10 +206,8 @@ void Macs2Engine::readResourceFile() {
 	// Original allocates all 512 slots, then frees unused ones. We pre-fill with nullptr.
 	GameObjects::instance()._objects.resize(0x200, nullptr);
 	for (int i = 1; i <= 0x200; i++) {
-		// The formula for the seek lives at l0037_0936
-		// The global [0752h] is loaded with 3000h bytes read from offset Ch + 4h in the file
-		// Regarding the 4h offset: Before the 3000h bytes, we have the values of the two globals 0776 and 077C
-		uint32 addressOffset = 0x17F4 + (0xC + 0x04) + i * 0xC;
+		// Directory object DATA dword: file+kMcsV1DirectoryOffset+kMcsV1ObjectDataPtrRel+i*12
+		const uint32 addressOffset = kMcsV1DirectoryOffset + kMcsV1ObjectDataPtrRel + (uint32)i * 0xC;
 		_fileStream->seek(addressOffset, SEEK_SET);
 		uint32 objectOffset = _fileStream->readUint32LE();
 		if (objectOffset == 0) {
@@ -202,7 +219,7 @@ void Macs2Engine::readResourceFile() {
 		gameObject->_index = i;
 		gameObject->_dataOffset = objectOffset;
 
-		// This loading happens around the l0037_082D: mark
+		// Object header (ReadyObject / initGameObject): x, y, scene, orientation, vertical scale
 		uint16 x = _fileStream->readUint16LE();
 		uint16 y = _fileStream->readUint16LE();
 		gameObject->_position = Common::Point(x, y);
@@ -212,8 +229,8 @@ void Macs2Engine::readResourceFile() {
 
 		const uint16 animSlotCount = maxAnimSlots();
 		for (int j = 1; j <= (int)animSlotCount; j++) {
-			// Per-slot data in file: 2 bytes animID, 2 bytes sourceKey, 4 bytes dataSize, data, 2 bytes speed, 1 byte mirrorFlag, 1 byte (discarded)
-			_fileStream->readUint16LE(); // runtime+0x24: animation slot ID (not used at runtime, editor metadata)
+			// Per-slot: animID, sourceKey, dataSize, data, speed, mirrorFlag, discarded byte
+			_fileStream->readUint16LE(); // runtime+0x24: animation slot ID (editor metadata)
 			uint16 blobSourceKey = _fileStream->readUint16LE();
 			uint32 dataSize = _fileStream->readUint32LE();
 			uint8 *data = new uint8[dataSize];
@@ -221,19 +238,12 @@ void Macs2Engine::readResourceFile() {
 			gameObject->_blobs.push_back(Common::Array<uint8>(data, dataSize));
 			delete[] data;
 			gameObject->_blobSourceKeys.push_back(blobSourceKey);
-			// Per-slot wAnimSpeed (slot+0x0C in runtime, walk speed used by walkAlongPath)
 			uint16 blobSpeed = _fileStream->readUint16LE();
 			gameObject->_blobWalkSpeeds.push_back(blobSpeed);
-			// Mirror flag (+0x32 in runtime)
 			uint16 blobMirrorFlag = _fileStream->readByte();
 			_fileStream->readByte(); // slot loaded flag (runtime-only, discarded from file)
 			gameObject->_blobMirrorFlags.push_back(blobMirrorFlag != 0);
 
-			// In order to get to l0037_0BBA: where the blob will be mirrored,
-			// the bytes at +Eh and +Fh must be != 0
-			// +Fh is set related to the inner loop - I think it means that
-			// the blob is empty
-			// +Eh is read here
 			if (blobMirrorFlag != 0) {
 				debugC(kDebugScript, "Object %.4x need to mirror blob %4.x", i, j);
 				if (dataSize > 0) {
@@ -241,16 +251,14 @@ void Macs2Engine::readResourceFile() {
 				}
 			}
 		}
-		// Per-object rendering flags (after all 21 animation slots):
-		// Binary loadObjectData reads these into runtime+0x184, +0x185, +0x186
-		_fileStream->readByte();                                // runtime+0x184: hasInventoryIcon (container flag) - derived dynamically from _blobs[0x13] presence
-		gameObject->_hasShading = _fileStream->readByte() != 0; // runtime+0x185: shading enabled
-		gameObject->_hasScaling = _fileStream->readByte() != 0; // runtime+0x186: scaling enabled
+		// Per-object flags after anim slots (loadObjectData -> runtime+0x184..+0x186)
+		_fileStream->readByte();                                // hasInventoryIcon (derived from slot 0x13)
+		gameObject->_hasShading = _fileStream->readByte() != 0; // runtime+0x185
+		gameObject->_hasScaling = _fileStream->readByte() != 0; // runtime+0x186
 
-		// Read the object script (resource offset table + script bytecode)
-		// Binary: scene table at +0x17F8 holds the script data file offset for each object
-		addressOffset = 0x17F8 + (0xC + 0x04) + i * 0xC;
-		_fileStream->seek(addressOffset, SEEK_SET);
+		// Object SCRIPT ptr in directory (+0x17F8). Zero SCRIPT keeps the object (no script table).
+		const uint32 scriptPtrOffset = kMcsV1DirectoryOffset + kMcsV1ObjectScriptPtrRel + (uint32)i * 0xC;
+		_fileStream->seek(scriptPtrOffset, SEEK_SET);
 
 		objectOffset = _fileStream->readUint32LE();
 		// Binary loadResourceFile prunes an object slot ONLY when its DATA offset
@@ -266,8 +274,7 @@ void Macs2Engine::readResourceFile() {
 			continue;
 		}
 		_fileStream->seek(objectOffset, SEEK_SET);
-		// Resource offset table at +0x18D equivalent in file (128 bytes = 32 dword offsets).
-		// Binary loadObjectData reads this into runtime+0x18D.
+		// Resource offset table (32 dwords).
 		const uint maxObjRes = maxObjectResources();
 		for (uint r = 0; r < maxObjRes; r++) {
 			gameObject->_resourceOffsets[r] = _fileStream->readUint32LE();
@@ -811,8 +818,8 @@ void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
 			_hotspotOverrides[i] = 0xFFFF;
 
 		// Match DOS changeScene: when View1 is not up yet (first call from
-		// readAmigaResources), only load scene data. Entry init/repeat — including
-		// intro frameWait/changeScene — must run from View1::tick so waits can
+		// readAmigaResources), only load scene data. Entry init/repeat - including
+		// intro frameWait/changeScene - must run from View1::tick so waits can
 		// complete in the game loop.
 		if (executeScript && currentView != nullptr)
 			_scriptExecutor->runSceneEntryScriptPasses();
@@ -1215,7 +1222,7 @@ int Macs2Engine::walkableDistance(int nodeA, int nodeB) {
 	const Common::Point &b = pathfindingPoints[nodeB - 1]._position;
 	if (!isPathWalkable(a.y, a.x, b.y, b.x))
 		return 0x500;
-	// Binary search for integer sqrt(dx² + dy²), matching binary at 1008:1293
+	// Binary search for integer sqrt(dx^2 + dy^2), matching binary at 1008:1293
 	int32 dx = abs((int)(b.x - a.x));
 	int32 dy = abs((int)(b.y - a.y));
 	int32 distSq = dx * dx + dy * dy;
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index 39d8d2e352d..b692e855fe8 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -255,7 +255,16 @@ protected:
 public:
 	Graphics::ManagedSurface readRLEImage(int64 offs, Common::MemoryReadStream *stream);
 
+	/** Open RESOURCE.MCS, check magic, dispatch to loadResourceFileV1. */
 	void readResourceFile();
+	/** MCS dialect from the 12-byte file magic. */
+	enum class McsFileVersion {
+		Unknown = 0,
+		V1 // AHFFMACS0100
+	};
+	McsFileVersion detectMcsFileVersion(Common::SeekableReadStream &stream) const;
+	/** Load AHFFMACS0100 layout (loadResourceFile @ 1008:2e8d). */
+	void loadResourceFileV1();
 	/** Amiga: open DataA/Mdir, load OO objects as GameObjects, cursors, and scene stubs. */
 	void readAmigaResources();
 	void applyAmigaUiPalette();
@@ -401,6 +410,7 @@ public:
 	Common::Array<BackgroundAnimationBlob> _backgroundAnimationsBlobs;
 
 	Common::MemoryReadStream *_fileStream = nullptr;
+	McsFileVersion _mcsFileVersion = McsFileVersion::Unknown;
 
 	/** Amiga MXFF line pitch: measureTextWidth @ 00224420 uses (font[+8] - 1). */
 	uint16 amigaTextLinePitch = 0;
@@ -560,8 +570,9 @@ public:
 
 	bool isDemo() const { return getFeatures() & ADGF_DEMO; }
 
-	/** MCS directory base; DOS resource layout uses 0x10. */
-	uint32 getMcsDirectoryOffset() const { return 0x10; }
+	/** MCS directory base (v1: file+0x10 after magic + actor/scene words). */
+	uint32 getMcsDirectoryOffset() const { return kMcsV1DirectoryOffset; }
+	McsFileVersion getMcsFileVersion() const { return _mcsFileVersion; }
 	int screenWidth() const { return kScreenWidth; }
 	int screenWidthLast() const { return screenWidth() - 1; }
 	int gameHeight() const { return kGameHeight; }
@@ -591,7 +602,7 @@ public:
 	bool scriptValuesHaveHighWord() const { return false; }
 	/** Script stream: variable index is followed by a padding word. */
 	bool scriptVarIndexHasPaddingWord() const { return false; }
-	/** Script coordinates → screen/runtime coordinates (identity on DOS). */
+	/** Script coordinates -> screen/runtime coordinates (identity on DOS). */
 	int16 scaleScriptCoord(int16 coord) const { return coord; }
 
 	/** Dialogue / text-box chrome (DOS l0037_B368 / B462). */
diff --git a/engines/macs2/macs2_constants.h b/engines/macs2/macs2_constants.h
index bc707dc8051..817f7bcb574 100644
--- a/engines/macs2/macs2_constants.h
+++ b/engines/macs2/macs2_constants.h
@@ -35,6 +35,30 @@ static constexpr int kUIHeight       = 64;
 static constexpr int kScreenHeight   = kGameHeight + kUIHeight;
 static constexpr int kScreenHeightLast = kScreenHeight - 1;
 
+/**
+ * RESOURCE.MCS header / layout (loadResourceFile @ 1008:2e8d).
+ * Magic is 12 ASCII bytes "AHFFMACS0100".
+ * (Input-recording uses a different 12-byte tag; do not confuse with MCS.)
+ */
+static constexpr uint kMcsMagicSize = 12;
+static constexpr const char *kMcsMagicV1 = "AHFFMACS0100";
+
+/** MCS v1 absolute file offsets (after validating AHFFMACS0100). */
+static constexpr uint32 kMcsV1ActorIndexOffset = 0x0C;
+static constexpr uint32 kMcsV1DirectoryOffset = 0x10;
+static constexpr uint32 kMcsV1DirectorySize = 0x3000; // 512 entries * 12 bytes
+/** Object DATA file-offset dword, relative to directory base (entry i at +rel + i*12). */
+static constexpr uint32 kMcsV1ObjectDataPtrRel = 0x17F4;
+/** Object SCRIPT/resource-table file-offset dword, relative to directory base. */
+static constexpr uint32 kMcsV1ObjectScriptPtrRel = 0x17F8;
+static constexpr uint32 kMcsV1VanillaPaletteOffset = kMcsV1DirectoryOffset + kMcsV1DirectorySize; // 0x3010
+static constexpr uint32 kMcsV1VanillaPaletteSize = 0x300;
+static constexpr uint32 kMcsV1ShadingTableOffset = kMcsV1VanillaPaletteOffset + kMcsV1VanillaPaletteSize; // 0x3310
+static constexpr uint32 kMcsV1ShadingTableSize = 0x800;
+static constexpr uint kMcsV1CursorImageCount = 0x21;
+static constexpr uint kMcsV1MapSceneOffsetCount = 256;
+static constexpr uint32 kMcsV1MapSceneOffsetsSize = kMcsV1MapSceneOffsetCount * 4; // 0x400
+
 } // namespace Macs2
 
 #endif // MACS2_CONSTANTS_H
diff --git a/engines/macs2/midiparser_macs2.cpp b/engines/macs2/midiparser_macs2.cpp
index 0ec816c2dd8..3cbefbac0e8 100644
--- a/engines/macs2/midiparser_macs2.cpp
+++ b/engines/macs2/midiparser_macs2.cpp
@@ -52,8 +52,8 @@ bool MidiParser_Macs2::loadMusic(const byte *data, uint32 size) {
 
 	// The original decrements _nextEventTimer on EVERY timer callback (120 Hz),
 	// not just on subdivision ticks. So delta=1 means 1/120th of a second.
-	// MidiParser: µs per tick = tempo / ppqn. With ppqn=1, tempo = µs per tick.
-	// At 120 callbacks/sec: µs per tick = 1000000/120 = 8333.
+	// MidiParser: us per tick = tempo / ppqn. With ppqn=1, tempo = us per tick.
+	// At 120 callbacks/sec: us per tick = 1000000/120 = 8333.
 	// Since our timer rate is also set to 1000000/120, setting tempo=8333 with
 	// ppqn=1 means exactly 1 tick per callback, matching the original behavior.
 	_ppqn = 1;
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 24404491fb3..4e203342a44 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -698,7 +698,7 @@ bool ScriptExecutor::loadNextScript() {
 	//
 	// Binary: when g_wExecutingScriptObjectId > 0x200 (e.g. opcode 0x29 sentinel),
 	// the outer loop stops without loading further object scripts. Check before the
-	// scene→object transition so a scene-script sentinel is not wiped by resetting
+	// scene->object transition so a scene-script sentinel is not wiped by resetting
 	// _executingObjectIndex to 0.
 	if (_executingScriptObjectId > 0x200)
 		return false;
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index f928bb3e308..b88d77fbc19 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -3994,7 +3994,7 @@ void Character::update() {
 			}
 			// Revert position
 			pos = savedPos;
-			// Wall-sliding: build push vector from ±1 and ±2 samples
+			// Wall-sliding: build push vector from +/-1 and +/-2 samples
 			int pushX = 0, pushY = 0;
 			if (Macs2Engine::isWalkabilityBlocking(lookupWalkability(Common::Point(pos.x + 1, pos.y))))
 				pushX--;


Commit: 3dad819e2d91f0305f8747280a578759e22797ae
    https://github.com/scummvm/scummvm/commit/3dad819e2d91f0305f8747280a578759e22797ae
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00

Commit Message:
MACS2: fixed inventory item transfer in scumm verb ui mode

Changed paths:
    engines/macs2/scummui.cpp
    engines/macs2/scummui.h
    engines/macs2/view1.cpp


diff --git a/engines/macs2/scummui.cpp b/engines/macs2/scummui.cpp
index 3231ad39e8c..de78c7a898d 100644
--- a/engines/macs2/scummui.cpp
+++ b/engines/macs2/scummui.cpp
@@ -285,6 +285,26 @@ bool ScummUI::handleClick(const Common::Point &pos, bool scriptsRunning) {
 	if (scriptsRunning)
 		return true;
 
+	const Script::MouseMode mode = g_engine->_scriptExecutor->_cursorMode;
+	const bool takingFromContainer =
+		mode == Script::MouseMode::UseInventory &&
+		_view->_activeInventoryItem != nullptr &&
+		_view->_uiPanelState == View1::kUiPanelContainerInventory;
+
+	// Drop a held container item onto the protagonist strip (classic Take/Drop).
+	if (takingFromContainer && isPointInInventoryStrip(pos)) {
+		_view->transferInventoryItem(_view->_activeInventoryItem,
+									 GameObjects::instance().getProtagonistObject());
+		_view->_activeInventoryItem = nullptr;
+		g_engine->_scriptExecutor->_inventoryActionFlag = true;
+		g_engine->setCursorMode(Script::MouseMode::Use);
+		_view->updateCursor();
+		_view->setInventorySource(_view->_inventorySource);
+		syncInventory();
+		_view->redraw();
+		return true;
+	}
+
 	const Common::Array<GameObject *> items = getProtagonistItems();
 	const int maxVisible = kInvCols * kInvRows;
 	for (int i = 0; i < maxVisible; i++) {
@@ -296,7 +316,6 @@ bool ScummUI::handleClick(const Common::Point &pos, bool scriptsRunning) {
 			continue;
 
 		GameObject *item = items[itemIdx];
-		const Script::MouseMode mode = g_engine->_scriptExecutor->_cursorMode;
 
 		if (mode == Script::MouseMode::Look) {
 			g_engine->_scriptExecutor->_interactedObjectID = 0x400 + item->_index;
@@ -405,6 +424,13 @@ Common::Rect ScummUI::getInvScrollLeftRect() const {
 	return Common::Rect(getInvArrowX(), kVerbY, getInvArrowX() + scrollW, kVerbY + kVerbH * kVerbRows);
 }
 
+bool ScummUI::isPointInInventoryStrip(const Common::Point &pos) const {
+	const Common::Rect left = getInvScrollLeftRect();
+	const Common::Rect right = getInvScrollRightRect();
+	const Common::Rect strip(left.left, left.top, right.right, left.bottom);
+	return strip.contains(pos);
+}
+
 Common::Rect ScummUI::getInvScrollRightRect() const {
 	const int scrollW = getScrollButtonWidth();
 	const int x = getInvArrowX() + scrollW + kInvCols * kInvItemW;
diff --git a/engines/macs2/scummui.h b/engines/macs2/scummui.h
index 694482f9212..7cfc2c439d3 100644
--- a/engines/macs2/scummui.h
+++ b/engines/macs2/scummui.h
@@ -85,6 +85,8 @@ private:
 	Common::Rect getInvItemRect(int index) const;
 	Common::Rect getInvScrollLeftRect() const;
 	Common::Rect getInvScrollRightRect() const;
+	/** True if pos hits the inventory scroll/item area of the strip. */
+	bool isPointInInventoryStrip(const Common::Point &pos) const;
 
 	View1 *_view;
 	int _activeVerbIndex;
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index b88d77fbc19..c5764a8ebce 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -171,7 +171,9 @@ bool View1::shouldShowScummVerbUI() const {
 		return false;
 	if (_isShowingTextBox || _isShowingDialoguePanel)
 		return false;
-	if (_uiPanelState == kUiPanelContainerInventory || _uiPanelState == kUiPanelSaveLoad)
+	// Keep the strip visible during container inventory so items can be taken
+	// into the protagonist inventory (Drop/Take is suppressed on that panel).
+	if (_uiPanelState == kUiPanelSaveLoad)
 		return false;
 	if (g_engine->_scriptExecutor->_cursorMode == Script::MouseMode::Disabled)
 		return false;


Commit: 151c31226ba2140a0efc28f4bf1e93e0f27b3d84
    https://github.com/scummvm/scummvm/commit/151c31226ba2140a0efc28f4bf1e93e0f27b3d84
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00

Commit Message:
MACS2: replaced opcode functions with script callbacks

Changed paths:
    engines/macs2/scriptexecutor.cpp
    engines/macs2/scriptexecutor.h


diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 4e203342a44..dcc818d8ed6 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -253,7 +253,7 @@ void ScriptExecutor::scriptSaveVariableHelper(uint32 value) {
 	setVariableValue(variableID, value);
 }
 
-void ScriptExecutor::scriptChangeAnimation() {
+OpcodeResult ScriptExecutor::scriptChangeAnimation() {
 	// scriptChangeAnimation (1008:b6be). Changes a background animation's
 	// current frame by calling advanceAnimFrame with a target position.
 	// Binary calls scriptReadValue() twice (16-bit reads); literals like 4097
@@ -263,22 +263,22 @@ void ScriptExecutor::scriptChangeAnimation() {
 	debugC(kDebugScript, "SCRIPT::changeAnimation(bgAnim=%u, targetFrame=%u)", backgroundAnimationIndex, targetFrameIndex);
 	if (backgroundAnimationIndex < 1) {
 		setScriptError(8);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (backgroundAnimationIndex > _engine->_backgroundAnimationsBlobs.size()) {
 		setScriptError(8);
-		return;
+		return OpcodeResult::Continue;
 	}
 	// Binary is 1-indexed, C++ array is 0-indexed
 	BackgroundAnimationBlob &blob = _engine->_backgroundAnimationsBlobs[backgroundAnimationIndex - 1];
 	if (blob._blob.empty()) {
 		setScriptError(8);
-		return;
+		return OpcodeResult::Continue;
 	}
 	const uint16 sequenceLength = BackgroundAnimationBlob::getAnimFrameCount(blob._blob);
 	if (targetFrameIndex > sequenceLength) {
 		setScriptError(9);
-		return;
+		return OpcodeResult::Continue;
 	}
 	BackgroundAnimationBlob::advanceAnimFrame(blob._blob, true, targetFrameIndex + 0x64);
 	// Match save/load restore: keep the requested sequence position in blob[+2]
@@ -291,6 +291,7 @@ void ScriptExecutor::scriptChangeAnimation() {
 	View1 *currentView = (View1 *)_engine->findView("View1");
 	if (currentView != nullptr)
 		currentView->presentFrame();
+	return OpcodeResult::Continue;
 }
 
 uint16 ScriptExecutor::getAreaAtPoint(uint16 x, uint16 y) {
@@ -647,13 +648,13 @@ void ScriptExecutor::step() {
 				shouldContinue = loadNextScript();
 			} else {
 				// Let the current script continue
-				ExecutionResult result = executeOpcodes();
+				OpcodeResult result = executeOpcodes();
 				if (hasScriptError()) {
 					recordScriptErrorPosition();
 					shouldContinue = false;
 					break;
 				}
-				if (result == ExecutionResult::WaitingForCallback) {
+				if (result == OpcodeResult::WaitForCallback) {
 					// We need to change our state as well now
 					_state = ExecutorState::WaitingForCallback;
 					if (!_debugPaused && !_waitingForUiClick) {
@@ -794,7 +795,7 @@ uint16 Script::ScriptExecutor::readUint16() {
 	return _stream->readUint16LE();
 }
 
-void Script::ScriptExecutor::scriptSetVar() {
+OpcodeResult Script::ScriptExecutor::scriptSetVar() {
 	// This writes to a script variable.
 	(void)readByte();
 	uint16 variableIndex = readUint16();
@@ -803,9 +804,10 @@ void Script::ScriptExecutor::scriptSetVar() {
 	scriptReadValuePair(var.a, var.b);
 	debugC(kDebugScript, "SCRIPT::setVar(variableIndex=%u, value1=%u, value2=%u)", variableIndex, var.a, var.b);
 	_variables[variableIndex] = var;
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptSetVarOr() {
+OpcodeResult Script::ScriptExecutor::scriptSetVarOr() {
 	// Padding/type byte (same as opcode 0x01) - read and discarded
 	(void)readByte();
 	uint16 variableIndex = readUint16();
@@ -821,9 +823,10 @@ void Script::ScriptExecutor::scriptSetVarOr() {
 	value3 |= 0x00;
 	debugC(kDebugScript, "SCRIPT::setVarOr(variableIndex=%u, value1=%u, value2=%u, value3=%u)", variableIndex, value1, value2, value3);
 	setVariableValue(variableIndex, value2, value3);
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptIfTrue() {
+OpcodeResult Script::ScriptExecutor::scriptIfTrue() {
 	uint16 res1;
 	uint16 res2;
 	scriptReadValuePair(res1, res2);
@@ -833,9 +836,10 @@ void Script::ScriptExecutor::scriptIfTrue() {
 		scriptSkipBlock();
 	}
 	_expectedEndLocation = _stream->pos();
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptIfFalse() {
+OpcodeResult Script::ScriptExecutor::scriptIfFalse() {
 	uint16 result1;
 	uint16 result2;
 	scriptReadValuePair(result1, result2);
@@ -845,9 +849,10 @@ void Script::ScriptExecutor::scriptIfFalse() {
 		scriptSkipBlock();
 	}
 	_expectedEndLocation = _stream->pos();
+	return OpcodeResult::Continue;
 }
 
-bool Script::ScriptExecutor::scriptCompare() {
+OpcodeResult Script::ScriptExecutor::scriptCompare() {
 	// Comparison opcode from executeOpcodes (1008:db56).
 	// Reads a comparison sub-opcode, two 32-bit values (v1:v2 and v3:v4),
 	// and skips the following block if the condition is NOT met.
@@ -896,10 +901,10 @@ bool Script::ScriptExecutor::scriptCompare() {
 		scriptSkipBlock();
 	}
 
-	return true;
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptIfInteraction() {
+OpcodeResult Script::ScriptExecutor::scriptIfInteraction() {
 	// "Use item on object" comparison from executeOpcodes (1008:db56).
 	// Reads sub-opcode (1=match, 2=NOT match), then the interacted pair
 	// and two comparison objects. Checks both orderings.
@@ -920,34 +925,39 @@ void Script::ScriptExecutor::scriptIfInteraction() {
 	if (!matched) {
 		scriptSkipBlock();
 	}
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptEndIf() {
+OpcodeResult Script::ScriptExecutor::scriptEndIf() {
 	// Opcode 0x07: no-op (confirmed: no handler in disassembly, falls through to loop).
 	// It has no specific case handling code in the original.
 	debugC(kDebugScript, "SCRIPT::endIf()");
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptElse() {
+OpcodeResult Script::ScriptExecutor::scriptElse() {
 	// This is some kind of skipping as well.
 	scriptSkipAlternate();
 	debugC(kDebugScript, "SCRIPT::else()");
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptNop09() {
+OpcodeResult Script::ScriptExecutor::scriptNop09() {
 	// ExecuteScript does not currently have a dedicated opcode 0x09 dispatch branch.
 	debugC(kDebugScript, "SCRIPT::nop09()");
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptPrintStringLeft() {
+OpcodeResult Script::ScriptExecutor::scriptPrintStringLeft() {
 	// l0037_DDE8:
 	debugC(kDebugScript, "SCRIPT::printStringLeft()");
 	scriptPrintString();
 	// Ends execution (confirmed: jumps to e3bd in disassembly).
 	endBuffering(_lastOpcodeTriggeredSkip);
+	return OpcodeResult::WaitForCallback;
 }
 
-void Script::ScriptExecutor::scriptMoveObject() {
+OpcodeResult Script::ScriptExecutor::scriptMoveObject() {
 	// scriptMoveObject (1008:aa83). Moves an object to a new scene/position.
 	// Handles render list updates for both source and destination scenes.
 	const uint32 objectID = scriptReadValue32() - 0x400;
@@ -958,12 +968,12 @@ void Script::ScriptExecutor::scriptMoveObject() {
 
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::FinishScript;
 	}
 	GameObject *object = GameObjects::getObjectByIndex(objectID);
 	if (object == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::FinishScript;
 	}
 	clearScriptError();
 
@@ -972,14 +982,14 @@ void Script::ScriptExecutor::scriptMoveObject() {
 		GameObject *oldParent = GameObjects::getObjectByIndex(object->_sceneIndex - 0x400);
 		if (oldParent == nullptr) {
 			setScriptError(2);
-			return;
+			return OpcodeResult::FinishScript;
 		}
 	}
 	if (sceneID > 0x400) {
 		GameObject *newParent = GameObjects::getObjectByIndex(sceneID - 0x400);
 		if (newParent == nullptr) {
 			setScriptError(0x19);
-			return;
+			return OpcodeResult::FinishScript;
 		}
 	}
 
@@ -1162,9 +1172,15 @@ void Script::ScriptExecutor::scriptMoveObject() {
 	}
 
 	currentView->rebuildCharacterLookupTable();
+	if (hasScriptError())
+		return OpcodeResult::FinishScript;
+	// Binary: after scriptMoveObject(), exits if position >= end.
+	if (_stream->pos() >= _stream->size())
+		return OpcodeResult::FinishScript;
+	return OpcodeResult::Continue;
 }
 
-ExecutionResult Script::ScriptExecutor::scriptChangeScene() {
+OpcodeResult Script::ScriptExecutor::scriptChangeScene() {
 	// Scene change from scriptChangeScene (1008:ad6e).
 	// Original behavior:
 	//   1. Read sceneID, transitionMode, transitionSpeed
@@ -1194,21 +1210,21 @@ ExecutionResult Script::ScriptExecutor::scriptChangeScene() {
 		endTimer();
 		endFrameWait();
 		endBuffering(_lastOpcodeTriggeredSkip);
-		return ExecutionResult::ScriptFinished;
+		return OpcodeResult::ReturnFinished;
 	}
 	if (transitionMode == 0 && (transitionSpeed == 0 || transitionSpeed > 0x40)) {
 		setScriptError(0x26);
 		endTimer();
 		endFrameWait();
 		endBuffering(_lastOpcodeTriggeredSkip);
-		return ExecutionResult::ScriptFinished;
+		return OpcodeResult::ReturnFinished;
 	}
 	if (transitionMode > 1) {
 		setScriptError(4);
 		endTimer();
 		endFrameWait();
 		endBuffering(_lastOpcodeTriggeredSkip);
-		return ExecutionResult::ScriptFinished;
+		return OpcodeResult::ReturnFinished;
 	}
 
 	// Binary scriptChangeScene (1008:ad6e): beginFrame, hourglass cursor, flipScreen
@@ -1273,11 +1289,11 @@ ExecutionResult Script::ScriptExecutor::scriptChangeScene() {
 	// re-entering scene 6 from the bar).
 	if (!isScriptWaitDeferred() && _state == ExecutorState::WaitingForCallback)
 		_state = ExecutorState::Executing;
-	return isScriptWaitDeferred() ? ExecutionResult::WaitingForCallback
-								  : ExecutionResult::ScriptFinished;
+	return isScriptWaitDeferred() ? OpcodeResult::WaitForCallback
+								  : OpcodeResult::ReturnFinished;
 }
 
-ExecutionResult Script::ScriptExecutor::scriptShowDialogue() {
+OpcodeResult Script::ScriptExecutor::scriptShowDialogue() {
 	// Show a dialogue option (1008:b2a8).
 	debugC(kDebugScript, "scriptShowDialogue: walkTarget=%d", _walkTargetObjectIndex);
 	const uint32 objectID = scriptReadValue32() - 0x400;
@@ -1292,29 +1308,29 @@ ExecutionResult Script::ScriptExecutor::scriptShowDialogue() {
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
 		endBuffering(_lastOpcodeTriggeredSkip);
-		return ExecutionResult::ScriptFinished;
+		return OpcodeResult::FinishScript;
 	}
 	GameObject *speaker = GameObjects::getObjectByIndex(objectID);
 	if (speaker == nullptr) {
 		setScriptError(0x19);
 		endBuffering(_lastOpcodeTriggeredSkip);
-		return ExecutionResult::ScriptFinished;
+		return OpcodeResult::FinishScript;
 	}
 	if (speaker->_dataOffset == 0) {
 		setScriptError(2);
 		endBuffering(_lastOpcodeTriggeredSkip);
-		return ExecutionResult::ScriptFinished;
+		return OpcodeResult::FinishScript;
 	}
 	if (side > 1) {
 		setScriptError(5);
 		endBuffering(_lastOpcodeTriggeredSkip);
-		return ExecutionResult::ScriptFinished;
+		return OpcodeResult::FinishScript;
 	}
 	// Binary: bSlotLoaded for portrait slots 0x12 and 0x13 (runtime+0x153, +0x163).
 	if (speaker->_blobs.size() < 19 || speaker->_blobs[17].empty() || speaker->_blobs[18].empty()) {
 		setScriptError(6);
 		endBuffering(_lastOpcodeTriggeredSkip);
-		return ExecutionResult::ScriptFinished;
+		return OpcodeResult::FinishScript;
 	}
 
 	View1 *currentView = (View1 *)_engine->findView("View1");
@@ -1347,10 +1363,10 @@ ExecutionResult Script::ScriptExecutor::scriptShowDialogue() {
 	endTimer();
 	endFrameWait();
 	endBuffering(_lastOpcodeTriggeredSkip);
-	return ExecutionResult::WaitingForCallback;
+	return OpcodeResult::WaitForCallback;
 }
 
-void Script::ScriptExecutor::scriptWalkToPosition() {
+OpcodeResult Script::ScriptExecutor::scriptWalkToPosition() {
 	// Binary scriptWalkToPosition (1008:b843):
 	// Sets up runtime movement state. Does NOT block - walkAlongPath handles
 	// actual movement per-frame from the game tick.
@@ -1362,16 +1378,16 @@ void Script::ScriptExecutor::scriptWalkToPosition() {
 	clearScriptError();
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	GameObject *object = GameObjects::getObjectByIndex(objectID);
 	if (object == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (object->_dataOffset == 0) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	View1 *currentView = (View1 *)_engine->findView("View1");
@@ -1412,9 +1428,10 @@ void Script::ScriptExecutor::scriptWalkToPosition() {
 	c->_motionTargetVerticalOffset = object->_verticalOffsetScale;
 
 	saveWalkRuntime(c, object);
+	return OpcodeResult::Continue;
 }
 
-ExecutionResult Script::ScriptExecutor::scriptWaitForWalk() {
+OpcodeResult Script::ScriptExecutor::scriptWaitForWalk() {
 	// Wait for walk completion from executeOpcodes (1008:db56).
 	// Original: validates object, checks runtime data exists, checks frozen flag,
 	// sets g_wWalkTargetObjectIndex, hides cursor, returns to gameTick.
@@ -1424,25 +1441,25 @@ ExecutionResult Script::ScriptExecutor::scriptWaitForWalk() {
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
 		endBuffering(_lastOpcodeTriggeredSkip);
-		return ExecutionResult::ScriptFinished;
+		return OpcodeResult::ReturnFinished;
 	}
 	GameObject *walkObject = GameObjects::getObjectByIndex(objectID);
 	if (walkObject == nullptr) {
 		setScriptError(0x19);
 		endBuffering(_lastOpcodeTriggeredSkip);
-		return ExecutionResult::ScriptFinished;
+		return OpcodeResult::ReturnFinished;
 	}
 	// Binary (1008:db56 opcode 0x11): object+0x0A runtime must exist; no on-screen Character required.
 	if (walkObject->_dataOffset == 0) {
 		setScriptError(2);
 		endBuffering(_lastOpcodeTriggeredSkip);
-		return ExecutionResult::ScriptFinished;
+		return OpcodeResult::ReturnFinished;
 	}
 	// Original checks runtime+0x231 (frozen/attached flag) -> error 0x1F
 	if (walkObject->_hasBoundsAttachment) {
 		setScriptError(0x1F);
 		endBuffering(_lastOpcodeTriggeredSkip);
-		return ExecutionResult::ScriptFinished;
+		return OpcodeResult::ReturnFinished;
 	}
 	_walkTargetObjectIndex = objectID;
 	endTimer();
@@ -1451,12 +1468,12 @@ ExecutionResult Script::ScriptExecutor::scriptWaitForWalk() {
 	enterBlockingWaitCursor();
 	// Binary: opcode 0x11 exits executeOpcodes with g_wScriptIsExecuting still true.
 	// runScriptExecutor returns immediately (no object iteration, no cursor restore).
-	// ScummVM equivalent: return WaitingForCallback so step() pauses execution.
+	// ScummVM equivalent: return WaitForCallback so step() pauses execution.
 	// step() handles cursor save/set to Disabled automatically.
-	return ExecutionResult::WaitingForCallback;
+	return OpcodeResult::WaitForCallback;
 }
 
-void Script::ScriptExecutor::scriptSkipUntil14() {
+OpcodeResult Script::ScriptExecutor::scriptSkipUntil14() {
 	// scriptSkipUntil14 @ 1008:a439: read tag, scan from script start for opcode 0x14
 	// with matching tag, then continue execution after that label (not at 0x13 block end).
 	const uint16 tag = readUint16();
@@ -1469,7 +1486,7 @@ void Script::ScriptExecutor::scriptSkipUntil14() {
 			uint16 tag14 = readUint16();
 			if (tag14 == tag) {
 				_expectedEndLocation = _stream->pos();
-				return;
+				return OpcodeResult::Continue;
 			}
 		} else {
 			_stream->seek(length, SEEK_CUR);
@@ -1477,23 +1494,26 @@ void Script::ScriptExecutor::scriptSkipUntil14() {
 	}
 	setScriptError(0x20);
 	_expectedEndLocation = _stream->pos();
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptSkipWord() {
+OpcodeResult Script::ScriptExecutor::scriptSkipWord() {
 	// If we reach opcode 14 regularly, just discard the payload and continue.
 	const uint16 val = readUint16();
 	debugC(kDebugScript, "SCRIPT::skipWord(%u)", val);
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptClearDialogueChoices() {
+OpcodeResult Script::ScriptExecutor::scriptClearDialogueChoices() {
 	// Mark that we are gathering strings for setting up a dialogue choice.
 	_dialogueChoices.clear();
 	_chosenDialogueOption = 0;
 	_dialogueChoiceScriptIndices.clear();
 	debugC(kDebugScript, "SCRIPT::clearDialogueChoices()");
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptAddDialogueChoice() {
+OpcodeResult Script::ScriptExecutor::scriptAddDialogueChoice() {
 	// Add a dialogue choice.
 	const uint16 index = scriptReadValue16();
 	const uint16 offset = readUint16();
@@ -1502,7 +1522,7 @@ void Script::ScriptExecutor::scriptAddDialogueChoice() {
 	// Binary (1008:c75a): if choice count already 16, set error 0x0E and discard entry.
 	if (_dialogueChoices.size() >= 16) {
 		setScriptError(0x0E);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	// Binary stores this index at scene+0x5351+choiceIndex*6 (first field of each 6-byte entry).
@@ -1528,9 +1548,10 @@ void Script::ScriptExecutor::scriptAddDialogueChoice() {
 		   "Opcode 16 choice text: index=%u textOffset=%u numLines=%u scriptObject=%u text=\"%s\"",
 		   index, offset, numLines, _executingScriptObjectId, joinDebugStrings(lines).c_str());
 	_dialogueChoices.push_back(lines);
+	return OpcodeResult::Continue;
 }
 
-ExecutionResult Script::ScriptExecutor::scriptShowDialogueChoice() {
+OpcodeResult Script::ScriptExecutor::scriptShowDialogueChoice() {
 	// Finish the dialogue choice.
 	const uint32 x = scriptReadValue32();
 	const uint32 y = scriptReadValue32();
@@ -1554,23 +1575,23 @@ ExecutionResult Script::ScriptExecutor::scriptShowDialogueChoice() {
 	// NOTE: EndTimer prevents race conditions from overlapping waits
 	endTimer();
 	endBuffering(_lastOpcodeTriggeredSkip);
-	return ExecutionResult::WaitingForCallback;
+	return OpcodeResult::WaitForCallback;
 }
 
-ExecutionResult Script::ScriptExecutor::scriptDismissPanel() {
+OpcodeResult Script::ScriptExecutor::scriptDismissPanel() {
 	// Set the stream to the end and let the calling code figure out that we are done
 	// for this run.
 	debugLogActorWalkState("dismissPanel (opcode 0x18)");
 	_stream->seek(_stream->size(), SEEK_SET);
 	endBuffering(_lastOpcodeTriggeredSkip);
-	return ExecutionResult::ScriptFinished;
+	return OpcodeResult::ReturnFinished;
 }
 
-void Script::ScriptExecutor::scriptWalkToAndPickup() {
+OpcodeResult Script::ScriptExecutor::scriptWalkToAndPickup() {
 	// Walk to and pick up an object (1008:c475).
 	// Binary returns immediately if pickup already in progress, without reading params.
 	if (_pickupInProgress) {
-		return;
+		return OpcodeResult::FinishScript;
 	}
 
 	const uint32 actorIndex = scriptReadValue32() - 0x400;
@@ -1585,35 +1606,35 @@ void Script::ScriptExecutor::scriptWalkToAndPickup() {
 
 	if (actorIndex < 1 || actorIndex > 0x200 || objectIndex < 1 || objectIndex > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::FinishScript;
 	}
 
 	GameObject *actorObject = GameObjects::getObjectByIndex(actorIndex);
 	GameObject *targetObject = GameObjects::getObjectByIndex(objectIndex);
 	if (actorObject == nullptr || targetObject == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::FinishScript;
 	}
 
 	if (targetObject->_dataOffset != 0) {
 		_engine->loadObjectData(targetObject);
 		if (hasScriptError())
-			return;
+			return OpcodeResult::FinishScript;
 	}
 
 	if (actorObject->_dataOffset == 0) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::FinishScript;
 	}
 
 	if (actorIndex == objectIndex || targetObject->_sceneIndex == actorObject->_index) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::FinishScript;
 	}
 
 	if (actorObject->_hasBoundsAttachment) {
 		setScriptError(0x1F);
-		return;
+		return OpcodeResult::FinishScript;
 	}
 
 	View1 *currentView = (View1 *)_engine->findView("View1");
@@ -1635,9 +1656,10 @@ void Script::ScriptExecutor::scriptWalkToAndPickup() {
 	if (!hasScriptError()) {
 		_walkTargetObjectIndex = actorIndex;
 	}
+	return OpcodeResult::FinishScript;
 }
 
-bool Script::ScriptExecutor::scriptSetPickupFrames() {
+OpcodeResult Script::ScriptExecutor::scriptSetPickupFrames() {
 	const int32 objectID = (int32)scriptReadValue32() - 0x400;
 	const uint16 frameStart = scriptReadValue16();
 	const uint16 frameEnd = scriptReadValue16();
@@ -1646,25 +1668,25 @@ bool Script::ScriptExecutor::scriptSetPickupFrames() {
 	clearScriptError();
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
-		return true;
+		return OpcodeResult::Continue;
 	}
 
 	GameObject *object = GameObjects::getObjectByIndex((uint16)objectID);
 	if (object == nullptr) {
 		setScriptError(0x19);
-		return true;
+		return OpcodeResult::Continue;
 	}
 	if (object->_dataOffset == 0) {
 		setScriptError(2);
-		return true;
+		return OpcodeResult::Continue;
 	}
 
 	object->_pickupFrameStart = frameStart;
 	object->_pickupFrameEnd = frameEnd;
-	return true;
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptSetupObject() {
+OpcodeResult Script::ScriptExecutor::scriptSetupObject() {
 	const int32 objectID = (int32)scriptReadValue32() - 0x400;
 	const uint16 slotID = scriptReadValue16();
 	const uint16 value = scriptReadValue16();
@@ -1673,47 +1695,50 @@ void Script::ScriptExecutor::scriptSetupObject() {
 	clearScriptError();
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	GameObject *object = GameObjects::getObjectByIndex((uint16)objectID);
 	if (object == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (object->_dataOffset == 0) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	if (slotID < 1 || slotID > _engine->maxAnimSlots()) {
 		setScriptError(0x10);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (!object->isAnimSlotLoaded(slotID)) {
 		setScriptError(0x10);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	// Binary writes to runtime+slot*0x10+0x30 which is slot_base+0x0C = wAnimSpeed.
 	if ((uint)(slotID - 1) < object->_blobWalkSpeeds.size()) {
 		object->_blobWalkSpeeds[slotID - 1] = value;
 	}
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptSetSkippable() {
+OpcodeResult Script::ScriptExecutor::scriptSetSkippable() {
 	// Sets g_wScriptSkippable [102Ah] = 1.
 	_scriptSkippable = true;
 	debugC(kDebugScript, "SCRIPT::setSkippable()");
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptClearSkippable() {
+OpcodeResult Script::ScriptExecutor::scriptClearSkippable() {
 	// Sets g_wScriptSkippable [102Ah] = 0.
 	_scriptSkippable = false;
 	debugC(kDebugScript, "SCRIPT::clearSkippable()");
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptPlayAnimation() {
+OpcodeResult Script::ScriptExecutor::scriptPlayAnimation() {
 	// scriptPlayAnimation (1008:bd58).
 	const uint32 objectID = scriptReadValue32() - 0x400;
 	const uint32 slotID = scriptReadValue16();
@@ -1723,36 +1748,36 @@ void Script::ScriptExecutor::scriptPlayAnimation() {
 	clearScriptError();
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	GameObject *gameObject = GameObjects::getObjectByIndex(objectID);
 	if (gameObject == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (gameObject->_dataOffset == 0) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (slotID < 1 || slotID > _engine->maxAnimSlots()) {
 		setScriptError(0x10);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (!gameObject->isAnimSlotLoaded((uint16)slotID)) {
 		setScriptError(0x10);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	Common::Array<uint8> *blob = gameObject->getAnimSlotBlob((uint16)slotID);
 	if (blob == nullptr || blob->empty()) {
 		setScriptError(0x10);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	AnimBlobView view(*blob);
 	if (!view.isValid()) {
 		setScriptError(8);
-		return;
+		return OpcodeResult::Continue;
 	}
 	// Binary getAnimFrameCount (1010:168c) returns sequence length (blob+0x0A+1),
 	// not the pixel frame count word at frameDataOffset. advanceAnimFrame uses
@@ -1760,13 +1785,14 @@ void Script::ScriptExecutor::scriptPlayAnimation() {
 	const uint16 seqLength = view.sequenceLength();
 	if (frameOffset < 0 || (uint16)frameOffset > seqLength) {
 		setScriptError(0x12);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	BackgroundAnimationBlob::advanceAnimFrame(*blob, true, (uint16)frameOffset + 0x64);
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptTestPathfinding() {
+OpcodeResult Script::ScriptExecutor::scriptTestPathfinding() {
 	const uint32 objectID = scriptReadValue32() - 0x400;
 	const uint32 x = scriptReadValue32();
 	const uint32 y = scriptReadValue32();
@@ -1776,18 +1802,19 @@ void Script::ScriptExecutor::scriptTestPathfinding() {
 	_pathWalkableResult = false;
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	GameObject *object = GameObjects::getObjectByIndex(objectID);
 	if (object == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (object->_dataOffset == 0) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	_pathWalkableResult = _engine->isPathWalkable(y, x, object->_position.y, object->_position.x);
+	return OpcodeResult::Continue;
 }
 
 Character *Script::ScriptExecutor::getOrCreateCharacter(uint16 objectID) {
@@ -1929,7 +1956,7 @@ void Script::ScriptExecutor::restoreOpenInventoryScriptContext() {
 	_isRunningScript = true;
 }
 
-void Script::ScriptExecutor::scriptSetYOffset() {
+OpcodeResult Script::ScriptExecutor::scriptSetYOffset() {
 	// scriptSetYOffset (1008:c047). Sets object field +8 (vertical offset)
 	// AND mirrors it into runtime field +0x21D (motion target).
 	const int32 objectID = (int32)scriptReadValue32() - 0x400;
@@ -1939,17 +1966,17 @@ void Script::ScriptExecutor::scriptSetYOffset() {
 	clearScriptError();
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	GameObject *object = GameObjects::getObjectByIndex((uint16)objectID);
 	if (object == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (object->_dataOffset == 0) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	object->_verticalOffsetScale = offset;
@@ -1958,9 +1985,10 @@ void Script::ScriptExecutor::scriptSetYOffset() {
 	}
 	object->_storedWalkRuntime.valid = true;
 	object->_storedWalkRuntime.motionTargetVerticalOffset = offset;
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptSetMotion() {
+OpcodeResult Script::ScriptExecutor::scriptSetMotion() {
 	const int32 objectID = (int32)scriptReadValue32() - 0x400;
 	const uint16 targetVerticalOffset = scriptReadValue16();
 	const uint16 verticalOffsetDelta = scriptReadValue16();
@@ -1971,25 +1999,26 @@ void Script::ScriptExecutor::scriptSetMotion() {
 	clearScriptError();
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	GameObject *object = GameObjects::getObjectByIndex((uint16)objectID);
 	if (object == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (object->_dataOffset == 0) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	Character *character = getOrCreateCharacter((uint16)objectID);
 	seedMotionState(object, character, targetVerticalOffset, verticalOffsetDelta, motionDistance);
 	debugLogActorWalkState("after setMotion");
+	return OpcodeResult::Continue;
 }
 
-bool Script::ScriptExecutor::scriptSetOrientation() {
+OpcodeResult Script::ScriptExecutor::scriptSetOrientation() {
 	const int32 objectID = (int32)scriptReadValue32() - 0x400;
 	const uint16 animIndex = scriptReadValue16();
 	debugC(kDebugScript, "SCRIPT::setOrientation(objectID=%d, animIndex=%u)", objectID, animIndex);
@@ -1997,24 +2026,24 @@ bool Script::ScriptExecutor::scriptSetOrientation() {
 	clearScriptError();
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
-		return true;
+		return OpcodeResult::Continue;
 	}
 
 	GameObject *object = GameObjects::getObjectByIndex((uint16)objectID);
 	if (object == nullptr) {
 		setScriptError(0x19);
-		return true;
+		return OpcodeResult::Continue;
 	}
 	if (animIndex < 9 || animIndex > 0x10) {
 		setScriptError(0x14);
-		return true;
+		return OpcodeResult::Continue;
 	}
 
 	object->_orientation = animIndex;
-	return true;
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptMoveToPosition() {
+OpcodeResult Script::ScriptExecutor::scriptMoveToPosition() {
 	// Opcode 0x23 scriptMoveToPosition (1008:bafc): seeds runtime walk state directly;
 	// does not pathfind or use time-based lerp.
 	const int32 objectID = (int32)scriptReadValue32() - 0x400;
@@ -2026,17 +2055,17 @@ void Script::ScriptExecutor::scriptMoveToPosition() {
 	clearScriptError();
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	GameObject *object = GameObjects::getObjectByIndex((uint16)objectID);
 	if (object == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (object->_dataOffset == 0) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	const Common::Point target(x, y);
@@ -2044,15 +2073,16 @@ void Script::ScriptExecutor::scriptMoveToPosition() {
 	if (!_engine->isPathWalkable(y, x, object->_position.y, object->_position.x) &&
 		Macs2Engine::isWalkabilityWalkable(_engine->getWalkabilityAt(target))) {
 		setScriptError(0x15);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	View1 *currentView = (View1 *)_engine->findView("View1");
 	Character *c = currentView ? getOrCreateCharacter((uint16)objectID) : nullptr;
 	seedMoveToPositionState(object, c, target, targetVerticalOffset);
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptAddValues() {
+OpcodeResult Script::ScriptExecutor::scriptAddValues() {
 	// Adds two values read and saves them to a script variable.
 	// ;; fn0037_C7E6: 0037:C7E6
 	const uint32 a = scriptReadValue32();
@@ -2067,9 +2097,10 @@ void Script::ScriptExecutor::scriptAddValues() {
 	scriptSaveVariableHelper(result);
 	// Skip forward across the second 9F4D read's data.
 	_stream->seek(3, SEEK_CUR);
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptSubValues() {
+OpcodeResult Script::ScriptExecutor::scriptSubValues() {
 	// Subtracts two values read and saves them to a script variable.
 	// ;; fn0037_C82E: 0037:C82E
 	const uint32 a = scriptReadValue32();
@@ -2080,9 +2111,10 @@ void Script::ScriptExecutor::scriptSubValues() {
 	_stream->seek(-6, SEEK_CUR);
 	scriptSaveVariableHelper(result);
 	_stream->seek(3, SEEK_CUR);
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptLoadSpecialAnim() {
+OpcodeResult Script::ScriptExecutor::scriptLoadSpecialAnim() {
 	// This one loads a special animation set into the overload slot (1008:c991).
 	const uint32 id = scriptReadValue32() - 0x400;
 	const uint16 shouldMirror = scriptReadValue16();
@@ -2092,16 +2124,16 @@ void Script::ScriptExecutor::scriptLoadSpecialAnim() {
 	clearScriptError();
 	if (id < 1 || id > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	GameObject *object = GameObjects::getObjectByIndex(id);
 	if (object == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (object->_dataOffset == 0) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	const Common::Array<uint8> &blob = Scenes::instance().readSpecialAnimBlob(animationID, g_engine->_fileStream);
@@ -2113,9 +2145,10 @@ void Script::ScriptExecutor::scriptLoadSpecialAnim() {
 	while (object->_blobs.size() <= 20)
 		object->_blobs.push_back(Common::Array<uint8>());
 	object->_blobs[20] = object->_overloadAnimation;
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptSetDirection() {
+OpcodeResult Script::ScriptExecutor::scriptSetDirection() {
 	// scriptSetDirection (1008:c858). Writes to runtime field +0x22D.
 	const uint32 characterID = scriptReadValue32() - 0x400;
 	const uint16 value = scriptReadValue16();
@@ -2124,21 +2157,22 @@ void Script::ScriptExecutor::scriptSetDirection() {
 	clearScriptError();
 	if (characterID < 1 || characterID > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	GameObject *object = GameObjects::getObjectByIndex(characterID);
 	if (object == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (object->_dataOffset == 0) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	object->_overloadAnimTriggerDirection = value;
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptStopAnimation() {
+OpcodeResult Script::ScriptExecutor::scriptStopAnimation() {
 	// scriptStopAnimation (1008:c8e4).
 	const uint32 characterID = scriptReadValue32() - 0x400;
 	debugC(kDebugScript, "SCRIPT::stopAnimation(characterID=%u)", characterID);
@@ -2146,53 +2180,62 @@ void Script::ScriptExecutor::scriptStopAnimation() {
 	clearScriptError();
 	if (characterID < 1 || characterID > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	GameObject *obj = GameObjects::getObjectByIndex(characterID);
 	if (obj == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (obj->_dataOffset == 0) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	obj->_overloadAnimTriggerDirection = 0x7FFF;
 	obj->_useOverloadAnimation = false;
 	obj->_overloadAnimation.clear();
 	if (obj->_blobs.size() > 20)
 		obj->_blobs[20].clear();
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptOpenInventory() {
+OpcodeResult Script::ScriptExecutor::scriptOpenInventory() {
 	const uint32 objectID = scriptReadValue32() - 0x400;
 	debugC(kDebugScript, "SCRIPT::openInventory(objectID=%u)", objectID);
 
 	clearScriptError();
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
-		return;
-	}
-	GameObject *inventorySource = GameObjects::getObjectByIndex(objectID);
-	if (inventorySource == nullptr) {
-		setScriptError(0x19);
-		return;
+	} else {
+		GameObject *inventorySource = GameObjects::getObjectByIndex(objectID);
+		if (inventorySource == nullptr) {
+			setScriptError(0x19);
+		} else {
+			View1 *currentView = (View1 *)_engine->findView("View1");
+			_savedExternalInventoryMouseMode = _cursorMode == MouseMode::UseInventory ? MouseMode::Use : _cursorMode;
+			_hasPendingExternalInventoryResume = true;
+			_externalInventorySourceObjectID = objectID;
+			saveOpenInventoryScriptContext();
+			// Save script click state (original saves at 0xf94-0xf9a equivalents)
+			_savedScriptClickFlag = _scriptClickFlag;
+			_savedScriptClickX = _scriptClickX;
+			_savedScriptClickY = _scriptClickY;
+			_savedScriptClickResult = _scriptClickResult;
+			currentView->openInventory(inventorySource);
+		}
 	}
 
-	View1 *currentView = (View1 *)_engine->findView("View1");
-	_savedExternalInventoryMouseMode = _cursorMode == MouseMode::UseInventory ? MouseMode::Use : _cursorMode;
-	_hasPendingExternalInventoryResume = true;
-	_externalInventorySourceObjectID = objectID;
-	saveOpenInventoryScriptContext();
-	// Save script click state (original saves at 0xf94-0xf9a equivalents)
-	_savedScriptClickFlag = _scriptClickFlag;
-	_savedScriptClickX = _scriptClickX;
-	_savedScriptClickY = _scriptClickY;
-	_savedScriptClickResult = _scriptClickResult;
-	currentView->openInventory(inventorySource);
-}
-
-void Script::ScriptExecutor::scriptLoadObjectAnim() {
+	// Binary executeOpcodes @ 1008:e0f9 (full + demo):
+	//   after openInventory: set script position to end, object id 0x201, finish.
+	// loadNextScript() honors ScriptObjectId > 0x200 and will not start/continue
+	// the object-script walk (including when 0x29 ran from the scene script).
+	_stream->seek(_stream->size(), SEEK_SET);
+	_executingObjectIndex = 0x201;
+	_executingScriptObjectId = 0x201;
+	return OpcodeResult::FinishScript;
+}
+
+OpcodeResult Script::ScriptExecutor::scriptLoadObjectAnim() {
 	const uint16 objectID = scriptReadValue16() - 0x400;
 	const uint16 slotID = scriptReadValue16();
 	const bool shouldMirror = scriptReadValue16() != 0;
@@ -2203,47 +2246,48 @@ void Script::ScriptExecutor::scriptLoadObjectAnim() {
 	clearScriptError();
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	GameObject *object = GameObjects::getObjectByIndex(objectID);
 	if (object == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (object->_dataOffset == 0) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (slotID < 1 || slotID > _engine->maxAnimSlots()) {
 		setScriptError(0x13);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	g_engine->loadAnimationFromSceneData(objectID, slotID, arrayIndex, shouldMirror, _executingScriptObjectId);
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptCheckObjectData() {
+OpcodeResult Script::ScriptExecutor::scriptCheckObjectData() {
 	const uint16 objectID = scriptReadValue16() - 0x400;
 	debugC(kDebugScript, "SCRIPT::checkObjectData(objectID=%u)", objectID);
 
 	clearScriptError();
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	GameObject *object = GameObjects::getObjectByIndex(objectID);
 	if (object == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (object->_dataOffset == 0) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	View1 *currentView = (View1 *)_engine->findView("View1");
 	if (currentView == nullptr)
-		return;
+		return OpcodeResult::Continue;
 
 	const Common::Point posBefore = object->_position;
 	const bool snapBefore = object->_snapToTarget;
@@ -2259,7 +2303,7 @@ void Script::ScriptExecutor::scriptCheckObjectData() {
 	// Binary: sortObjectsByDepth (1008:0d79) then loadObjectData (1008:08ec).
 	_engine->sortObjectsByDepth(objectID);
 	if (!_engine->loadObjectData(object))
-		return;
+		return OpcodeResult::Continue;
 
 	debugC(kDebugPath,
 		   "WALK::checkObjectData obj=%u pos before=(%d,%d) after=(%d,%d) snap %d->%d (loadObjectData clears runtime)",
@@ -2277,9 +2321,10 @@ void Script::ScriptExecutor::scriptCheckObjectData() {
 		currentView->_characters.push_back(character);
 	}
 	currentView->rebuildCharacterLookupTable();
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptCheckInventory() {
+OpcodeResult Script::ScriptExecutor::scriptCheckInventory() {
 	const uint16 objectID = scriptReadValue16() - 0x400;
 	const uint16 parentID = scriptReadValue16();
 	debugC(kDebugScript, "SCRIPT::checkInventory(objectID=%u, parentID=%u)", objectID, parentID);
@@ -2287,17 +2332,18 @@ void Script::ScriptExecutor::scriptCheckInventory() {
 	clearScriptError();
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	const GameObject *object = GameObjects::getObjectByIndex(objectID);
 	if (object == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::Continue;
 	}
 	_inventoryCheckResult = object->_sceneIndex == parentID;
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptSetSnapToTarget() {
+OpcodeResult Script::ScriptExecutor::scriptSetSnapToTarget() {
 	const uint16 objectID = scriptReadValue16() - 0x400;
 	const bool enabled = scriptReadValue16() != 0;
 	debugC(kDebugScript, "SCRIPT::setSnapToTarget(objectID=%u, enabled=%u)", objectID, enabled);
@@ -2305,21 +2351,22 @@ void Script::ScriptExecutor::scriptSetSnapToTarget() {
 	clearScriptError();
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	GameObject *object = GameObjects::getObjectByIndex(objectID);
 	if (object == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (object->_dataOffset == 0) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	object->_snapToTarget = enabled;
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptTestObjectAnimFrame() {
+OpcodeResult Script::ScriptExecutor::scriptTestObjectAnimFrame() {
 	// scriptTestObjectAnimFrame (1008:be91).
 	uint32 objectID = scriptReadValue32() - 0x400;
 	uint16 slotID = scriptReadValue16();
@@ -2332,46 +2379,48 @@ void Script::ScriptExecutor::scriptTestObjectAnimFrame() {
 
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	GameObject *object = GameObjects::getObjectByIndex(objectID);
 	if (object == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (object->_dataOffset == 0) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (slotID < 1 || slotID > _engine->maxAnimSlots()) {
 		setScriptError(0x10);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	const Common::Array<uint8> *blob = object->getAnimSlotBlob(slotID);
 	if (blob == nullptr || blob->empty()) {
 		setScriptError(0x10);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	AnimBlobView view(*blob);
 	if (!view.isValid()) {
 		setScriptError(8);
-		return;
+		return OpcodeResult::Continue;
 	}
 	const uint16 seqPos = view.sequencePosition();
 	_animBlobRangeTestResult = seqPos >= minFrame && seqPos <= maxFrame;
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptPrintStringRight() {
+OpcodeResult Script::ScriptExecutor::scriptPrintStringRight() {
 	// Opcode 0x30: scriptPrintString with flag=1 (vs opcode 0x0A with flag=0)
 	// which changes behaviour in the function.
 	debugC(kDebugScript, "SCRIPT::printStringRight()");
 	scriptPrintString(true);
 	endBuffering(_lastOpcodeTriggeredSkip);
+	return OpcodeResult::WaitForCallback;
 }
 
-void Script::ScriptExecutor::scriptSetPaletteDarkness() {
+OpcodeResult Script::ScriptExecutor::scriptSetPaletteDarkness() {
 	// Binary (1008:ce0b): clamps value to 0..100, writes to sceneData+0x5205
 	// (darkenPercent), then calls applyPaletteInterpolation to update displayed palette.
 	int16 darkenPercent = (int16)scriptReadValue16();
@@ -2385,9 +2434,10 @@ void Script::ScriptExecutor::scriptSetPaletteDarkness() {
 	View1 *view = (View1 *)g_engine->findView("View1");
 	if (view)
 		view->_paletteDirty = true;
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptSetObjectShading() {
+OpcodeResult Script::ScriptExecutor::scriptSetObjectShading() {
 	// Opcode 0x32 (1008:b9ba): runtime+0x185 bHasShading (NOT clickable - misnamed in script docs).
 	const uint16 objectID = scriptReadValue16() - 0x400;
 	const uint16 hasShading = scriptReadValue16();
@@ -2396,21 +2446,22 @@ void Script::ScriptExecutor::scriptSetObjectShading() {
 	clearScriptError();
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	GameObject *object = GameObjects::getObjectByIndex(objectID);
 	if (object == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (object->_dataOffset == 0) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	object->_hasShading = hasShading != 0;
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptSetObjectScaling() {
+OpcodeResult Script::ScriptExecutor::scriptSetObjectScaling() {
 	// Opcode 0x33 (1008:ba5b): runtime+0x186 bHasScaling (NOT visible - misnamed in script docs).
 	const uint16 objectID = scriptReadValue16() - 0x400;
 	const uint16 hasScaling = scriptReadValue16();
@@ -2419,21 +2470,22 @@ void Script::ScriptExecutor::scriptSetObjectScaling() {
 	clearScriptError();
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	GameObject *object = GameObjects::getObjectByIndex(objectID);
 	if (object == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (object->_dataOffset == 0) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	object->_hasScaling = hasScaling != 0;
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptSetHotspotOverride() {
+OpcodeResult Script::ScriptExecutor::scriptSetHotspotOverride() {
 	// Opcode 0x34 scriptSetHotspotOverride (1008:ce40): scene+0x5BD1 hotspot remap table.
 	const uint16 v1 = scriptReadValue16() - 0x800;
 	const uint16 v2 = scriptReadValue16() - 0x800;
@@ -2443,16 +2495,17 @@ void Script::ScriptExecutor::scriptSetHotspotOverride() {
 	const uint16 maxHotspot = _engine->maxHotspots();
 	if (v1 < 1 || v1 > maxHotspot || v2 < 1 || v2 > maxHotspot) {
 		setScriptError(0x1e);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (v1 == v2) {
 		g_engine->_hotspotOverrides[v1] = 0xFFFF;
 	} else {
 		g_engine->_hotspotOverrides[v1] = v2;
 	}
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptSetObjectBounds() {
+OpcodeResult Script::ScriptExecutor::scriptSetObjectBounds() {
 	// Opcode 0x35 scriptSetObjectBounds (1008:c19f): runtime+0x231..+0x238 attachment.
 	const uint16 objectID = scriptReadValue16() - 0x400;
 	const uint16 otherObjectID = scriptReadValue16() - 0x400;
@@ -2464,17 +2517,17 @@ void Script::ScriptExecutor::scriptSetObjectBounds() {
 	clearScriptError();
 	if (objectID < 1 || objectID > 0x200 || otherObjectID < 1 || otherObjectID > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	GameObject *object = GameObjects::getObjectByIndex(objectID);
 	GameObject *otherObject = GameObjects::getObjectByIndex(otherObjectID);
 	if (object == nullptr || otherObject == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (object->_dataOffset == 0 || otherObject->_dataOffset == 0) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	if (objectID == otherObjectID) {
@@ -2490,16 +2543,17 @@ void Script::ScriptExecutor::scriptSetObjectBounds() {
 		object->_boundsAttachmentValue2 = value2;
 		object->_boundsAttachmentValue3 = value3;
 	}
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptDismissAllPanels() {
+OpcodeResult Script::ScriptExecutor::scriptDismissAllPanels() {
 	// Opcode 0x36 -> scriptDismissPanel (1008:d6dd): restore pending UI background only.
 	View1 *currentView = (View1 *)_engine->findView("View1");
 	if (currentView == nullptr)
-		return;
+		return OpcodeResult::Continue;
 	if (currentView->_pendingPanelRequest == View1::kPanelRequestNone ||
 		!currentView->_uiBackgroundRestorePending) {
-		return;
+		return OpcodeResult::Continue;
 	}
 	debugC(kDebugScript, "SCRIPT::dismissPanel()");
 	currentView->redraw();
@@ -2509,9 +2563,10 @@ void Script::ScriptExecutor::scriptDismissAllPanels() {
 	currentView->_isDialogueChoiceInputActive = false;
 	currentView->_activeInventoryItem = nullptr;
 	currentView->clearClickedButtonIndex();
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptResetToSceneScript() {
+OpcodeResult Script::ScriptExecutor::scriptResetToSceneScript() {
 	// scriptResetToSceneScript (1008:ad3e). Resets script execution
 	// context back to the current scene script at position 0.
 	debugC(kDebugScript, "SCRIPT::resetToSceneScript()");
@@ -2520,9 +2575,10 @@ void Script::ScriptExecutor::scriptResetToSceneScript() {
 	_scriptExecutionState = ScriptExecutionState::ExecutingSceneScript;
 	setCurrentSceneScriptAt(0);
 	_expectedEndLocation = 0;
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptLoadOverlayFont() {
+OpcodeResult Script::ScriptExecutor::scriptLoadOverlayFont() {
 	// scriptLoadOverlayFont (1008:d749). Loads a font resource for
 	// overlay text into the overlay font buffer.
 	uint8 resourceIndex = readByte();
@@ -2531,13 +2587,14 @@ void Script::ScriptExecutor::scriptLoadOverlayFont() {
 	if (!_engine->loadOverlayFont(resourceIndex, _executingScriptObjectId)) {
 		warning("Opcode 0x38: failed to load overlay font resource %u", resourceIndex);
 	}
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptAddOverlayTextEntry() {
+OpcodeResult Script::ScriptExecutor::scriptAddOverlayTextEntry() {
 	// Opcode 0x3A scriptAddOverlayTextEntry (1008:d82c).
 	View1 *currentView = (View1 *)_engine->findView("View1");
 	if (currentView == nullptr)
-		return;
+		return OpcodeResult::Continue;
 
 	clearScriptError();
 	const uint16 x = scriptReadValue16();
@@ -2549,15 +2606,15 @@ void Script::ScriptExecutor::scriptAddOverlayTextEntry() {
 
 	if (!_overlayTextStageActive) {
 		setScriptError(0x21);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (currentView->_overlayTextEntries.size() >= 10) {
 		setScriptError(0x22);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (entryType != 1) {
 		setScriptError(0x23);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	Common::StringArray strings;
@@ -2570,11 +2627,11 @@ void Script::ScriptExecutor::scriptAddOverlayTextEntry() {
 	}
 	if (strings.empty()) {
 		warning("Empty overlay text entry at offset %u", stringOffset);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (strings[0].size() >= 0x29) {
 		setScriptError(0x24);
-		return;
+		return OpcodeResult::Continue;
 	}
 	debugC(kDebugScript,
 		   "Opcode 3A overlay text: rawPos=(%u,%u) align=%u textOffset=%u entryType=%u scriptObject=%u text=\"%s\"",
@@ -2585,30 +2642,33 @@ void Script::ScriptExecutor::scriptAddOverlayTextEntry() {
 	entry.alignment = alignment;
 	entry.text = strings[0];
 	currentView->addOverlayTextEntry(entry);
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptClearOverlayText() {
+OpcodeResult Script::ScriptExecutor::scriptClearOverlayText() {
 	debugC(kDebugScript, "SCRIPT::clearOverlayText()");
 	View1 *currentView = (View1 *)_engine->findView("View1");
 	if (currentView != nullptr) {
 		currentView->clearOverlayTextEntries();
 	}
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptFadeToBlack() {
+OpcodeResult Script::ScriptExecutor::scriptFadeToBlack() {
 	const uint16 fadeSpeed = scriptReadValue16();
 	debugC(kDebugScript, "SCRIPT::fadeToBlack(speed=%u)", fadeSpeed);
 	View1 *currentView = (View1 *)_engine->findView("View1");
 	// Binary (executeOpcodes 0x3C): skip fade when g_wHelpButtonDisabled is set.
 	if (currentView != nullptr && currentView->isHelpButtonDisabled()) {
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (currentView != nullptr && fadeSpeed != 0) {
 		currentView->startFadeToBlack(fadeSpeed);
 	}
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptFrameWait() {
+OpcodeResult Script::ScriptExecutor::scriptFrameWait() {
 	// The original interpreter stores a frame countdown that is decremented
 	// once per game tick, rather than using a wall-clock timer.
 	uint16 duration = scriptReadValue16();
@@ -2617,9 +2677,10 @@ void Script::ScriptExecutor::scriptFrameWait() {
 	startFrameWait(duration);
 	enterBlockingWaitCursor();
 	endBuffering(_lastOpcodeTriggeredSkip);
+	return OpcodeResult::WaitForCallback;
 }
 
-void Script::ScriptExecutor::scriptSetPathfinding() {
+OpcodeResult Script::ScriptExecutor::scriptSetPathfinding() {
 	// scriptSetPathfinding (1008:c6d7). Sets/clears a pathfinding override.
 	// Index must be in range 200..0xEF (walkability values).
 	// Writes to scene data at index*5 + 0x4EA5 (enable byte) and +0x4EA6 (value).
@@ -2631,7 +2692,7 @@ void Script::ScriptExecutor::scriptSetPathfinding() {
 		   (!active || Macs2Engine::isWalkabilityWalkable(overrideValue)) ? "yes" : "NO");
 	if (areaID < 200 || areaID > 0xEF) {
 		setScriptError(0x0D);
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (active) {
 		g_engine->setPathfindingOverride(areaID, overrideValue);
@@ -2639,9 +2700,10 @@ void Script::ScriptExecutor::scriptSetPathfinding() {
 		g_engine->removePathfindingOverride(areaID);
 	}
 	debugLogActorWalkState("after setPathfinding");
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptTestSceneAnimFrame() {
+OpcodeResult Script::ScriptExecutor::scriptTestSceneAnimFrame() {
 	// scriptTestSceneAnimFrame (1008:b78d).
 	uint32 sceneAnimIndex = scriptReadValue32() - 0x1000;
 	uint32 minFrame = scriptReadValue32();
@@ -2653,68 +2715,73 @@ void Script::ScriptExecutor::scriptTestSceneAnimFrame() {
 
 	if (sceneAnimIndex == 0 || sceneAnimIndex > _engine->_backgroundAnimationsBlobs.size()) {
 		setScriptError(8);
-		return;
+		return OpcodeResult::Continue;
 	}
 	const BackgroundAnimationBlob &blob = _engine->_backgroundAnimationsBlobs[sceneAnimIndex - 1];
 	if (blob._blob.empty()) {
 		setScriptError(8);
-		return;
+		return OpcodeResult::Continue;
 	}
 	AnimBlobView view(blob._blob);
 	if (!view.isValid()) {
 		setScriptError(8);
-		return;
+		return OpcodeResult::Continue;
 	}
 	const uint16 seqPos = view.sequencePosition();
 	_animBlobRangeTestResult = seqPos >= minFrame && seqPos <= maxFrame;
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptEndOverlayText() {
+OpcodeResult Script::ScriptExecutor::scriptEndOverlayText() {
 	debugC(kDebugScript, "SCRIPT::endOverlayText()");
 	if (_overlayTextStageActive) {
 		_overlayTextStageActive = false;
 	}
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptFadeFromBlack() {
+OpcodeResult Script::ScriptExecutor::scriptFadeFromBlack() {
 	const uint16 fadeSpeed = scriptReadValue16();
 	debugC(kDebugScript, "SCRIPT::fadeFromBlack(speed = %u)", fadeSpeed);
 	View1 *currentView = (View1 *)_engine->findView("View1");
 	if (currentView != nullptr && currentView->isHelpButtonDisabled()) {
-		return;
+		return OpcodeResult::Continue;
 	}
 	if (currentView != nullptr && fadeSpeed != 0) {
 		currentView->startFading(fadeSpeed);
 	}
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptLoadPcmSound() {
+OpcodeResult Script::ScriptExecutor::scriptLoadPcmSound() {
 	const uint8 resourceIndex = readByte();
 	Common::Array<uint8> soundData;
 	int rateHz = 0x1F40;
 	int headerSkip = 2;
 	if (!loadSoundResource(soundData, resourceIndex, rateHz, headerSkip)) {
 		warning("Opcode 0x3E: failed to load PCM sound resource %u", resourceIndex);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	// Opcode 0x3E only installs the buffer; playback is opcode 0x40.
 	_engine->setCurrentSoundData(soundData, rateHz, headerSkip);
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptPlayPcmSound() {
+OpcodeResult Script::ScriptExecutor::scriptPlayPcmSound() {
 	// Binary (1000:0c7f): no error path; no-op when Sound Blaster disabled.
 	// Buffer was installed by opcode 0x3E; only start playback here.
 	if (_soundEnabled) {
 		if (!_engine->hasCurrentSoundData()) {
 			warning("Opcode 0x40: playPcmSound with no loaded sound data");
-			return;
+			return OpcodeResult::Continue;
 		}
 		_engine->playSample();
 	}
+	return OpcodeResult::Continue;
 }
 
-bool Script::ScriptExecutor::scriptWaitForSound() {
+OpcodeResult Script::ScriptExecutor::scriptWaitForSound() {
 	if (_soundEnabled && _soundSystemActive) {
 		_waitForPcmSound = true;
 		endTimer();
@@ -2723,42 +2790,44 @@ bool Script::ScriptExecutor::scriptWaitForSound() {
 			   g_engine->isSamplePlaying() ? 1 : 0, g_engine->hasCurrentSoundData() ? 1 : 0);
 		debugLogActorWalkState("waitForSound start");
 		enterBlockingWaitCursor();
-		return true;
+		return OpcodeResult::WaitForCallback;
 	}
 	debugC(kDebugScript, "SCRIPT::waitForSound skipped (soundEnabled=%d soundSystemActive=%d)",
 		   _soundEnabled ? 1 : 0, _soundSystemActive ? 1 : 0);
-	return false;
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptStopPcmSound() {
+OpcodeResult Script::ScriptExecutor::scriptStopPcmSound() {
 	if (_soundEnabled) {
 		_engine->stopSample();
 	}
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptLoadMusicSlot() {
+OpcodeResult Script::ScriptExecutor::scriptLoadMusicSlot() {
 	const uint16 slotID = scriptReadValue16();
 	const uint8 resourceIndex = readByte();
 
 	clearScriptError();
 	if (slotID < 1 || slotID > 2) {
 		setScriptError(0x27);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	// Binary (1000:0cac): cannot reload slot while it is the active playing slot (error 0x28).
 	if (_activeMusicSlot == slotID) {
 		setScriptError(0x28);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	Common::Array<uint8> slotData;
 	if (loadMusicResource(slotData, resourceIndex)) {
 		_musicSlots[slotID - 1] = Common::move(slotData);
 	}
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptPlayMusicSlot() {
+OpcodeResult Script::ScriptExecutor::scriptPlayMusicSlot() {
 	const uint16 slotID = scriptReadValue16();
 	const uint16 startMuted = scriptReadValue16();
 	const uint16 fadeParam = scriptReadValue16();
@@ -2766,12 +2835,12 @@ void Script::ScriptExecutor::scriptPlayMusicSlot() {
 	clearScriptError();
 	if (slotID < 1 || slotID > 2) {
 		setScriptError(0x27);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	if (!_musicEnabled || !_soundSystemActive) {
 		_activeMusicSlot = slotID;
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	if (_activeMusicSlot != 0) {
@@ -2782,7 +2851,7 @@ void Script::ScriptExecutor::scriptPlayMusicSlot() {
 	if (_musicSlots[slotID - 1].empty()) {
 		warning("Opcode 0x44: playMusicSlot with empty slot %u", slotID);
 		_activeMusicSlot = slotID;
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	if (!_engine->getMusic()->playSongData(_musicSlots[slotID - 1])) {
@@ -2790,7 +2859,7 @@ void Script::ScriptExecutor::scriptPlayMusicSlot() {
 		_musicControlStep = 0;
 		_musicControlVolume = 0;
 		_activeMusicSlot = slotID;
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	if (startMuted == 0) {
@@ -2806,9 +2875,10 @@ void Script::ScriptExecutor::scriptPlayMusicSlot() {
 	}
 
 	_activeMusicSlot = slotID;
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptStopMusicSlot() {
+OpcodeResult Script::ScriptExecutor::scriptStopMusicSlot() {
 	const uint16 slotID = scriptReadValue16();
 	const uint16 stopImmediately = scriptReadValue16();
 	const uint16 fadeParam = scriptReadValue16();
@@ -2816,12 +2886,12 @@ void Script::ScriptExecutor::scriptStopMusicSlot() {
 	clearScriptError();
 	if (slotID < 1 || slotID > 2) {
 		setScriptError(0x27);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	if (!_musicEnabled || !_soundSystemActive) {
 		_activeMusicSlot = 0;
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	if (_activeMusicSlot == slotID) {
@@ -2834,26 +2904,27 @@ void Script::ScriptExecutor::scriptStopMusicSlot() {
 			_activeMusicSlot = 0;
 		}
 	}
+	return OpcodeResult::Continue;
 }
 
-bool Script::ScriptExecutor::scriptWaitForMusic() {
+OpcodeResult Script::ScriptExecutor::scriptWaitForMusic() {
 	if (_soundSystemActive && _musicEnabled) {
 		_waitForMusicControl = true;
 		endTimer();
 		endBuffering(_lastOpcodeTriggeredSkip);
 		enterBlockingWaitCursor();
-		return true;
+		return OpcodeResult::WaitForCallback;
 	}
-	return false;
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptFreeMusicSlot() {
+OpcodeResult Script::ScriptExecutor::scriptFreeMusicSlot() {
 	const uint16 slotID = scriptReadValue16();
 
 	clearScriptError();
 	if (slotID < 1 || slotID > 2) {
 		setScriptError(0x27);
-		return;
+		return OpcodeResult::Continue;
 	}
 
 	if (_activeMusicSlot == slotID) {
@@ -2863,77 +2934,82 @@ void Script::ScriptExecutor::scriptFreeMusicSlot() {
 		_activeMusicSlot = 0;
 	}
 	_musicSlots[slotID - 1].clear();
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptGetObjectX() {
+OpcodeResult Script::ScriptExecutor::scriptGetObjectX() {
 	// Opcode 0x48 scriptGetObjectX (1008:d917): saves object field +0 (X).
 	int32 objectID = (int32)scriptReadValue32() - 0x400;
 
 	clearScriptError();
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	GameObject *object = GameObjects::getObjectByIndex((uint16)objectID);
 	if (object == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::Continue;
 	}
 	scriptSaveVariableHelper(object->_position.x);
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptGetObjectY() {
+OpcodeResult Script::ScriptExecutor::scriptGetObjectY() {
 	// Opcode 0x49 scriptGetObjectY (1008:d977): saves object field +2 (Y).
 	int32 objectID = (int32)scriptReadValue32() - 0x400;
 
 	clearScriptError();
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	GameObject *object = GameObjects::getObjectByIndex((uint16)objectID);
 	if (object == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::Continue;
 	}
 	scriptSaveVariableHelper(object->_position.y);
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptGetObjectField8() {
+OpcodeResult Script::ScriptExecutor::scriptGetObjectField8() {
 	// Opcode 0x4A scriptGetObjectField8 (1008:d9d8): saves object field +8.
 	int32 objectID = (int32)scriptReadValue32() - 0x400;
 
 	clearScriptError();
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	GameObject *object = GameObjects::getObjectByIndex((uint16)objectID);
 	if (object == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::Continue;
 	}
 	scriptSaveVariableHelper(object->_verticalOffsetScale);
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptGetObjectOrientation() {
+OpcodeResult Script::ScriptExecutor::scriptGetObjectOrientation() {
 	// Opcode 0x4B scriptGetObjectOrientation (1008:da3a): saves object field +6.
 	int32 objectID = (int32)scriptReadValue32() - 0x400;
 
 	clearScriptError();
 	if (objectID < 1 || objectID > 0x200) {
 		setScriptError(2);
-		return;
+		return OpcodeResult::Continue;
 	}
 	GameObject *object = GameObjects::getObjectByIndex((uint16)objectID);
 	if (object == nullptr) {
 		setScriptError(0x19);
-		return;
+		return OpcodeResult::Continue;
 	}
 	scriptSaveVariableHelper(object->_orientation);
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptClearActorInventory() {
+OpcodeResult Script::ScriptExecutor::scriptClearActorInventory() {
 	for (GameObject *object : GameObjects::instance()._objects) {
 		if (object != nullptr && object->_sceneIndex == Scenes::instance()._currentActorIndex + 0x400) {
 			object->_sceneIndex = 0;
@@ -2948,9 +3024,10 @@ void Script::ScriptExecutor::scriptClearActorInventory() {
 			currentView->_activeInventoryItem = nullptr;
 		}
 	}
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptSetPathfindingRemap() {
+OpcodeResult Script::ScriptExecutor::scriptSetPathfindingRemap() {
 	// scriptSetPathfindingRemap (1008:dafb). Writes to scene+value*5+0x4EA8.
 	const uint16 sourceValue = scriptReadValue16();
 	const uint16 targetValue = scriptReadValue16();
@@ -2959,27 +3036,29 @@ void Script::ScriptExecutor::scriptSetPathfindingRemap() {
 	if (sourceValue < AREA_OVERRIDE_MIN || sourceValue > AREA_OVERRIDE_MAX ||
 		targetValue < AREA_OVERRIDE_MIN || targetValue > AREA_OVERRIDE_MAX) {
 		setScriptError(0x0D);
-		return;
+		return OpcodeResult::Continue;
 	}
 	g_engine->_areaOverrides[sourceValue - AREA_OVERRIDE_MIN] = targetValue;
+	return OpcodeResult::Continue;
 }
 
-bool Script::ScriptExecutor::scriptWaitForAdlib() {
+OpcodeResult Script::ScriptExecutor::scriptWaitForAdlib() {
 	if (_soundSystemActive && _musicEnabled) {
 		_waitForAdlibReady = true;
 		endTimer();
 		endBuffering(_lastOpcodeTriggeredSkip);
 		enterBlockingWaitCursor();
-		return true;
+		return OpcodeResult::WaitForCallback;
 	}
-	return false;
+	return OpcodeResult::Continue;
 }
 
-void Script::ScriptExecutor::scriptFreePcmSound() {
+OpcodeResult Script::ScriptExecutor::scriptFreePcmSound() {
 	debugC(kDebugScript, "SCRIPT::freePcmSound()");
 	if (_soundEnabled)
 		_engine->stopSample();
 	_engine->clearCurrentSoundData();
+	return OpcodeResult::Continue;
 }
 
 void ScriptExecutor::setOpcodeTable(const OpcodeEntry *table, uint size) {
@@ -2994,511 +3073,92 @@ const char *ScriptExecutor::opcodeName(uint8 opcode) const {
 	return name ? name : "?";
 }
 
-OpcodeResult ScriptExecutor::opSetVar() {
-	scriptSetVar();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetVarOr() {
-	scriptSetVarOr();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opIfFalse() {
-	scriptIfFalse();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opIfTrue() {
-	scriptIfTrue();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opCompare() {
-	if (!scriptCompare())
-		return OpcodeResult::FinishScript;
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opIfInteraction() {
-	scriptIfInteraction();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opEndIf() {
-	scriptEndIf();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opElse() {
-	scriptElse();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opNop09() {
-	scriptNop09();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opPrintStringLeft() {
-	scriptPrintStringLeft();
-	return OpcodeResult::WaitForCallback;
-}
-
-OpcodeResult ScriptExecutor::opMoveObject() {
-	scriptMoveObject();
-	if (hasScriptError())
-		return OpcodeResult::FinishScript;
-	// Binary: after scriptMoveObject(), exits if position >= end.
-	if (_stream->pos() >= _stream->size())
-		return OpcodeResult::FinishScript;
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opChangeScene() {
-	const ExecutionResult result = scriptChangeScene();
-	if (result == ExecutionResult::WaitingForCallback)
-		return OpcodeResult::WaitForCallback;
-	return OpcodeResult::ReturnFinished;
-}
-
-OpcodeResult ScriptExecutor::opShowDialogue() {
-	const ExecutionResult dialogueResult = scriptShowDialogue();
-	if (dialogueResult == ExecutionResult::WaitingForCallback)
-		return OpcodeResult::WaitForCallback;
-	return OpcodeResult::FinishScript;
-}
-
-OpcodeResult ScriptExecutor::opChangeAnimation() {
-	scriptChangeAnimation();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opFrameWait() {
-	scriptFrameWait();
-	return OpcodeResult::WaitForCallback;
-}
-
-OpcodeResult ScriptExecutor::opWalkToPosition() {
-	scriptWalkToPosition();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opWaitForWalk() {
-	const ExecutionResult result = scriptWaitForWalk();
-	if (result == ExecutionResult::WaitingForCallback)
-		return OpcodeResult::WaitForCallback;
-	return OpcodeResult::ReturnFinished;
-}
-
-OpcodeResult ScriptExecutor::opSetPathfinding() {
-	scriptSetPathfinding();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSkipUntil14() {
-	scriptSkipUntil14();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSkipWord() {
-	scriptSkipWord();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opClearDialogueChoices() {
-	scriptClearDialogueChoices();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opAddDialogueChoice() {
-	scriptAddDialogueChoice();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opShowDialogueChoice() {
-	const ExecutionResult result = scriptShowDialogueChoice();
-	if (result == ExecutionResult::WaitingForCallback)
-		return OpcodeResult::WaitForCallback;
-	return OpcodeResult::ReturnFinished;
-}
-
-OpcodeResult ScriptExecutor::opDismissPanel() {
-	const ExecutionResult result = scriptDismissPanel();
-	if (result == ExecutionResult::WaitingForCallback)
-		return OpcodeResult::WaitForCallback;
-	return OpcodeResult::ReturnFinished;
-}
-
-OpcodeResult ScriptExecutor::opWalkToAndPickup() {
-	scriptWalkToAndPickup();
-	return OpcodeResult::FinishScript;
-}
-
-OpcodeResult ScriptExecutor::opSetPickupFrames() {
-	scriptSetPickupFrames();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetupObject() {
-	scriptSetupObject();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetSkippable() {
-	scriptSetSkippable();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opClearSkippable() {
-	scriptClearSkippable();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opPlayAnimation() {
-	scriptPlayAnimation();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opTestPathfinding() {
-	scriptTestPathfinding();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetYOffset() {
-	scriptSetYOffset();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetMotion() {
-	scriptSetMotion();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetOrientation() {
-	scriptSetOrientation();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opMoveToPosition() {
-	scriptMoveToPosition();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opAddValues() {
-	scriptAddValues();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSubValues() {
-	scriptSubValues();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opLoadSpecialAnim() {
-	scriptLoadSpecialAnim();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetDirection() {
-	scriptSetDirection();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opStopAnimation() {
-	scriptStopAnimation();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opOpenInventory() {
-	// Binary executeOpcodes @ 1008:e0f9 (full + demo):
-	//   scriptOpenInventory();
-	//   g_wScriptPosition = g_wScriptEndPosition;
-	//   g_wExecutingScriptObjectId = 0x201;
-	//   goto LAB_1008_e3bd;
-	// loadNextScript() honors ScriptObjectId > 0x200 and will not start/continue
-	// the object-script walk (including when 0x29 ran from the scene script).
-	scriptOpenInventory();
-	_stream->seek(_stream->size(), SEEK_SET);
-	_executingObjectIndex = 0x201;
-	_executingScriptObjectId = 0x201;
-	return OpcodeResult::FinishScript;
-}
-
-OpcodeResult ScriptExecutor::opLoadObjectAnim() {
-	scriptLoadObjectAnim();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opCheckObjectData() {
-	scriptCheckObjectData();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opCheckInventory() {
-	scriptCheckInventory();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetSnapToTarget() {
-	scriptSetSnapToTarget();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opTestSceneAnimFrame() {
-	scriptTestSceneAnimFrame();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opTestObjectAnimFrame() {
-	scriptTestObjectAnimFrame();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opPrintStringRight() {
-	scriptPrintStringRight();
-	return OpcodeResult::WaitForCallback;
-}
-
-OpcodeResult ScriptExecutor::opSetPaletteDarkness() {
-	scriptSetPaletteDarkness();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetObjectShading() {
-	scriptSetObjectShading();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetObjectScaling() {
-	scriptSetObjectScaling();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetHotspotOverride() {
-	scriptSetHotspotOverride();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetObjectBounds() {
-	scriptSetObjectBounds();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opDismissAllPanels() {
-	scriptDismissAllPanels();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opResetToSceneScript() {
-	scriptResetToSceneScript();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opLoadOverlayFont() {
-	scriptLoadOverlayFont();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opEndOverlayText() {
-	scriptEndOverlayText();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opAddOverlayTextEntry() {
-	scriptAddOverlayTextEntry();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opClearOverlayText() {
-	scriptClearOverlayText();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opFadeToBlack() {
-	scriptFadeToBlack();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opFadeFromBlack() {
-	scriptFadeFromBlack();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opLoadPcmSound() {
-	scriptLoadPcmSound();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opFreePcmSound() {
-	scriptFreePcmSound();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opPlayPcmSound() {
-	scriptPlayPcmSound();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opWaitForSound() {
-	if (scriptWaitForSound())
-		return OpcodeResult::WaitForCallback;
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opStopPcmSound() {
-	scriptStopPcmSound();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opLoadMusicSlot() {
-	scriptLoadMusicSlot();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opPlayMusicSlot() {
-	scriptPlayMusicSlot();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opStopMusicSlot() {
-	scriptStopMusicSlot();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opFreeMusicSlot() {
-	scriptFreeMusicSlot();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opWaitForMusic() {
-	if (scriptWaitForMusic())
-		return OpcodeResult::WaitForCallback;
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opGetObjectX() {
-	scriptGetObjectX();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opGetObjectY() {
-	scriptGetObjectY();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opGetObjectField8() {
-	scriptGetObjectField8();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opGetObjectOrientation() {
-	scriptGetObjectOrientation();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opClearActorInventory() {
-	scriptClearActorInventory();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetPathfindingRemap() {
-	scriptSetPathfindingRemap();
-	return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opWaitForAdlib() {
-	if (scriptWaitForAdlib())
-		return OpcodeResult::WaitForCallback;
-	return OpcodeResult::Continue;
-}
-
 // Script dialect v1 opcode table (indexed by primary opcode byte).
 // Later dialects can install an alternate table via setOpcodeTable().
 const ScriptExecutor::OpcodeEntry ScriptExecutor::kV1OpcodeTable[] = {
 	{nullptr, nullptr},
-	{"setVar", &ScriptExecutor::opSetVar},
-	{"setVarOr", &ScriptExecutor::opSetVarOr},
-	{"ifFalse", &ScriptExecutor::opIfFalse},
-	{"ifTrue", &ScriptExecutor::opIfTrue},
-	{"compare", &ScriptExecutor::opCompare},
-	{"ifInteraction", &ScriptExecutor::opIfInteraction},
-	{"endIf", &ScriptExecutor::opEndIf},
-	{"else", &ScriptExecutor::opElse},
-	{"nop09", &ScriptExecutor::opNop09},
-	{"printStringLeft", &ScriptExecutor::opPrintStringLeft},
-	{"moveObject", &ScriptExecutor::opMoveObject},
-	{"changeScene", &ScriptExecutor::opChangeScene},
-	{"showDialogue", &ScriptExecutor::opShowDialogue},
-	{"changeAnimation", &ScriptExecutor::opChangeAnimation},
-	{"frameWait", &ScriptExecutor::opFrameWait},
-	{"walkToPosition", &ScriptExecutor::opWalkToPosition},
-	{"waitForWalk", &ScriptExecutor::opWaitForWalk},
-	{"setPathfinding", &ScriptExecutor::opSetPathfinding},
-	{"skipUntil14", &ScriptExecutor::opSkipUntil14},
-	{"skipWord", &ScriptExecutor::opSkipWord},
-	{"clearDialogueChoices", &ScriptExecutor::opClearDialogueChoices},
-	{"addDialogueChoice", &ScriptExecutor::opAddDialogueChoice},
-	{"showDialogueChoice", &ScriptExecutor::opShowDialogueChoice},
-	{"dismissPanel", &ScriptExecutor::opDismissPanel},
-	{"walkToAndPickup", &ScriptExecutor::opWalkToAndPickup},
-	{"setPickupFrames", &ScriptExecutor::opSetPickupFrames},
-	{"setupObject", &ScriptExecutor::opSetupObject},
-	{"setSkippable", &ScriptExecutor::opSetSkippable},
-	{"clearSkippable", &ScriptExecutor::opClearSkippable},
-	{"playAnimation", &ScriptExecutor::opPlayAnimation},
-	{"testPathfinding", &ScriptExecutor::opTestPathfinding},
-	{"setYOffset", &ScriptExecutor::opSetYOffset},
-	{"setMotion", &ScriptExecutor::opSetMotion},
-	{"setOrientation", &ScriptExecutor::opSetOrientation},
-	{"moveToPosition", &ScriptExecutor::opMoveToPosition},
-	{"addValues", &ScriptExecutor::opAddValues},
-	{"subValues", &ScriptExecutor::opSubValues},
-	{"loadSpecialAnim", &ScriptExecutor::opLoadSpecialAnim},
-	{"setDirection", &ScriptExecutor::opSetDirection},
-	{"stopAnimation", &ScriptExecutor::opStopAnimation},
-	{"openInventory", &ScriptExecutor::opOpenInventory},
-	{"loadObjectAnim", &ScriptExecutor::opLoadObjectAnim},
-	{"checkObjectData", &ScriptExecutor::opCheckObjectData},
-	{"checkInventory", &ScriptExecutor::opCheckInventory},
-	{"setSnapToTarget", &ScriptExecutor::opSetSnapToTarget},
-	{"testSceneAnimFrame", &ScriptExecutor::opTestSceneAnimFrame},
-	{"testObjectAnimFrame", &ScriptExecutor::opTestObjectAnimFrame},
-	{"printStringRight", &ScriptExecutor::opPrintStringRight},
-	{"setPaletteDarkness", &ScriptExecutor::opSetPaletteDarkness},
-	{"setObjectShading", &ScriptExecutor::opSetObjectShading},
-	{"setObjectScaling", &ScriptExecutor::opSetObjectScaling},
-	{"setHotspotOverride", &ScriptExecutor::opSetHotspotOverride},
-	{"setObjectBounds", &ScriptExecutor::opSetObjectBounds},
-	{"dismissAllPanels", &ScriptExecutor::opDismissAllPanels},
-	{"resetToSceneScript", &ScriptExecutor::opResetToSceneScript},
-	{"loadOverlayFont", &ScriptExecutor::opLoadOverlayFont},
-	{"endOverlayText", &ScriptExecutor::opEndOverlayText},
-	{"addOverlayTextEntry", &ScriptExecutor::opAddOverlayTextEntry},
-	{"clearOverlayText", &ScriptExecutor::opClearOverlayText},
-	{"fadeToBlack", &ScriptExecutor::opFadeToBlack},
-	{"fadeFromBlack", &ScriptExecutor::opFadeFromBlack},
-	{"loadPcmSound", &ScriptExecutor::opLoadPcmSound},
-	{"freePcmSound", &ScriptExecutor::opFreePcmSound},
-	{"playPcmSound", &ScriptExecutor::opPlayPcmSound},
-	{"waitForSound", &ScriptExecutor::opWaitForSound},
-	{"stopPcmSound", &ScriptExecutor::opStopPcmSound},
-	{"loadMusicSlot", &ScriptExecutor::opLoadMusicSlot},
-	{"playMusicSlot", &ScriptExecutor::opPlayMusicSlot},
-	{"stopMusicSlot", &ScriptExecutor::opStopMusicSlot},
-	{"freeMusicSlot", &ScriptExecutor::opFreeMusicSlot},
-	{"waitForMusic", &ScriptExecutor::opWaitForMusic},
-	{"getObjectX", &ScriptExecutor::opGetObjectX},
-	{"getObjectY", &ScriptExecutor::opGetObjectY},
-	{"getObjectField8", &ScriptExecutor::opGetObjectField8},
-	{"getObjectOrientation", &ScriptExecutor::opGetObjectOrientation},
-	{"clearActorInventory", &ScriptExecutor::opClearActorInventory},
-	{"setPathfindingRemap", &ScriptExecutor::opSetPathfindingRemap},
-	{"waitForAdlib", &ScriptExecutor::opWaitForAdlib}
+	{"setVar", &ScriptExecutor::scriptSetVar},
+	{"setVarOr", &ScriptExecutor::scriptSetVarOr},
+	{"ifFalse", &ScriptExecutor::scriptIfFalse},
+	{"ifTrue", &ScriptExecutor::scriptIfTrue},
+	{"compare", &ScriptExecutor::scriptCompare},
+	{"ifInteraction", &ScriptExecutor::scriptIfInteraction},
+	{"endIf", &ScriptExecutor::scriptEndIf},
+	{"else", &ScriptExecutor::scriptElse},
+	{"nop09", &ScriptExecutor::scriptNop09},
+	{"printStringLeft", &ScriptExecutor::scriptPrintStringLeft},
+	{"moveObject", &ScriptExecutor::scriptMoveObject},
+	{"changeScene", &ScriptExecutor::scriptChangeScene},
+	{"showDialogue", &ScriptExecutor::scriptShowDialogue},
+	{"changeAnimation", &ScriptExecutor::scriptChangeAnimation},
+	{"frameWait", &ScriptExecutor::scriptFrameWait},
+	{"walkToPosition", &ScriptExecutor::scriptWalkToPosition},
+	{"waitForWalk", &ScriptExecutor::scriptWaitForWalk},
+	{"setPathfinding", &ScriptExecutor::scriptSetPathfinding},
+	{"skipUntil14", &ScriptExecutor::scriptSkipUntil14},
+	{"skipWord", &ScriptExecutor::scriptSkipWord},
+	{"clearDialogueChoices", &ScriptExecutor::scriptClearDialogueChoices},
+	{"addDialogueChoice", &ScriptExecutor::scriptAddDialogueChoice},
+	{"showDialogueChoice", &ScriptExecutor::scriptShowDialogueChoice},
+	{"dismissPanel", &ScriptExecutor::scriptDismissPanel},
+	{"walkToAndPickup", &ScriptExecutor::scriptWalkToAndPickup},
+	{"setPickupFrames", &ScriptExecutor::scriptSetPickupFrames},
+	{"setupObject", &ScriptExecutor::scriptSetupObject},
+	{"setSkippable", &ScriptExecutor::scriptSetSkippable},
+	{"clearSkippable", &ScriptExecutor::scriptClearSkippable},
+	{"playAnimation", &ScriptExecutor::scriptPlayAnimation},
+	{"testPathfinding", &ScriptExecutor::scriptTestPathfinding},
+	{"setYOffset", &ScriptExecutor::scriptSetYOffset},
+	{"setMotion", &ScriptExecutor::scriptSetMotion},
+	{"setOrientation", &ScriptExecutor::scriptSetOrientation},
+	{"moveToPosition", &ScriptExecutor::scriptMoveToPosition},
+	{"addValues", &ScriptExecutor::scriptAddValues},
+	{"subValues", &ScriptExecutor::scriptSubValues},
+	{"loadSpecialAnim", &ScriptExecutor::scriptLoadSpecialAnim},
+	{"setDirection", &ScriptExecutor::scriptSetDirection},
+	{"stopAnimation", &ScriptExecutor::scriptStopAnimation},
+	{"openInventory", &ScriptExecutor::scriptOpenInventory},
+	{"loadObjectAnim", &ScriptExecutor::scriptLoadObjectAnim},
+	{"checkObjectData", &ScriptExecutor::scriptCheckObjectData},
+	{"checkInventory", &ScriptExecutor::scriptCheckInventory},
+	{"setSnapToTarget", &ScriptExecutor::scriptSetSnapToTarget},
+	{"testSceneAnimFrame", &ScriptExecutor::scriptTestSceneAnimFrame},
+	{"testObjectAnimFrame", &ScriptExecutor::scriptTestObjectAnimFrame},
+	{"printStringRight", &ScriptExecutor::scriptPrintStringRight},
+	{"setPaletteDarkness", &ScriptExecutor::scriptSetPaletteDarkness},
+	{"setObjectShading", &ScriptExecutor::scriptSetObjectShading},
+	{"setObjectScaling", &ScriptExecutor::scriptSetObjectScaling},
+	{"setHotspotOverride", &ScriptExecutor::scriptSetHotspotOverride},
+	{"setObjectBounds", &ScriptExecutor::scriptSetObjectBounds},
+	{"dismissAllPanels", &ScriptExecutor::scriptDismissAllPanels},
+	{"resetToSceneScript", &ScriptExecutor::scriptResetToSceneScript},
+	{"loadOverlayFont", &ScriptExecutor::scriptLoadOverlayFont},
+	{"endOverlayText", &ScriptExecutor::scriptEndOverlayText},
+	{"addOverlayTextEntry", &ScriptExecutor::scriptAddOverlayTextEntry},
+	{"clearOverlayText", &ScriptExecutor::scriptClearOverlayText},
+	{"fadeToBlack", &ScriptExecutor::scriptFadeToBlack},
+	{"fadeFromBlack", &ScriptExecutor::scriptFadeFromBlack},
+	{"loadPcmSound", &ScriptExecutor::scriptLoadPcmSound},
+	{"freePcmSound", &ScriptExecutor::scriptFreePcmSound},
+	{"playPcmSound", &ScriptExecutor::scriptPlayPcmSound},
+	{"waitForSound", &ScriptExecutor::scriptWaitForSound},
+	{"stopPcmSound", &ScriptExecutor::scriptStopPcmSound},
+	{"loadMusicSlot", &ScriptExecutor::scriptLoadMusicSlot},
+	{"playMusicSlot", &ScriptExecutor::scriptPlayMusicSlot},
+	{"stopMusicSlot", &ScriptExecutor::scriptStopMusicSlot},
+	{"freeMusicSlot", &ScriptExecutor::scriptFreeMusicSlot},
+	{"waitForMusic", &ScriptExecutor::scriptWaitForMusic},
+	{"getObjectX", &ScriptExecutor::scriptGetObjectX},
+	{"getObjectY", &ScriptExecutor::scriptGetObjectY},
+	{"getObjectField8", &ScriptExecutor::scriptGetObjectField8},
+	{"getObjectOrientation", &ScriptExecutor::scriptGetObjectOrientation},
+	{"clearActorInventory", &ScriptExecutor::scriptClearActorInventory},
+	{"setPathfindingRemap", &ScriptExecutor::scriptSetPathfindingRemap},
+	{"waitForAdlib", &ScriptExecutor::scriptWaitForAdlib}
 };
 const uint ScriptExecutor::kV1OpcodeTableSize = ARRAYSIZE(ScriptExecutor::kV1OpcodeTable);
 
-ExecutionResult Script::ScriptExecutor::executeOpcodes() {
+OpcodeResult Script::ScriptExecutor::executeOpcodes() {
 	debugC(kDebugScript, "----- Scripting function entered - scene: %.2x 1014: %.2x 1012: %.2x", Scenes::instance()._currentSceneIndex, _isSceneInitRun, _repeatRunFlag);
 	_isRunningScript = true;
 	// Confirmed: no interrupt mechanism exists. Wait states (frameWait, walkTarget,
@@ -3534,7 +3194,7 @@ ExecutionResult Script::ScriptExecutor::executeOpcodes() {
 		if (scriptDebuggerShouldPause()) {
 			_debugPaused = true;
 			_state = ExecutorState::Executing; // preserve state for resume
-			return ExecutionResult::WaitingForCallback;
+			return OpcodeResult::WaitForCallback;
 		}
 #endif
 		const byte opcode1 = readByte(); // [bp - 1h]
@@ -3567,9 +3227,9 @@ ExecutionResult Script::ScriptExecutor::executeOpcodes() {
 
 		const OpcodeResult opcodeResult = (this->*(entry->handler))();
 		if (opcodeResult == OpcodeResult::WaitForCallback)
-			return ExecutionResult::WaitingForCallback;
+			return OpcodeResult::WaitForCallback;
 		if (opcodeResult == OpcodeResult::ReturnFinished)
-			return ExecutionResult::ScriptFinished;
+			return OpcodeResult::ReturnFinished;
 
 		endBuffering(_lastOpcodeTriggeredSkip);
 		if (opcodeResult == OpcodeResult::FinishScript)
@@ -3579,7 +3239,7 @@ ExecutionResult Script::ScriptExecutor::executeOpcodes() {
 	if (hasScriptError())
 		recordScriptErrorPosition();
 	debugC(kDebugScript, "----- Scripting function left");
-	return ExecutionResult::ScriptFinished;
+	return OpcodeResult::FinishScript;
 }
 
 void ScriptExecutor::run(bool firstRun) {
diff --git a/engines/macs2/scriptexecutor.h b/engines/macs2/scriptexecutor.h
index 59af9b705bf..30fd3af2ff1 100644
--- a/engines/macs2/scriptexecutor.h
+++ b/engines/macs2/scriptexecutor.h
@@ -67,25 +67,17 @@ enum class ExecutorState {
 	WaitingForCallback
 };
 
-enum class ExecutionResult {
-	// We have finished executing the script
-	ScriptFinished,
-
-	// We are now waiting for a callback
-	WaitingForCallback
-};
-
-/** Per-opcode handler result for table-driven dispatch in executeOpcodes(). */
+/** Result of a script opcode handler / executeOpcodes(). */
 enum class OpcodeResult {
 	/** Continue with the next opcode in the current script. */
 	Continue,
-	/** Break the opcode loop, then return ScriptFinished (clears running flag). */
+	/** Break the opcode loop, then finish (clears running flag). */
 	FinishScript,
-	/** Return WaitingForCallback immediately (leaves running flag set). */
+	/** Pause execution and wait for an external callback (leaves running flag set). */
 	WaitForCallback,
 	/**
-	 * Return ScriptFinished immediately (leaves running flag set).
-	 * Matches legacy `return scriptXxx()` paths such as changeScene / waitForWalk.
+	 * Finish immediately (leaves running flag set).
+	 * Used by changeScene / waitForWalk / showDialogueChoice / dismissPanel.
 	 */
 	ReturnFinished
 };
@@ -133,164 +125,84 @@ private:
 #ifdef DEMACS2
 public:
 #endif
-	void scriptSetVar();
-	void scriptSetVarOr();
-	void scriptIfTrue();
-	void scriptIfFalse();
-	bool scriptCompare();
-	void scriptIfInteraction();
-	void scriptEndIf();
-	void scriptElse();
-	void scriptNop09();
-	void scriptPrintStringLeft();
-	void scriptMoveObject();
-	ExecutionResult scriptChangeScene();
-	ExecutionResult scriptShowDialogue();
-	void scriptWalkToPosition();
-	ExecutionResult scriptWaitForWalk();
-	void scriptSkipWord();
-	void scriptClearDialogueChoices();
-	void scriptAddDialogueChoice();
-	ExecutionResult scriptShowDialogueChoice();
-	ExecutionResult scriptDismissPanel();
-	void scriptWalkToAndPickup();
-	bool scriptSetPickupFrames();
-	void scriptSetupObject();
-	void scriptSetSkippable();
-	void scriptClearSkippable();
-	void scriptPlayAnimation();
-	void scriptTestPathfinding();
-	void scriptSetYOffset();
-	void scriptSetMotion();
-	bool scriptSetOrientation();
-	void scriptMoveToPosition();
-	void scriptAddValues();
-	void scriptSubValues();
-	void scriptLoadSpecialAnim();
-	void scriptSetDirection();
-	void scriptStopAnimation();
-	void scriptOpenInventory();
-	void scriptLoadObjectAnim();
-	void scriptCheckObjectData();
-	void scriptCheckInventory();
-	void scriptSetSnapToTarget();
-	void scriptTestObjectAnimFrame();
-	void scriptPrintStringRight();
-	void scriptSetPaletteDarkness();
-	void scriptSetObjectShading();
-	void scriptSetObjectScaling();
-	void scriptSetHotspotOverride();
-	void scriptSetObjectBounds();
-	void scriptDismissAllPanels();
-	void scriptResetToSceneScript();
-	void scriptLoadOverlayFont();
-	void scriptAddOverlayTextEntry();
-	void scriptClearOverlayText();
-	void scriptFadeToBlack();
-	void scriptLoadPcmSound();
-	void scriptPlayPcmSound();
-	bool scriptWaitForSound();
-	void scriptStopPcmSound();
-	void scriptLoadMusicSlot();
-	void scriptPlayMusicSlot();
-	void scriptStopMusicSlot();
-	bool scriptWaitForMusic();
-	void scriptFreeMusicSlot();
-	void scriptGetObjectX();
-	void scriptGetObjectY();
-	void scriptGetObjectField8();
-	void scriptGetObjectOrientation();
-	void scriptClearActorInventory();
-	void scriptSetPathfindingRemap();
-	bool scriptWaitForAdlib();
-	void scriptSkipUntil14();
-	void scriptChangeAnimation();
-	void scriptFrameWait();
-	void scriptSetPathfinding();
-	void scriptTestSceneAnimFrame();
-	void scriptEndOverlayText();
-	void scriptFadeFromBlack();
-	void scriptFreePcmSound();
-
-	// Table-driven opcode wrappers (return OpcodeResult for executeOpcodes).
-	OpcodeResult opSetVar();
-	OpcodeResult opSetVarOr();
-	OpcodeResult opIfFalse();
-	OpcodeResult opIfTrue();
-	OpcodeResult opCompare();
-	OpcodeResult opIfInteraction();
-	OpcodeResult opEndIf();
-	OpcodeResult opElse();
-	OpcodeResult opNop09();
-	OpcodeResult opPrintStringLeft();
-	OpcodeResult opMoveObject();
-	OpcodeResult opChangeScene();
-	OpcodeResult opShowDialogue();
-	OpcodeResult opChangeAnimation();
-	OpcodeResult opFrameWait();
-	OpcodeResult opWalkToPosition();
-	OpcodeResult opWaitForWalk();
-	OpcodeResult opSetPathfinding();
-	OpcodeResult opSkipUntil14();
-	OpcodeResult opSkipWord();
-	OpcodeResult opClearDialogueChoices();
-	OpcodeResult opAddDialogueChoice();
-	OpcodeResult opShowDialogueChoice();
-	OpcodeResult opDismissPanel();
-	OpcodeResult opWalkToAndPickup();
-	OpcodeResult opSetPickupFrames();
-	OpcodeResult opSetupObject();
-	OpcodeResult opSetSkippable();
-	OpcodeResult opClearSkippable();
-	OpcodeResult opPlayAnimation();
-	OpcodeResult opTestPathfinding();
-	OpcodeResult opSetYOffset();
-	OpcodeResult opSetMotion();
-	OpcodeResult opSetOrientation();
-	OpcodeResult opMoveToPosition();
-	OpcodeResult opAddValues();
-	OpcodeResult opSubValues();
-	OpcodeResult opLoadSpecialAnim();
-	OpcodeResult opSetDirection();
-	OpcodeResult opStopAnimation();
-	OpcodeResult opOpenInventory();
-	OpcodeResult opLoadObjectAnim();
-	OpcodeResult opCheckObjectData();
-	OpcodeResult opCheckInventory();
-	OpcodeResult opSetSnapToTarget();
-	OpcodeResult opTestSceneAnimFrame();
-	OpcodeResult opTestObjectAnimFrame();
-	OpcodeResult opPrintStringRight();
-	OpcodeResult opSetPaletteDarkness();
-	OpcodeResult opSetObjectShading();
-	OpcodeResult opSetObjectScaling();
-	OpcodeResult opSetHotspotOverride();
-	OpcodeResult opSetObjectBounds();
-	OpcodeResult opDismissAllPanels();
-	OpcodeResult opResetToSceneScript();
-	OpcodeResult opLoadOverlayFont();
-	OpcodeResult opEndOverlayText();
-	OpcodeResult opAddOverlayTextEntry();
-	OpcodeResult opClearOverlayText();
-	OpcodeResult opFadeToBlack();
-	OpcodeResult opFadeFromBlack();
-	OpcodeResult opLoadPcmSound();
-	OpcodeResult opFreePcmSound();
-	OpcodeResult opPlayPcmSound();
-	OpcodeResult opWaitForSound();
-	OpcodeResult opStopPcmSound();
-	OpcodeResult opLoadMusicSlot();
-	OpcodeResult opPlayMusicSlot();
-	OpcodeResult opStopMusicSlot();
-	OpcodeResult opFreeMusicSlot();
-	OpcodeResult opWaitForMusic();
-	OpcodeResult opGetObjectX();
-	OpcodeResult opGetObjectY();
-	OpcodeResult opGetObjectField8();
-	OpcodeResult opGetObjectOrientation();
-	OpcodeResult opClearActorInventory();
-	OpcodeResult opSetPathfindingRemap();
-	OpcodeResult opWaitForAdlib();
+	OpcodeResult scriptSetVar();
+	OpcodeResult scriptSetVarOr();
+	OpcodeResult scriptIfTrue();
+	OpcodeResult scriptIfFalse();
+	OpcodeResult scriptCompare();
+	OpcodeResult scriptIfInteraction();
+	OpcodeResult scriptEndIf();
+	OpcodeResult scriptElse();
+	OpcodeResult scriptNop09();
+	OpcodeResult scriptPrintStringLeft();
+	OpcodeResult scriptMoveObject();
+	OpcodeResult scriptChangeScene();
+	OpcodeResult scriptShowDialogue();
+	OpcodeResult scriptWalkToPosition();
+	OpcodeResult scriptWaitForWalk();
+	OpcodeResult scriptSkipWord();
+	OpcodeResult scriptClearDialogueChoices();
+	OpcodeResult scriptAddDialogueChoice();
+	OpcodeResult scriptShowDialogueChoice();
+	OpcodeResult scriptDismissPanel();
+	OpcodeResult scriptWalkToAndPickup();
+	OpcodeResult scriptSetPickupFrames();
+	OpcodeResult scriptSetupObject();
+	OpcodeResult scriptSetSkippable();
+	OpcodeResult scriptClearSkippable();
+	OpcodeResult scriptPlayAnimation();
+	OpcodeResult scriptTestPathfinding();
+	OpcodeResult scriptSetYOffset();
+	OpcodeResult scriptSetMotion();
+	OpcodeResult scriptSetOrientation();
+	OpcodeResult scriptMoveToPosition();
+	OpcodeResult scriptAddValues();
+	OpcodeResult scriptSubValues();
+	OpcodeResult scriptLoadSpecialAnim();
+	OpcodeResult scriptSetDirection();
+	OpcodeResult scriptStopAnimation();
+	OpcodeResult scriptOpenInventory();
+	OpcodeResult scriptLoadObjectAnim();
+	OpcodeResult scriptCheckObjectData();
+	OpcodeResult scriptCheckInventory();
+	OpcodeResult scriptSetSnapToTarget();
+	OpcodeResult scriptTestObjectAnimFrame();
+	OpcodeResult scriptPrintStringRight();
+	OpcodeResult scriptSetPaletteDarkness();
+	OpcodeResult scriptSetObjectShading();
+	OpcodeResult scriptSetObjectScaling();
+	OpcodeResult scriptSetHotspotOverride();
+	OpcodeResult scriptSetObjectBounds();
+	OpcodeResult scriptDismissAllPanels();
+	OpcodeResult scriptResetToSceneScript();
+	OpcodeResult scriptLoadOverlayFont();
+	OpcodeResult scriptAddOverlayTextEntry();
+	OpcodeResult scriptClearOverlayText();
+	OpcodeResult scriptFadeToBlack();
+	OpcodeResult scriptLoadPcmSound();
+	OpcodeResult scriptPlayPcmSound();
+	OpcodeResult scriptWaitForSound();
+	OpcodeResult scriptStopPcmSound();
+	OpcodeResult scriptLoadMusicSlot();
+	OpcodeResult scriptPlayMusicSlot();
+	OpcodeResult scriptStopMusicSlot();
+	OpcodeResult scriptWaitForMusic();
+	OpcodeResult scriptFreeMusicSlot();
+	OpcodeResult scriptGetObjectX();
+	OpcodeResult scriptGetObjectY();
+	OpcodeResult scriptGetObjectField8();
+	OpcodeResult scriptGetObjectOrientation();
+	OpcodeResult scriptClearActorInventory();
+	OpcodeResult scriptSetPathfindingRemap();
+	OpcodeResult scriptWaitForAdlib();
+	OpcodeResult scriptSkipUntil14();
+	OpcodeResult scriptChangeAnimation();
+	OpcodeResult scriptFrameWait();
+	OpcodeResult scriptSetPathfinding();
+	OpcodeResult scriptTestSceneAnimFrame();
+	OpcodeResult scriptEndOverlayText();
+	OpcodeResult scriptFadeFromBlack();
+	OpcodeResult scriptFreePcmSound();
 
 	inline void scriptUnimplementedOpcode(const char *source, uint16 opcode) {
 		debug("Unimplemented opcode (%s): %.2x.", source, opcode);
@@ -517,7 +429,7 @@ public:
 	Common::Point getCharPosition();
 
 	// executeOpcodes (1008:db56): the opcode dispatch loop for the current script.
-	ExecutionResult executeOpcodes();
+	OpcodeResult executeOpcodes();
 
 	// Will execute the script and any object scripts until execution should be stopped
 	// TODO: Consider if we should let the executor also figure out where to get the




More information about the Scummvm-git-logs mailing list