[Scummvm-git-logs] scummvm master -> 41e44119d72c1b7204caa0b9429a5a5e4c3bfa57

mgerhardy noreply at scummvm.org
Fri Aug 21 08:49:10 UTC 2026


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

Summary:
edf16d6572 MACS2: added v2 opcode stubs
ba68e3e2d7 MACS2: implemented a few v2 opcodes
ab8a0503cd MACS2: renamed scummui to actionbar
9214414116 MACS2: implemented action bar opcodes
6198477c1f MACS2: implemented sound related v2 opcodes
71edc0dc64 MACS2: implemented talky version support for v1
83214e6a54 MACS2: sound opcodes for v2
81ad5de3f0 MACS2: started with v2 anim opcodes
1e44c0ed29 MACS2: v2 anim loading
624a678aad MACS2: updated comment
4775cf2528 MACS2: replaced constants for screen dimensions with engine getters
7308bcc606 MACS2: replace constants with engine methods for width and height
95a613f747 MACS2: use engine getters instead of constants
41e44119d7 MACS2: some v2 resource loading


Commit: edf16d65720a66ff789ffd4ee3443a25ce686f61
    https://github.com/scummvm/scummvm/commit/edf16d65720a66ff789ffd4ee3443a25ce686f61
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-21T10:43:52+02:00

Commit Message:
MACS2: added v2 opcode stubs

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


diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index dcc818d8ed6..69bf4af70c0 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -3158,6 +3158,339 @@ const ScriptExecutor::OpcodeEntry ScriptExecutor::kV1OpcodeTable[] = {
 };
 const uint ScriptExecutor::kV1OpcodeTableSize = ARRAYSIZE(ScriptExecutor::kV1OpcodeTable);
 
+void ScriptExecutor::scriptSkipOpcodeRemainder(uint8 opcode) {
+	if (_stream != nullptr && (uint32)_stream->pos() < _expectedEndLocation) {
+		debugC(kDebugScript, "SCRIPT::%s() [skip remainder 0x%02x]", opcodeName(opcode), opcode);
+		_stream->seek(_expectedEndLocation, SEEK_SET);
+	}
+}
+
+OpcodeResult ScriptExecutor::scriptNopSkipRemainder() {
+	debugC(kDebugScript, "SCRIPT::%s() [v2 nop]", opcodeName(_lastOpcode));
+	scriptSkipOpcodeRemainder(_lastOpcode);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptPlaySfx() {
+	debugC(kDebugScript, "SCRIPT::playSfx() [stub]");
+	scriptSkipOpcodeRemainder(0x40);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptPlaySong() {
+	debugC(kDebugScript, "SCRIPT::playSong() [stub]");
+	scriptSkipOpcodeRemainder(0x44);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptStopSong() {
+	debugC(kDebugScript, "SCRIPT::stopSong() [stub]");
+	scriptSkipOpcodeRemainder(0x45);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptSetMainActor() {
+	debugC(kDebugScript, "SCRIPT::setMainActor() [stub]");
+	scriptSkipOpcodeRemainder(0x4F);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptLoadDeltaAnim() {
+	debugC(kDebugScript, "SCRIPT::loadDeltaAnim() [stub]");
+	scriptSkipOpcodeRemainder(0x50);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptPlayDeltaAnim() {
+	debugC(kDebugScript, "SCRIPT::playDeltaAnim() [stub]");
+	scriptSkipOpcodeRemainder(0x51);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptRemoveDeltaAnim() {
+	debugC(kDebugScript, "SCRIPT::removeDeltaAnim() [stub]");
+	scriptSkipOpcodeRemainder(0x52);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptSetButtonStep() {
+	debugC(kDebugScript, "SCRIPT::setButtonStep() [stub]");
+	scriptSkipOpcodeRemainder(0x53);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptTestButtonAnimFrame() {
+	debugC(kDebugScript, "SCRIPT::testButtonAnimFrame() [stub]");
+	scriptSkipOpcodeRemainder(0x54);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptScreenShot() {
+	debugC(kDebugScript, "SCRIPT::screenShot() [stub]");
+	scriptSkipOpcodeRemainder(0x55);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptWaitObjectAnimStep() {
+	debugC(kDebugScript, "SCRIPT::waitObjectAnimStep() [stub]");
+	scriptSkipOpcodeRemainder(0x56);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptWaitSpecialAnimStep() {
+	debugC(kDebugScript, "SCRIPT::waitSpecialAnimStep() [stub]");
+	scriptSkipOpcodeRemainder(0x57);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptSetObjectAdjust() {
+	debugC(kDebugScript, "SCRIPT::setObjectAdjust() [stub]");
+	scriptSkipOpcodeRemainder(0x58);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptReloadSpecialAnim() {
+	debugC(kDebugScript, "SCRIPT::reloadSpecialAnim() [stub]");
+	scriptSkipOpcodeRemainder(0x59);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptPlayDiskDelta() {
+	debugC(kDebugScript, "SCRIPT::playDiskDelta() [stub]");
+	scriptSkipOpcodeRemainder(0x5A);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptSetDiskCache() {
+	debugC(kDebugScript, "SCRIPT::setDiskCache() [stub]");
+	scriptSkipOpcodeRemainder(0x5B);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptSetMidiVolume() {
+	debugC(kDebugScript, "SCRIPT::setMidiVolume() [stub]");
+	scriptSkipOpcodeRemainder(0x5C);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptSetWaveVolume() {
+	debugC(kDebugScript, "SCRIPT::setWaveVolume() [stub]");
+	scriptSkipOpcodeRemainder(0x5D);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptLoadSpecAnimAnim() {
+	debugC(kDebugScript, "SCRIPT::loadSpecAnimAnim() [stub]");
+	scriptSkipOpcodeRemainder(0x5E);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptSetSpecAnimAnim() {
+	debugC(kDebugScript, "SCRIPT::setSpecAnimAnim() [stub]");
+	scriptSkipOpcodeRemainder(0x5F);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptClearSpecAnimAnim() {
+	debugC(kDebugScript, "SCRIPT::clearSpecAnimAnim() [stub]");
+	scriptSkipOpcodeRemainder(0x60);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptSetDeltaRange() {
+	debugC(kDebugScript, "SCRIPT::setDeltaRange() [stub]");
+	scriptSkipOpcodeRemainder(0x61);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptClearDeltaRange() {
+	debugC(kDebugScript, "SCRIPT::clearDeltaRange() [stub]");
+	scriptSkipOpcodeRemainder(0x62);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptAddDeltaSfx() {
+	debugC(kDebugScript, "SCRIPT::addDeltaSfx() [stub]");
+	scriptSkipOpcodeRemainder(0x63);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptClearDeltaSfxList() {
+	debugC(kDebugScript, "SCRIPT::clearDeltaSfxList() [stub]");
+	scriptSkipOpcodeRemainder(0x64);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptShowActionBar() {
+	debugC(kDebugScript, "SCRIPT::showActionBar() [stub]");
+	scriptSkipOpcodeRemainder(0x65);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptHideActionBar() {
+	debugC(kDebugScript, "SCRIPT::hideActionBar() [stub]");
+	scriptSkipOpcodeRemainder(0x66);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptSetCursorType() {
+	debugC(kDebugScript, "SCRIPT::setCursorType() [stub]");
+	scriptSkipOpcodeRemainder(0x67);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptCheckDeltaSpeed() {
+	debugC(kDebugScript, "SCRIPT::checkDeltaSpeed() [stub]");
+	scriptSkipOpcodeRemainder(0x68);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptLoadDistanceMask() {
+	debugC(kDebugScript, "SCRIPT::loadDistanceMask() [stub]");
+	scriptSkipOpcodeRemainder(0x69);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptLoadAreaMask() {
+	debugC(kDebugScript, "SCRIPT::loadAreaMask() [stub]");
+	scriptSkipOpcodeRemainder(0x6A);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptLoadWalkMask() {
+	debugC(kDebugScript, "SCRIPT::loadWalkMask() [stub]");
+	scriptSkipOpcodeRemainder(0x6B);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptLoadShadowMask() {
+	debugC(kDebugScript, "SCRIPT::loadShadowMask() [stub]");
+	scriptSkipOpcodeRemainder(0x6C);
+	return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::scriptTalkTo() {
+	debugC(kDebugScript, "SCRIPT::talkTo() [stub]");
+	scriptSkipOpcodeRemainder(0x6D);
+	return OpcodeResult::Continue;
+}
+
+// Script dialect v2: v1 handlers for 0x01..0x4E with audio remaps, plus 0x4F..0x6D stubs.
+const ScriptExecutor::OpcodeEntry ScriptExecutor::kV2OpcodeTable[] = {
+	{nullptr, nullptr},
+	{"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},
+	// v2: DOS PCM/music-slot opcodes become no-ops; file-based audio uses 0x40/0x44/0x45.
+	{"nop3E", &ScriptExecutor::scriptNopSkipRemainder},
+	{"nop3F", &ScriptExecutor::scriptNopSkipRemainder},
+	{"playSfx", &ScriptExecutor::scriptPlaySfx},
+	{"waitForSound", &ScriptExecutor::scriptWaitForSound},
+	{"stopPcmSound", &ScriptExecutor::scriptStopPcmSound},
+	{"nop43", &ScriptExecutor::scriptNopSkipRemainder},
+	{"playSong", &ScriptExecutor::scriptPlaySong},
+	{"stopSong", &ScriptExecutor::scriptStopSong},
+	{"nop46", &ScriptExecutor::scriptNopSkipRemainder},
+	{"nop47", &ScriptExecutor::scriptNopSkipRemainder},
+	{"getObjectX", &ScriptExecutor::scriptGetObjectX},
+	{"getObjectY", &ScriptExecutor::scriptGetObjectY},
+	{"getObjectField8", &ScriptExecutor::scriptGetObjectField8},
+	{"getObjectOrientation", &ScriptExecutor::scriptGetObjectOrientation},
+	{"clearActorInventory", &ScriptExecutor::scriptClearActorInventory},
+	{"setPathfindingRemap", &ScriptExecutor::scriptSetPathfindingRemap},
+	{"waitForAdlib", &ScriptExecutor::scriptWaitForAdlib},
+	{"setMainActor", &ScriptExecutor::scriptSetMainActor},
+	{"loadDeltaAnim", &ScriptExecutor::scriptLoadDeltaAnim},
+	{"playDeltaAnim", &ScriptExecutor::scriptPlayDeltaAnim},
+	{"removeDeltaAnim", &ScriptExecutor::scriptRemoveDeltaAnim},
+	{"setButtonStep", &ScriptExecutor::scriptSetButtonStep},
+	{"testButtonAnimFrame", &ScriptExecutor::scriptTestButtonAnimFrame},
+	{"screenShot", &ScriptExecutor::scriptScreenShot},
+	{"waitObjectAnimStep", &ScriptExecutor::scriptWaitObjectAnimStep},
+	{"waitSpecialAnimStep", &ScriptExecutor::scriptWaitSpecialAnimStep},
+	{"setObjectAdjust", &ScriptExecutor::scriptSetObjectAdjust},
+	{"reloadSpecialAnim", &ScriptExecutor::scriptReloadSpecialAnim},
+	{"playDiskDelta", &ScriptExecutor::scriptPlayDiskDelta},
+	{"setDiskCache", &ScriptExecutor::scriptSetDiskCache},
+	{"setMidiVolume", &ScriptExecutor::scriptSetMidiVolume},
+	{"setWaveVolume", &ScriptExecutor::scriptSetWaveVolume},
+	{"loadSpecAnimAnim", &ScriptExecutor::scriptLoadSpecAnimAnim},
+	{"setSpecAnimAnim", &ScriptExecutor::scriptSetSpecAnimAnim},
+	{"clearSpecAnimAnim", &ScriptExecutor::scriptClearSpecAnimAnim},
+	{"setDeltaRange", &ScriptExecutor::scriptSetDeltaRange},
+	{"clearDeltaRange", &ScriptExecutor::scriptClearDeltaRange},
+	{"addDeltaSfx", &ScriptExecutor::scriptAddDeltaSfx},
+	{"clearDeltaSfxList", &ScriptExecutor::scriptClearDeltaSfxList},
+	{"showActionBar", &ScriptExecutor::scriptShowActionBar},
+	{"hideActionBar", &ScriptExecutor::scriptHideActionBar},
+	{"setCursorType", &ScriptExecutor::scriptSetCursorType},
+	{"checkDeltaSpeed", &ScriptExecutor::scriptCheckDeltaSpeed},
+	{"loadDistanceMask", &ScriptExecutor::scriptLoadDistanceMask},
+	{"loadAreaMask", &ScriptExecutor::scriptLoadAreaMask},
+	{"loadWalkMask", &ScriptExecutor::scriptLoadWalkMask},
+	{"loadShadowMask", &ScriptExecutor::scriptLoadShadowMask},
+	{"talkTo", &ScriptExecutor::scriptTalkTo}
+};
+const uint ScriptExecutor::kV2OpcodeTableSize = ARRAYSIZE(ScriptExecutor::kV2OpcodeTable);
+
 OpcodeResult Script::ScriptExecutor::executeOpcodes() {
 	debugC(kDebugScript, "----- Scripting function entered - scene: %.2x 1014: %.2x 1012: %.2x", Scenes::instance()._currentSceneIndex, _isSceneInitRun, _repeatRunFlag);
 	_isRunningScript = true;
diff --git a/engines/macs2/scriptexecutor.h b/engines/macs2/scriptexecutor.h
index 30fd3af2ff1..2f204605db5 100644
--- a/engines/macs2/scriptexecutor.h
+++ b/engines/macs2/scriptexecutor.h
@@ -120,6 +120,13 @@ public:
 	/** Script dialect v1 opcode table (MCS / Amiga demo bytecode). */
 	static const OpcodeEntry kV1OpcodeTable[];
 	static const uint kV1OpcodeTableSize;
+	/**
+	 * Script dialect v2 opcode table (extends v1 through 0x6D).
+	 * Remaps a few audio slots and adds 0x4F..0x6D; new handlers are stubs
+	 * that consume the length-prefixed payload via scriptSkipOpcodeRemainder().
+	 */
+	static const OpcodeEntry kV2OpcodeTable[];
+	static const uint kV2OpcodeTableSize;
 
 private:
 #ifdef DEMACS2
@@ -204,6 +211,48 @@ public:
 	OpcodeResult scriptFadeFromBlack();
 	OpcodeResult scriptFreePcmSound();
 
+	/** Seek to the length-prefixed end of the current opcode payload. */
+	void scriptSkipOpcodeRemainder(uint8 opcode);
+
+	// Dialect v2: remapped audio slots (v1 PCM/music-slot opcodes are NOPs here).
+	OpcodeResult scriptNopSkipRemainder();
+	OpcodeResult scriptPlaySfx();
+	OpcodeResult scriptPlaySong();
+	OpcodeResult scriptStopSong();
+
+	// Dialect v2: extended opcodes 0x4F..0x6D (stubs until backends exist).
+	OpcodeResult scriptSetMainActor();
+	OpcodeResult scriptLoadDeltaAnim();
+	OpcodeResult scriptPlayDeltaAnim();
+	OpcodeResult scriptRemoveDeltaAnim();
+	OpcodeResult scriptSetButtonStep();
+	OpcodeResult scriptTestButtonAnimFrame();
+	OpcodeResult scriptScreenShot();
+	OpcodeResult scriptWaitObjectAnimStep();
+	OpcodeResult scriptWaitSpecialAnimStep();
+	OpcodeResult scriptSetObjectAdjust();
+	OpcodeResult scriptReloadSpecialAnim();
+	OpcodeResult scriptPlayDiskDelta();
+	OpcodeResult scriptSetDiskCache();
+	OpcodeResult scriptSetMidiVolume();
+	OpcodeResult scriptSetWaveVolume();
+	OpcodeResult scriptLoadSpecAnimAnim();
+	OpcodeResult scriptSetSpecAnimAnim();
+	OpcodeResult scriptClearSpecAnimAnim();
+	OpcodeResult scriptSetDeltaRange();
+	OpcodeResult scriptClearDeltaRange();
+	OpcodeResult scriptAddDeltaSfx();
+	OpcodeResult scriptClearDeltaSfxList();
+	OpcodeResult scriptShowActionBar();
+	OpcodeResult scriptHideActionBar();
+	OpcodeResult scriptSetCursorType();
+	OpcodeResult scriptCheckDeltaSpeed();
+	OpcodeResult scriptLoadDistanceMask();
+	OpcodeResult scriptLoadAreaMask();
+	OpcodeResult scriptLoadWalkMask();
+	OpcodeResult scriptLoadShadowMask();
+	OpcodeResult scriptTalkTo();
+
 	inline void scriptUnimplementedOpcode(const char *source, uint16 opcode) {
 		debug("Unimplemented opcode (%s): %.2x.", source, opcode);
 	}


Commit: ba68e3e2d7d8149b0984d966bf299ff56e71bc23
    https://github.com/scummvm/scummvm/commit/ba68e3e2d7d8149b0984d966bf299ff56e71bc23
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-21T10:43:52+02:00

Commit Message:
MACS2: implemented a few v2 opcodes

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


diff --git a/engines/macs2/gameobjects.h b/engines/macs2/gameobjects.h
index 9864c6d7392..b3f2a38ccff 100644
--- a/engines/macs2/gameobjects.h
+++ b/engines/macs2/gameobjects.h
@@ -123,6 +123,9 @@ public:
 	// this factor scales how much that height displaces the object upward
 	// when drawn. 0 = no vertical offset. 100 = full elevation offset.
 	uint16 _verticalOffsetScale = 0;
+	// Dialect v2 setObjectAdjust: runtime object adjust pair.
+	uint16 _objectAdjust1 = 0;
+	uint16 _objectAdjust2 = 0;
 	// Runtime +0x217: frame index during pickup animation at which the item is grabbed
 	uint16 _pickupFrameStart = 0;
 	// Runtime +0x219: frame index at which pickup animation completes
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 69bf4af70c0..02bd97720e5 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -20,8 +20,10 @@
  */
 
 #include "macs2/scriptexecutor.h"
+#include "audio/mixer.h"
 #include "common/debug.h"
 #include "common/memstream.h"
+#include "common/system.h"
 #include "macs2/amiga_archive.h"
 #include "macs2/amiga_decode.h"
 #include "macs2/debugtools.h"
@@ -3190,8 +3192,31 @@ OpcodeResult ScriptExecutor::scriptStopSong() {
 }
 
 OpcodeResult ScriptExecutor::scriptSetMainActor() {
-	debugC(kDebugScript, "SCRIPT::setMainActor() [stub]");
-	scriptSkipOpcodeRemainder(0x4F);
+	const uint32 objectID = scriptReadValue32() - 0x400;
+	debugC(kDebugScript, "SCRIPT::setMainActor(objectID=%u)", objectID);
+
+	clearScriptError();
+	if (objectID < 1 || objectID > 0x200) {
+		setScriptError(2);
+		return OpcodeResult::Continue;
+	}
+	GameObject *object = GameObjects::getObjectByIndex(objectID);
+	if (object == nullptr) {
+		setScriptError(0x19);
+		return OpcodeResult::Continue;
+	}
+	if (object->_dataOffset == 0) {
+		setScriptError(2);
+		return OpcodeResult::Continue;
+	}
+
+	Scenes::instance()._currentActorIndex = objectID;
+	View1 *currentView = (View1 *)_engine->findView("View1");
+	if (currentView != nullptr) {
+		currentView->refreshProtagonistInventoryAfterLoad((uint16)objectID);
+		if (currentView->_inventorySource != nullptr)
+			currentView->setInventorySource(currentView->_inventorySource);
+	}
 	return OpcodeResult::Continue;
 }
 
@@ -3226,7 +3251,7 @@ OpcodeResult ScriptExecutor::scriptTestButtonAnimFrame() {
 }
 
 OpcodeResult ScriptExecutor::scriptScreenShot() {
-	debugC(kDebugScript, "SCRIPT::screenShot() [stub]");
+	debugC(kDebugScript, "SCRIPT::screenShot() [nop]");
 	scriptSkipOpcodeRemainder(0x55);
 	return OpcodeResult::Continue;
 }
@@ -3244,8 +3269,28 @@ OpcodeResult ScriptExecutor::scriptWaitSpecialAnimStep() {
 }
 
 OpcodeResult ScriptExecutor::scriptSetObjectAdjust() {
-	debugC(kDebugScript, "SCRIPT::setObjectAdjust() [stub]");
-	scriptSkipOpcodeRemainder(0x58);
+	const uint32 objectID = scriptReadValue32() - 0x400;
+	const uint16 adjust1 = scriptReadValue16();
+	const uint16 adjust2 = scriptReadValue16();
+	debugC(kDebugScript, "SCRIPT::setObjectAdjust(objectID=%u, adjust1=%u, adjust2=%u)",
+		   objectID, adjust1, adjust2);
+
+	clearScriptError();
+	if (objectID < 1 || objectID > 0x200) {
+		setScriptError(2);
+		return OpcodeResult::Continue;
+	}
+	GameObject *object = GameObjects::getObjectByIndex(objectID);
+	if (object == nullptr) {
+		setScriptError(0x19);
+		return OpcodeResult::Continue;
+	}
+	if (object->_dataOffset == 0) {
+		setScriptError(2);
+		return OpcodeResult::Continue;
+	}
+	object->_objectAdjust1 = adjust1;
+	object->_objectAdjust2 = adjust2;
 	return OpcodeResult::Continue;
 }
 
@@ -3262,20 +3307,40 @@ OpcodeResult ScriptExecutor::scriptPlayDiskDelta() {
 }
 
 OpcodeResult ScriptExecutor::scriptSetDiskCache() {
-	debugC(kDebugScript, "SCRIPT::setDiskCache() [stub]");
+	const uint16 cacheSetting = scriptReadValue16();
+	debugC(kDebugScript, "SCRIPT::setDiskCache(setting=%u) [nop]", cacheSetting);
 	scriptSkipOpcodeRemainder(0x5B);
 	return OpcodeResult::Continue;
 }
 
 OpcodeResult ScriptExecutor::scriptSetMidiVolume() {
-	debugC(kDebugScript, "SCRIPT::setMidiVolume() [stub]");
-	scriptSkipOpcodeRemainder(0x5C);
+	const uint16 volumePercent = scriptReadValue16();
+	debugC(kDebugScript, "SCRIPT::setMidiVolume(volume=%u)", volumePercent);
+
+	clearScriptError();
+	if (volumePercent > 100) {
+		setScriptError(0x30);
+		return OpcodeResult::Continue;
+	}
+	// Maps 0=loud..100=silent to OPL attenuation (0..0x3F).
+	_musicControlVolume = (uint16)((100 - volumePercent) * 0x3F / 100);
+	if (_engine->getMusic() != nullptr)
+		_engine->getMusic()->setVolume(_engine->scaledMusicVolume(_musicControlVolume));
 	return OpcodeResult::Continue;
 }
 
 OpcodeResult ScriptExecutor::scriptSetWaveVolume() {
-	debugC(kDebugScript, "SCRIPT::setWaveVolume() [stub]");
-	scriptSkipOpcodeRemainder(0x5D);
+	const uint16 volumePercent = scriptReadValue16();
+	debugC(kDebugScript, "SCRIPT::setWaveVolume(volume=%u)", volumePercent);
+
+	clearScriptError();
+	if (volumePercent > 100) {
+		setScriptError(0x30);
+		return OpcodeResult::Continue;
+	}
+	const int mixerVolume = volumePercent * 255 / 100;
+	g_system->getMixer()->setVolumeForSoundType(Audio::Mixer::kSFXSoundType, mixerVolume);
+	g_system->getMixer()->setVolumeForSoundType(Audio::Mixer::kSpeechSoundType, mixerVolume);
 	return OpcodeResult::Continue;
 }
 
@@ -3316,26 +3381,42 @@ OpcodeResult ScriptExecutor::scriptAddDeltaSfx() {
 }
 
 OpcodeResult ScriptExecutor::scriptClearDeltaSfxList() {
-	debugC(kDebugScript, "SCRIPT::clearDeltaSfxList() [stub]");
+	debugC(kDebugScript, "SCRIPT::clearDeltaSfxList() [nop]");
 	scriptSkipOpcodeRemainder(0x64);
 	return OpcodeResult::Continue;
 }
 
 OpcodeResult ScriptExecutor::scriptShowActionBar() {
-	debugC(kDebugScript, "SCRIPT::showActionBar() [stub]");
-	scriptSkipOpcodeRemainder(0x65);
+	debugC(kDebugScript, "SCRIPT::showActionBar()");
+	View1 *currentView = (View1 *)_engine->findView("View1");
+	if (currentView != nullptr) {
+		currentView->openScriptActionBar(
+			Common::Point(_engine->screenWidth() / 2, _engine->gameHeight() / 2), _cursorMode);
+	}
 	return OpcodeResult::Continue;
 }
 
 OpcodeResult ScriptExecutor::scriptHideActionBar() {
-	debugC(kDebugScript, "SCRIPT::hideActionBar() [stub]");
-	scriptSkipOpcodeRemainder(0x66);
+	debugC(kDebugScript, "SCRIPT::hideActionBar()");
+	View1 *currentView = (View1 *)_engine->findView("View1");
+	if (currentView != nullptr) {
+		MouseMode savedMode = _cursorMode;
+		currentView->closeScriptActionBar(savedMode);
+		_cursorMode = savedMode;
+	}
 	return OpcodeResult::Continue;
 }
 
 OpcodeResult ScriptExecutor::scriptSetCursorType() {
-	debugC(kDebugScript, "SCRIPT::setCursorType() [stub]");
-	scriptSkipOpcodeRemainder(0x67);
+	const uint16 cursorType = scriptReadValue16();
+	debugC(kDebugScript, "SCRIPT::setCursorType(type=0x%x)", cursorType);
+
+	if (cursorType > 0x12 && cursorType < 0x17) {
+		_engine->setCursorMode((MouseMode)cursorType);
+		View1 *currentView = (View1 *)_engine->findView("View1");
+		if (currentView != nullptr)
+			currentView->updateCursor();
+	}
 	return OpcodeResult::Continue;
 }
 
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index c5764a8ebce..bd6eab42b1f 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -874,6 +874,24 @@ void View1::openMainMenu(Common::Point clickedPosition) {
 	redraw();
 }
 
+void View1::openScriptActionBar(const Common::Point &position, Script::MouseMode restoreCursorMode) {
+	if (_uiPanelState != kUiPanelNone || hasScummVerbUI())
+		return;
+	openMainMenu(position);
+	g_engine->setCursorMode(restoreCursorMode);
+	updateCursor();
+}
+
+void View1::closeScriptActionBar(Script::MouseMode &outSavedCursorMode) {
+	if (_uiPanelState != kUiPanelActionBar)
+		return;
+	outSavedCursorMode = g_engine->_scriptExecutor->_cursorMode;
+	_uiPanelState = kUiPanelNone;
+	_clickedButtonIndex = 0;
+	_uiBackgroundRestorePending = false;
+	redraw();
+}
+
 void View1::enterMapMode() {
 	// Binary handleInput end-block when scene+0x61db != 0 (1008:e8bf): fade, load map
 	// from scene+0x5DDB (_mapSceneOffsets[0]), set cursor 0x18 (PanelUse).
diff --git a/engines/macs2/view1.h b/engines/macs2/view1.h
index 41592ce2574..77121774921 100644
--- a/engines/macs2/view1.h
+++ b/engines/macs2/view1.h
@@ -516,6 +516,10 @@ public:
 	GameObject *getClickedInventoryItem(const Common::Point &p);
 
 	void openMainMenu(Common::Point clickedPosition);
+	/** Script-driven action bar open; restores the given cursor after opening. */
+	void openScriptActionBar(const Common::Point &position, Script::MouseMode restoreCursorMode);
+	/** Script-driven action bar close; writes the cursor mode that was active. */
+	void closeScriptActionBar(Script::MouseMode &outSavedCursorMode);
 	void enterMapMode();
 
 	// Binary openActionBarAtPosition (1008:3fba): stores button hit rects at panel+4+col*(btnW+4).


Commit: ab8a0503cdcab96df6f0ee9c35da713ba15e1d54
    https://github.com/scummvm/scummvm/commit/ab8a0503cdcab96df6f0ee9c35da713ba15e1d54
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-21T10:43:52+02:00

Commit Message:
MACS2: renamed scummui to actionbar

Changed paths:
  A engines/macs2/actionbar.cpp
  A engines/macs2/actionbar.h
  R engines/macs2/scummui.cpp
  R engines/macs2/scummui.h
    engines/macs2/macs2.cpp
    engines/macs2/macs2.h
    engines/macs2/module.mk
    engines/macs2/saveload.cpp
    engines/macs2/scriptexecutor.cpp
    engines/macs2/scriptexecutor.h
    engines/macs2/view1.cpp
    engines/macs2/view1.h


diff --git a/engines/macs2/actionbar.cpp b/engines/macs2/actionbar.cpp
new file mode 100644
index 00000000000..9d2cb641823
--- /dev/null
+++ b/engines/macs2/actionbar.cpp
@@ -0,0 +1,933 @@
+/* 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/actionbar.h"
+
+#include "common/debug.h"
+#include "common/system.h"
+#include "engines/savestate.h"
+#include "gui/message.h"
+#include "macs2/detection.h"
+#include "macs2/gameobjects.h"
+#include "macs2/macs2.h"
+#include "macs2/music.h"
+#include "macs2/view1.h"
+
+namespace Macs2 {
+
+static Common::String getObjectDisplayName(const GameObject *obj) {
+	if (!obj)
+		return Common::String();
+
+	const GameObjects &objects = GameObjects::instance();
+	if (obj->_index < objects._objectNames.size() && !objects._objectNames[obj->_index].empty())
+		return objects._objectNames[obj->_index];
+
+	return Common::String();
+}
+
+const ActionBar::VerbDef ActionBar::kVerbs[4] = {
+	{"Walk", Script::MouseMode::Walk},
+	{"Look", Script::MouseMode::Look},
+	{"Use", Script::MouseMode::Use},
+	{"Talk", Script::MouseMode::Talk}
+};
+
+ActionBar::ActionBar(View1 *view)
+	: _view(view), _activeVerbIndex(0), _hoveredVerb(-1), _hoveredItemIndex(-1), _hoveredScrollButton(-1),
+	  _inventoryScrollOffset(0) {
+}
+
+bool ActionBar::isPointInUI(const Common::Point &pos) const {
+	if (useNativeSkin()) {
+		if (g_engine->_menuMode == 0)
+			return false;
+		return pos.y >= (int16)g_engine->_panelTopY;
+	}
+	return pos.y >= kUITop;
+}
+
+void ActionBar::syncInventory() {
+	rebuildProtagonistItems();
+	const int maxOffset = MAX(0, (int)_protagonistItems.size() - kInvCols * kInvRows);
+	if (_inventoryScrollOffset > maxOffset)
+		_inventoryScrollOffset = maxOffset;
+	if (_inventoryScrollOffset < 0)
+		_inventoryScrollOffset = 0;
+}
+
+void ActionBar::rebuildProtagonistItems() {
+	_protagonistItems.clear();
+
+	if (_view->isInventorySourceProtagonist()) {
+		_protagonistItems = _view->_inventoryItems;
+		return;
+	}
+
+	const uint16 invScene = Scenes::instance()._currentActorIndex + 0x400;
+	for (GameObject *obj : GameObjects::instance()._objects) {
+		if (obj && obj->_sceneIndex == invScene)
+			_protagonistItems.push_back(obj);
+	}
+}
+
+void ActionBar::resetInventoryAfterLoad() {
+	_inventoryScrollOffset = 0;
+	_hoveredItemIndex = -1;
+	_hoveredScrollButton = -1;
+	_hoveredVerb = -1;
+	_sentenceObject.clear();
+	rebuildProtagonistItems();
+	syncActiveVerbFromCursorMode();
+
+	if (_view->_activeInventoryItem) {
+		bool inInventory = false;
+		for (GameObject *obj : _protagonistItems) {
+			if (obj == _view->_activeInventoryItem) {
+				inInventory = true;
+				break;
+			}
+		}
+		if (!inInventory) {
+			_view->_activeInventoryItem = nullptr;
+			g_engine->_scriptExecutor->_interactedInventoryItemId = 0;
+		}
+	}
+}
+
+void ActionBar::syncActiveVerbFromCursorMode() {
+	const Script::MouseMode mode = g_engine->_scriptExecutor->_cursorMode;
+	if (mode == Script::MouseMode::UseInventory) {
+		_activeVerbIndex = 2;
+		return;
+	}
+
+	for (int i = 0; i < 4; i++) {
+		if (kVerbs[i].mode == mode) {
+			_activeVerbIndex = i;
+			return;
+		}
+	}
+}
+
+bool ActionBar::useScummSkin() const {
+	return g_engine->enhancementEnabled(kEnhUIUX);
+}
+
+bool ActionBar::useNativeSkin() const {
+	return !useScummSkin() && g_engine->hasNativeHudAssets();
+}
+
+int ActionBar::gameAreaBottomY() const {
+	if (useNativeSkin())
+		return (int)g_engine->_panelTopY;
+	return kGameHeight;
+}
+
+void ActionBar::draw(Graphics::ManagedSurface &s) {
+	if (useNativeSkin())
+		drawNative(s);
+	else if (useScummSkin())
+		drawScumm(s);
+}
+
+void ActionBar::drawScumm(Graphics::ManagedSurface &s) {
+	syncActiveVerbFromCursorMode();
+	rebuildProtagonistItems();
+	_view->drawBorderSide(Common::Point(0, kUITop), Common::Point(kScreenWidth, kUIHeight), s);
+	drawSentenceLine(s);
+	drawVerbBar(s);
+	drawInventoryStrip(s);
+}
+
+void ActionBar::drawUIButton(const Common::Rect &rect, bool pressed, Graphics::ManagedSurface &s) {
+	_view->drawBorderSide(Common::Point(rect.left, rect.top), Common::Point(rect.width(), rect.height()), s);
+	const View1::BorderStyle &style = pressed ? View1::kBorderPressed : View1::kBorderRaised;
+	_view->drawNinePatchBorder(Common::Point(rect.left, rect.top), Common::Point(rect.width(), rect.height()),
+							   style, false, false, s);
+}
+
+void ActionBar::drawSentenceLine(Graphics::ManagedSurface &s) {
+	Common::String sentence;
+	const Script::MouseMode mode = g_engine->_scriptExecutor->_cursorMode;
+
+	if (mode == Script::MouseMode::UseInventory && _view->_activeInventoryItem) {
+		sentence = "Use";
+		const Common::String itemName = getObjectDisplayName(_view->_activeInventoryItem);
+		if (!itemName.empty())
+			sentence += " " + itemName;
+	} else if (_activeVerbIndex >= 0 && _activeVerbIndex < 4) {
+		sentence = kVerbs[_activeVerbIndex].label;
+	}
+	if (!_sentenceObject.empty()) {
+		if (!sentence.empty()) {
+			if (mode == Script::MouseMode::UseInventory)
+				sentence += " with ";
+			else
+				sentence += " ";
+		}
+		sentence += _sentenceObject;
+	}
+
+	if (sentence.empty())
+		return;
+
+	const bool usePanelFont = g_engine->numPanelGlyphs > 0;
+	const GlyphData *font = usePanelFont ? g_engine->_panelGlyphs : g_engine->_glyphs;
+	const uint16 fontCount = usePanelFont ? g_engine->numPanelGlyphs : g_engine->numGlyphs;
+	const int glyphH = usePanelFont ? g_engine->maxPanelGlyphHeight : g_engine->maxGlyphHeight;
+	if (usePanelFont)
+		sentence.toUppercase();
+	const int textY = kUITop + MAX(0, (kSentenceH - glyphH) / 2);
+	const int textX = MAX(0, (kScreenWidth - _view->measureStringWithFont(sentence, font, fontCount)) / 2);
+	_view->renderStringWithFontTo(textX, textY, sentence, font, fontCount, s);
+}
+
+void ActionBar::drawVerbBar(Graphics::ManagedSurface &s) {
+	for (int i = 0; i < 4; i++) {
+		const Common::Rect r = getVerbRect(i);
+		const bool isActive = (i == _activeVerbIndex);
+		const bool isHovered = (i == _hoveredVerb);
+
+		drawUIButton(r, isActive || isHovered, s);
+
+		const int textX = r.left + (r.width() - (int)strlen(kVerbs[i].label) * 6) / 2;
+		const int textY = r.top + (r.height() - (int)g_engine->maxGlyphHeight) / 2;
+		_view->renderStringTo(textX, textY, kVerbs[i].label, s);
+	}
+}
+
+int ActionBar::getScrollButtonWidth() const {
+	uint16 maxW = 0;
+	const Common::Array<uint16> &indices = g_engine->inventoryIconIndices;
+	for (int i = 2; i <= 3 && i < (int)indices.size(); i++) {
+		const int imgIdx = (int)indices[i] - 1;
+		if (imgIdx >= 0 && imgIdx < (int)g_engine->_imageResources.size())
+			maxW = MAX(maxW, g_engine->_imageResources[imgIdx]._width);
+	}
+	return MAX(22, (int)maxW + 6);
+}
+
+int ActionBar::getInvArrowX() const {
+	return kInvX;
+}
+
+void ActionBar::drawScrollButton(Graphics::ManagedSurface &s, const Common::Rect &rect,
+							   int iconResourceIndex, bool hovered) {
+	drawUIButton(rect, hovered, s);
+
+	if (iconResourceIndex < 0 || iconResourceIndex >= (int)g_engine->_imageResources.size())
+		return;
+
+	const AnimFrame &frame = g_engine->_imageResources[iconResourceIndex];
+	if (frame._data.empty() || frame._width == 0 || frame._height == 0)
+		return;
+
+	int iconX = rect.left + (rect.width() - frame._width) / 2;
+	int iconY = rect.top + (rect.height() - frame._height) / 2;
+	if (hovered) {
+		iconX++;
+		iconY++;
+	}
+	_view->drawSprite(iconX, iconY, frame, s, false);
+}
+
+void ActionBar::drawInventoryStrip(Graphics::ManagedSurface &s) {
+	const Common::Array<uint16> &indices = g_engine->inventoryIconIndices;
+	const int upIconIdx = (indices.size() > 2) ? (int)indices[2] - 1 : -1;
+	const int downIconIdx = (indices.size() > 3) ? (int)indices[3] - 1 : -1;
+
+	drawScrollButton(s, getInvScrollLeftRect(), upIconIdx, _hoveredScrollButton == 0);
+	drawScrollButton(s, getInvScrollRightRect(), downIconIdx, _hoveredScrollButton == 1);
+
+	const Common::Array<GameObject *> items = getProtagonistItems();
+	const int maxVisible = kInvCols * kInvRows;
+	for (int i = 0; i < maxVisible; i++) {
+		const Common::Rect r = getInvItemRect(i);
+		drawUIButton(r, true, s);
+
+		const int itemIdx = _inventoryScrollOffset + i;
+		if (itemIdx >= (int)items.size())
+			continue;
+
+		const bool isHovered = (i == _hoveredItemIndex);
+		const bool isActive = (_view->_activeInventoryItem == items[itemIdx]);
+
+		AnimFrame *icon = _view->getInventoryIcon(items[itemIdx]);
+		if (icon && !icon->_data.empty()) {
+			_view->drawSpriteFitted(r, *icon, s, kInvIconInset);
+		}
+		delete icon;
+
+		if (isActive) {
+			_view->renderStringTo(r.left + 1, r.top, "*", s);
+		} else if (isHovered) {
+			_view->renderStringTo(r.left + 1, r.top, ".", s);
+		}
+	}
+}
+
+Common::Array<GameObject *> ActionBar::getProtagonistItems() const {
+	return _protagonistItems;
+}
+
+bool ActionBar::handleClick(const Common::Point &pos, bool scriptsRunning) {
+	if (useNativeSkin())
+		return handleClickNative(pos);
+	if (useScummSkin())
+		return handleClickScumm(pos, scriptsRunning);
+	return false;
+}
+
+bool ActionBar::handleClickScumm(const Common::Point &pos, bool scriptsRunning) {
+	for (int i = 0; i < 4; i++) {
+		if (getVerbRect(i).contains(pos)) {
+			_activeVerbIndex = i;
+			g_engine->setCursorMode(kVerbs[i].mode);
+			_view->_activeInventoryItem = nullptr;
+			g_engine->_scriptExecutor->_interactedInventoryItemId = 0;
+			_view->updateCursor();
+			return true;
+		}
+	}
+
+	if (getInvScrollLeftRect().contains(pos)) {
+		if (_inventoryScrollOffset > 0)
+			_inventoryScrollOffset -= kInvCols * kInvRows;
+		return true;
+	}
+	if (getInvScrollRightRect().contains(pos)) {
+		const int maxItems = (int)getProtagonistItems().size();
+		const int maxVisible = kInvCols * kInvRows;
+		if (_inventoryScrollOffset + maxVisible < maxItems)
+			_inventoryScrollOffset += maxVisible;
+		return true;
+	}
+
+	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++) {
+		const int itemIdx = _inventoryScrollOffset + i;
+		if (itemIdx >= (int)items.size())
+			break;
+
+		if (!getInvItemRect(i).contains(pos))
+			continue;
+
+		GameObject *item = items[itemIdx];
+
+		if (mode == Script::MouseMode::Look) {
+			g_engine->_scriptExecutor->_interactedObjectID = 0x400 + item->_index;
+			g_engine->_scriptExecutor->_interactedInventoryItemId = 0;
+			_view->_pendingPanelRequest = View1::kPanelRequestInventory;
+			g_engine->runScriptExecutor(false);
+			_view->_pendingPanelRequest = View1::kPanelRequestNone;
+		} else if (mode == Script::MouseMode::Use) {
+			_view->_activeInventoryItem = item;
+			g_engine->_scriptExecutor->_interactedInventoryItemId = 0x400 + item->_index;
+			AnimFrame *icon = _view->getInventoryIcon(item);
+			if (icon != nullptr) {
+				const int cursorSlot = (int)Script::MouseMode::UseInventory - 1;
+				g_engine->_imageResources[cursorSlot] = *icon;
+				delete icon;
+			}
+			g_engine->setCursorMode(Script::MouseMode::UseInventory);
+			_view->updateCursor();
+		} else if (mode == Script::MouseMode::UseInventory && _view->_activeInventoryItem) {
+			g_engine->_scriptExecutor->_interactedObjectID = 0x400 + _view->_activeInventoryItem->_index;
+			g_engine->_scriptExecutor->_interactedInventoryItemId = 0x400 + item->_index;
+			_view->_pendingPanelRequest = View1::kPanelRequestInventory;
+			g_engine->runScriptExecutor(false);
+			_view->_pendingPanelRequest = View1::kPanelRequestNone;
+			_view->_activeInventoryItem = nullptr;
+			g_engine->setCursorMode(Script::MouseMode::Use);
+			_view->updateCursor();
+			syncInventory();
+		}
+		return true;
+	}
+
+	return true;
+}
+
+void ActionBar::handleMouseMove(const Common::Point &pos) {
+	if (useNativeSkin()) {
+		handleMouseMoveNative(pos);
+		return;
+	}
+	if (useScummSkin())
+		handleMouseMoveScumm(pos);
+}
+
+void ActionBar::handleMouseMoveScumm(const Common::Point &pos) {
+	const int oldHoveredVerb = _hoveredVerb;
+	const int oldHoveredItemIndex = _hoveredItemIndex;
+	const int oldHoveredScrollButton = _hoveredScrollButton;
+
+	_hoveredVerb = -1;
+	_hoveredItemIndex = -1;
+	_hoveredScrollButton = -1;
+	clearSentenceObject();
+
+	for (int i = 0; i < 4; i++) {
+		if (getVerbRect(i).contains(pos)) {
+			_hoveredVerb = i;
+			break;
+		}
+	}
+
+	if (_hoveredVerb < 0) {
+		if (getInvScrollLeftRect().contains(pos)) {
+			_hoveredScrollButton = 0;
+		} else if (getInvScrollRightRect().contains(pos)) {
+			_hoveredScrollButton = 1;
+		} else {
+			const Common::Array<GameObject *> items = getProtagonistItems();
+			const int maxVisible = kInvCols * kInvRows;
+			for (int i = 0; i < maxVisible; i++) {
+				const int itemIdx = _inventoryScrollOffset + i;
+				if (itemIdx >= (int)items.size())
+					break;
+				if (getInvItemRect(i).contains(pos)) {
+					_hoveredItemIndex = i;
+					break;
+				}
+			}
+		}
+	}
+
+	if (oldHoveredVerb != _hoveredVerb || oldHoveredItemIndex != _hoveredItemIndex ||
+		oldHoveredScrollButton != _hoveredScrollButton) {
+		_view->presentFrame();
+	}
+}
+
+void ActionBar::updateSentenceLine(const Common::String &objectName) {
+	_sentenceObject = objectName;
+}
+
+void ActionBar::clearSentenceObject() {
+	_sentenceObject.clear();
+}
+
+Common::Rect ActionBar::getVerbRect(int index) const {
+	const int col = index % kVerbCols;
+	const int row = index / kVerbCols;
+	const int x = col * kVerbW;
+	const int y = kVerbY + row * kVerbH;
+	return Common::Rect(x, y, x + kVerbW, y + kVerbH);
+}
+
+Common::Rect ActionBar::getInvItemRect(int index) const {
+	const int col = index % kInvCols;
+	const int row = index / kInvCols;
+	const int scrollW = getScrollButtonWidth();
+	const int x = getInvArrowX() + scrollW + col * kInvItemW;
+	const int y = kVerbY + row * kInvItemH;
+	return Common::Rect(x, y, x + kInvItemW, y + kInvItemH);
+}
+
+Common::Rect ActionBar::getInvScrollLeftRect() const {
+	const int scrollW = getScrollButtonWidth();
+	return Common::Rect(getInvArrowX(), kVerbY, getInvArrowX() + scrollW, kVerbY + kVerbH * kVerbRows);
+}
+
+bool ActionBar::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 ActionBar::getInvScrollRightRect() const {
+	const int scrollW = getScrollButtonWidth();
+	const int x = getInvArrowX() + scrollW + kInvCols * kInvItemW;
+	return Common::Rect(x, kVerbY, x + scrollW, kVerbY + kVerbH * kVerbRows);
+}
+
+void ActionBar::refreshSaveSlotNames() {
+	g_engine->_saveSlotNames.clear();
+	const uint16 visible = g_engine->_hudTextLayout[3] ? g_engine->_hudTextLayout[3] : 9;
+	const uint16 start = g_engine->_saveListScroll == 0 ? 1 : g_engine->_saveListScroll;
+	for (uint16 i = 0; i < visible; i++) {
+		const int slot = (int)start - 1 + (int)i;
+		SaveStateDescriptor desc = g_engine->getMetaEngine()->querySaveMetaInfos(
+			g_engine->getGameId().c_str(), slot);
+		if (desc.getSaveSlot() != -1 && !desc.getDescription().empty())
+			g_engine->_saveSlotNames.push_back(desc.getDescription());
+		else
+			g_engine->_saveSlotNames.push_back(Common::String::format("--- Slot %d ---", slot + 1));
+	}
+}
+
+Common::String ActionBar::buildNativeSentenceLine() const {
+	const Script::MouseMode mode = g_engine->_scriptExecutor->_cursorMode;
+	const char *verb = "Walk";
+	if (mode == Script::MouseMode::Look)
+		verb = "Look";
+	else if (mode == Script::MouseMode::Talk)
+		verb = "Talk";
+	else if (mode == Script::MouseMode::Use || mode == Script::MouseMode::UseInventory)
+		verb = "Use";
+	else if (mode == Script::MouseMode::PanelCursor || mode == Script::MouseMode::Disabled)
+		return Common::String();
+
+	Common::String line(verb);
+	if (_view->_activeInventoryItem != nullptr) {
+		const Common::String itemName = getObjectDisplayName(_view->_activeInventoryItem);
+		if (!itemName.empty()) {
+			line += " ";
+			line += itemName;
+			line += " with";
+		}
+	}
+
+	uint16 hoverId = 0;
+	const Common::Point mouse = g_system->getEventManager()->getMousePos();
+	if (!isPointInUI(mouse)) {
+		hoverId = _view->getHitObjectID(mouse);
+		if (hoverId == 0)
+			hoverId = g_engine->getHotspotAtPoint(mouse);
+	}
+	if (hoverId >= 0x400) {
+		const uint16 objIndex = hoverId - 0x400;
+		if (objIndex < GameObjects::instance()._objectNames.size() &&
+			!GameObjects::instance()._objectNames[objIndex].empty()) {
+			line += " ";
+			line += GameObjects::instance()._objectNames[objIndex];
+		}
+	} else if (_view->_hoverHotspotId != 0 &&
+			   _view->_hoverHotspotId < GameObjects::instance()._objectNames.size() &&
+			   !GameObjects::instance()._objectNames[_view->_hoverHotspotId].empty()) {
+		line += " ";
+		line += GameObjects::instance()._objectNames[_view->_hoverHotspotId];
+	}
+	return line;
+}
+
+const HudButton *ActionBar::findHudButtonAt(const Common::Point &pos, int *outIndex) const {
+	if (outIndex)
+		*outIndex = -1;
+	if (!isPointInUI(pos))
+		return nullptr;
+	const uint16 panelTop = g_engine->_panelTopY;
+	const uint16 menuMode = g_engine->_menuMode;
+	for (uint i = 0; i < g_engine->_hudButtons.size(); i++) {
+		const HudButton &btn = g_engine->_hudButtons[i];
+		if (btn.menuId != menuMode || btn.frame._data.empty())
+			continue;
+		const AnimFrame &hitFrame = btn.frame;
+		const Common::Point local(pos.x - btn.x, pos.y - (int)panelTop - btn.y);
+		if (local.x < 0 || local.y < 0 || local.x >= hitFrame._width || local.y >= hitFrame._height)
+			continue;
+		if (!hitFrame.pixelHit(local) &&
+			(btn.activeFrame._data.empty() || !btn.activeFrame.pixelHit(local)) &&
+			(btn.hoverFrame._data.empty() || !btn.hoverFrame.pixelHit(local)))
+			continue;
+		if (outIndex)
+			*outIndex = (int)i;
+		return &btn;
+	}
+	return nullptr;
+}
+
+void ActionBar::drawNative(Graphics::ManagedSurface &s) {
+	if (!g_engine->hasNativeHudAssets())
+		return;
+	if (g_engine->_menuMode == 0)
+		return;
+
+	const uint16 panelTop = g_engine->_panelTopY;
+	const uint16 menuMode = g_engine->_menuMode;
+	const int megaIndex = (int)menuMode - 1;
+	if (megaIndex >= 0 && megaIndex < 6 && g_engine->_hudMegapicLoaded[megaIndex]) {
+		const Graphics::ManagedSurface &mega = g_engine->_hudMegapics[megaIndex];
+		s.blitFrom(mega, Common::Point(0, panelTop));
+	} else {
+		s.fillRect(Common::Rect(0, panelTop, s.w, panelTop + g_engine->_panelHeight), 0);
+	}
+
+	for (const HudButton &btn : g_engine->_hudButtons) {
+		if (btn.menuId != menuMode || btn.frame._data.empty())
+			continue;
+		const AnimFrame *frame = &btn.frame;
+		const Script::MouseMode mode = g_engine->_scriptExecutor->_cursorMode;
+		const bool selected =
+			(menuMode == 1 &&
+			 ((btn.buttonId == 1 && mode == Script::MouseMode::Walk) ||
+			  (btn.buttonId == 2 && mode == Script::MouseMode::Look) ||
+			  (btn.buttonId == 3 && mode == Script::MouseMode::Talk) ||
+			  (btn.buttonId == 4 && (mode == Script::MouseMode::Use ||
+									mode == Script::MouseMode::UseInventory)))) ||
+			(menuMode == 2 &&
+			 ((btn.buttonId == 0x1e && g_engine->_optionsSubMode == 1) ||
+			  (btn.buttonId == 0x1f && g_engine->_optionsSubMode == 2)));
+		const bool pressed = (_pressedButtonId != 0 && btn.buttonId == _pressedButtonId);
+		const bool hovered = (_hoveredButtonId != 0 && btn.buttonId == _hoveredButtonId);
+
+		if ((pressed || selected) && !btn.activeFrame._data.empty())
+			frame = &btn.activeFrame;
+		else if (hovered && !btn.hoverFrame._data.empty())
+			frame = &btn.hoverFrame;
+		else if (hovered && !btn.activeFrame._data.empty())
+			frame = &btn.activeFrame;
+
+		_view->drawSprite(btn.x, panelTop + btn.y, *frame, s, false);
+	}
+
+	const uint16 optTextX = g_engine->_hudTextLayout[0];
+	const uint16 optTextY = g_engine->_hudTextLayout[1];
+	const uint16 optTextMaxW = g_engine->_hudTextLayout[2] ? g_engine->_hudTextLayout[2] : 212;
+	const uint16 lineCount = g_engine->_hudTextLayout[3] ? g_engine->_hudTextLayout[3] : 9;
+	const uint16 linePitch = g_engine->_hudTextLayout[4] ? g_engine->_hudTextLayout[4] : 10;
+	const GlyphData *panelFont = g_engine->numPanelGlyphs ? g_engine->_panelGlyphs : g_engine->_glyphs;
+	const uint16 panelFontCount = g_engine->numPanelGlyphs ? g_engine->numPanelGlyphs : g_engine->numGlyphs;
+
+	if (menuMode == 1) {
+		if (_view->_inventorySource == nullptr ||
+			_view->_inventorySource->_index != Scenes::instance()._currentActorIndex)
+			_view->setInventorySource(GameObjects::instance().getProtagonistObject());
+		else
+			_view->setInventorySource(_view->_inventorySource);
+
+		const uint16 cols = g_engine->_inventCols;
+		const uint16 rows = g_engine->_inventRows;
+		const uint16 slotW = g_engine->_inventSlotW;
+		const uint16 slotH = g_engine->_inventSlotH;
+		const uint16 originX = g_engine->_inventOriginX;
+		const uint16 originY = g_engine->_inventOriginY;
+		const uint16 scroll = g_engine->_inventScroll == 0 ? 1 : g_engine->_inventScroll;
+		const uint16 slotCount = cols * rows;
+
+		for (uint16 slot = 0; slot < slotCount; slot++) {
+			const uint16 itemIndex = (uint16)(scroll - 1 + slot);
+			if (itemIndex >= _view->_inventoryItems.size())
+				break;
+			GameObject *item = _view->_inventoryItems[itemIndex];
+			if (item == nullptr)
+				continue;
+			AnimFrame *icon = _view->getInventoryIcon(item);
+			if (icon == nullptr)
+				continue;
+
+			const uint16 col = slot % cols;
+			const uint16 row = slot / cols;
+			const int slotX = originX + col * slotW;
+			const int slotY = panelTop + originY + row * slotH;
+			const int iconX = slotX + (int)slotW / 2 - (int)icon->_width / 2;
+			const int iconY = slotY + (int)slotH / 2 - (int)icon->_height / 2;
+			_view->drawSprite(iconX, iconY, *icon, s, false);
+			delete icon;
+		}
+
+		const GlyphData *font = g_engine->numGlyphs ? g_engine->_glyphs : panelFont;
+		const uint16 fontCount = g_engine->numGlyphs ? g_engine->numGlyphs : panelFontCount;
+		if (fontCount != 0) {
+			Common::String sentence = buildNativeSentenceLine();
+			if (!sentence.empty()) {
+				const uint16 maxW = (uint16)(kScreenWidth - 16);
+				while (sentence.size() > 1) {
+					if ((uint16)_view->measureStringWithFont(sentence, font, fontCount) <= maxW)
+						break;
+					sentence.deleteLastChar();
+				}
+				const int textW = _view->measureStringWithFont(sentence, font, fontCount);
+				const int textX = MAX(0, (kScreenWidth - textW) / 2);
+				const int glyphH = g_engine->maxGlyphHeight ? (int)g_engine->maxGlyphHeight : 12;
+				const int textY = MAX(0, (int)panelTop - glyphH - 2);
+				_view->renderStringWithFontTo((uint16)textX, (uint16)textY, sentence, font, fontCount, s);
+			}
+		}
+	} else if (menuMode == 2 && panelFontCount != 0) {
+		if (g_engine->_saveSlotNames.empty())
+			refreshSaveSlotNames();
+		for (uint i = 0; i < g_engine->_saveSlotNames.size() && i < lineCount; i++) {
+			Common::String name = g_engine->_saveSlotNames[i];
+			while (name.size() > 1) {
+				if ((uint16)_view->measureStringWithFont(name, panelFont, panelFontCount) <= optTextMaxW)
+					break;
+				name.deleteLastChar();
+			}
+			_view->renderStringWithFontTo(optTextX, panelTop + optTextY + (int)i * linePitch,
+										  name, panelFont, panelFontCount, s);
+		}
+	} else if (menuMode == 4 && panelFontCount != 0 && _view->_isDialogueChoiceInputActive) {
+		// Dialogue choice list at layout[5..6]; wired when assets set menuMode 4.
+		const uint16 dlgX = g_engine->_hudTextLayout[5];
+		const uint16 dlgY = g_engine->_hudTextLayout[6];
+		const uint16 pitch = g_engine->_hudTextLayout[4] ? g_engine->_hudTextLayout[4] : 10;
+		uint line = 0;
+		for (uint choice = 0; choice < _view->_dialogueChoiceLineCounts.size(); choice++) {
+			for (uint li = 0; li < _view->_dialogueChoiceLineCounts[choice] &&
+							  line < _view->_drawnStringBox.size();
+				 li++, line++) {
+				_view->renderStringWithFontTo(dlgX, panelTop + dlgY + (int)line * pitch,
+											  _view->_drawnStringBox[line], panelFont, panelFontCount, s);
+			}
+		}
+	}
+}
+
+bool ActionBar::handleClickNative(const Common::Point &pos) {
+	if (!isPointInUI(pos))
+		return false;
+
+	const uint16 panelTop = g_engine->_panelTopY;
+	const int localY = pos.y - (int)panelTop;
+	const uint16 menuMode = g_engine->_menuMode;
+
+	if (menuMode == 1) {
+		const uint16 cols = g_engine->_inventCols;
+		const uint16 rows = g_engine->_inventRows;
+		const uint16 slotW = g_engine->_inventSlotW;
+		const uint16 slotH = g_engine->_inventSlotH;
+		const uint16 originX = g_engine->_inventOriginX;
+		const uint16 originY = g_engine->_inventOriginY;
+		const uint16 scroll = g_engine->_inventScroll == 0 ? 1 : g_engine->_inventScroll;
+
+		if (pos.x >= (int)originX && localY >= (int)originY) {
+			const int relX = pos.x - (int)originX;
+			const int relY = localY - (int)originY;
+			if (relX < (int)(cols * slotW) && relY < (int)(rows * slotH)) {
+				const uint16 col = (uint16)(relX / slotW);
+				const uint16 row = (uint16)(relY / slotH);
+				const uint16 itemIndex = (uint16)(scroll - 1 + row * cols + col);
+				if (itemIndex < _view->_inventoryItems.size()) {
+					GameObject *item = _view->_inventoryItems[itemIndex];
+					Script::MouseMode mode = g_engine->_scriptExecutor->_cursorMode;
+					if (mode == Script::MouseMode::Look || mode == Script::MouseMode::Use ||
+						mode == Script::MouseMode::Talk) {
+						g_engine->_scriptExecutor->_interactedObjectID = item->_index;
+						g_engine->runScriptExecutor(false);
+						g_engine->_scriptExecutor->_interactedObjectID = 0;
+					} else if (mode == Script::MouseMode::Walk || mode == Script::MouseMode::UseInventory) {
+						_view->_activeInventoryItem = item;
+						g_engine->_scriptExecutor->_interactedInventoryItemId = item->_index + 0x400;
+						g_engine->setCursorMode(Script::MouseMode::UseInventory);
+						AnimFrame *icon = _view->getInventoryIcon(item);
+						if (icon != nullptr) {
+							const int cursorSlot = (int)Script::MouseMode::UseInventory - 1;
+							if (cursorSlot >= 0 && cursorSlot < (int)g_engine->_imageResources.size())
+								g_engine->_imageResources[cursorSlot] = *icon;
+							delete icon;
+						}
+						_view->updateCursor();
+					}
+					_view->redraw();
+					return true;
+				}
+			}
+		}
+	}
+
+	if (menuMode == 2 && g_engine->_optionsSubMode != 0) {
+		const uint16 textX = g_engine->_hudTextLayout[0];
+		const uint16 textY = g_engine->_hudTextLayout[1];
+		const uint16 textMaxW = g_engine->_hudTextLayout[2] ? g_engine->_hudTextLayout[2] : 212;
+		const uint16 lineCount = g_engine->_hudTextLayout[3] ? g_engine->_hudTextLayout[3] : 9;
+		const uint16 linePitch = g_engine->_hudTextLayout[4] ? g_engine->_hudTextLayout[4] : 10;
+		if (pos.x >= (int)textX && pos.x < (int)(textX + textMaxW) &&
+			localY >= (int)textY && localY < (int)(textY + lineCount * linePitch)) {
+			const uint16 row = (uint16)((localY - textY) / linePitch);
+			const uint16 start = g_engine->_saveListScroll == 0 ? 1 : g_engine->_saveListScroll;
+			const int slot = (int)start - 1 + (int)row;
+			if (g_engine->_optionsSubMode == 2) {
+				g_engine->loadGameState(slot);
+			} else if (g_engine->_optionsSubMode == 1) {
+				Common::String name = Common::String::format("Save %d", slot + 1);
+				if (row < g_engine->_saveSlotNames.size() &&
+					!g_engine->_saveSlotNames[row].empty() &&
+					!g_engine->_saveSlotNames[row].hasPrefix("---"))
+					name = g_engine->_saveSlotNames[row];
+				g_engine->saveGameState(slot, name);
+				refreshSaveSlotNames();
+			}
+			_view->redraw();
+			return true;
+		}
+	}
+
+	if (menuMode == 4 && _view->_isDialogueChoiceInputActive) {
+		const uint16 dlgX = g_engine->_hudTextLayout[5];
+		const uint16 dlgY = g_engine->_hudTextLayout[6];
+		const uint16 pitch = g_engine->_hudTextLayout[4] ? g_engine->_hudTextLayout[4] : 10;
+		uint totalLines = 0;
+		for (uint n : _view->_dialogueChoiceLineCounts)
+			totalLines += n;
+		if (totalLines > 0 && pos.x >= (int)dlgX &&
+			localY >= (int)dlgY && localY < (int)(dlgY + totalLines * pitch)) {
+			const int clickedLine = (localY - (int)dlgY) / (int)pitch;
+			int cumulative = 0;
+			for (uint i = 0; i < _view->_dialogueChoiceLineCounts.size(); i++) {
+				cumulative += (int)_view->_dialogueChoiceLineCounts[i];
+				if (clickedLine < cumulative) {
+					_view->_isDialogueChoiceInputActive = false;
+					_view->triggerDialogueChoice((uint8)(i + 1));
+					_view->redraw();
+					return true;
+				}
+			}
+		}
+	}
+
+	const HudButton *btn = findHudButtonAt(pos);
+	if (btn == nullptr)
+		return true;
+
+	_pressedButtonId = btn->buttonId;
+	const uint16 id = btn->buttonId;
+	if (id == 1) {
+		g_engine->setCursorMode(Script::MouseMode::Walk);
+		_view->_activeInventoryItem = nullptr;
+		g_engine->_scriptExecutor->_interactedInventoryItemId = 0;
+	} else if (id == 2) {
+		g_engine->setCursorMode(Script::MouseMode::Look);
+	} else if (id == 3) {
+		g_engine->setCursorMode(Script::MouseMode::Talk);
+	} else if (id == 4) {
+		g_engine->setCursorMode(Script::MouseMode::Use);
+	} else if (id == 0x33) {
+		g_engine->_savedMenuCursorMode = g_engine->_scriptExecutor->_cursorMode;
+		g_engine->_menuMode = 2;
+		g_engine->_optionsSubMode = 0;
+		g_engine->_saveListScroll = 1;
+		refreshSaveSlotNames();
+		g_engine->setCursorMode(Script::MouseMode::PanelCursor);
+	} else if (id == 0x32) {
+		g_engine->_menuMode = 1;
+		g_engine->_optionsSubMode = 0;
+		g_engine->setCursorMode(g_engine->_savedMenuCursorMode);
+	} else if (id == 0x1e) {
+		g_engine->_optionsSubMode = 1;
+		refreshSaveSlotNames();
+	} else if (id == 0x1f) {
+		g_engine->_optionsSubMode = 2;
+		refreshSaveSlotNames();
+	} else if (id == 0x20) {
+		// Soft restart requires dialect-v2 reinit; not available until that loader lands.
+		debugC(1, kDebugScript, "ActionBar: restart button ignored (no soft-restart yet)");
+		g_engine->_menuMode = 1;
+		g_engine->_optionsSubMode = 0;
+		g_engine->setCursorMode(Script::MouseMode::PanelCursor);
+	} else if (id == 0x21) {
+		::GUI::MessageDialog quitDialog(
+			Common::U32String("Quit the game?"),
+			Common::U32String("Quit"), Common::U32String("Cancel"));
+		if (quitDialog.runModal() == ::GUI::kMessageOK)
+			Engine::quitGame();
+	} else if (id == 0x14 || id == 0x16) {
+		const uint16 page = (id == 0x14) ? 1 : (g_engine->_inventCols * g_engine->_inventRows);
+		if (g_engine->_inventScroll > page)
+			g_engine->_inventScroll = (uint16)(g_engine->_inventScroll - page);
+		else
+			g_engine->_inventScroll = 1;
+	} else if (id == 0x15 || id == 0x17) {
+		const uint16 page = (id == 0x15) ? 1 : (g_engine->_inventCols * g_engine->_inventRows);
+		const uint16 maxStart = _view->_inventoryItems.empty() ? 1
+			: (uint16)((_view->_inventoryItems.size() > page) ? (_view->_inventoryItems.size() - page + 1) : 1);
+		uint16 next = (uint16)(g_engine->_inventScroll + page);
+		if (next > maxStart)
+			next = maxStart;
+		if (next < 1)
+			next = 1;
+		g_engine->_inventScroll = next;
+	} else if (id == 0x2a) {
+		const uint16 page = g_engine->_hudTextLayout[3] ? g_engine->_hudTextLayout[3] : 9;
+		if (g_engine->_saveListScroll > page)
+			g_engine->_saveListScroll = (uint16)(g_engine->_saveListScroll - page);
+		else
+			g_engine->_saveListScroll = 1;
+		refreshSaveSlotNames();
+	} else if (id == 0x2b) {
+		const uint16 page = g_engine->_hudTextLayout[3] ? g_engine->_hudTextLayout[3] : 9;
+		g_engine->_saveListScroll = (uint16)(g_engine->_saveListScroll + page);
+		if (g_engine->_saveListScroll > 100)
+			g_engine->_saveListScroll = 100;
+		refreshSaveSlotNames();
+	} else if (id == 0x42) {
+		g_engine->_skipSpeed = 1;
+	} else if (id == 0x43) {
+		g_engine->_skipSpeed = 2;
+	} else if (id == 0x44) {
+		g_engine->_skipSpeed = 3;
+	} else if (id == 0x45) {
+		g_engine->_skipSpeed = 4;
+	} else if (id == 0x3c) {
+		g_engine->_scriptExecutor->_musicEnabled = true;
+	} else if (id == 0x3d) {
+		g_engine->_scriptExecutor->_musicEnabled = false;
+		if (g_engine->getMusic())
+			g_engine->getMusic()->stopMusic();
+	} else if (id == 0x3e) {
+		g_engine->_scriptExecutor->_soundEnabled = true;
+	} else if (id == 0x3f) {
+		g_engine->_scriptExecutor->_soundEnabled = false;
+		g_engine->stopSample();
+	} else if (id == 0x40) {
+		g_engine->_scriptExecutor->_textEnabled = true;
+	} else if (id == 0x41) {
+		g_engine->_scriptExecutor->_textEnabled = false;
+	} else {
+		debugC(1, kDebugScript, "ActionBar: unhandled button id=0x%x menu=%u", id, menuMode);
+	}
+	_view->updateCursor();
+	_view->redraw();
+	return true;
+}
+
+void ActionBar::handleMouseMoveNative(const Common::Point &pos) {
+	const uint16 oldHovered = _hoveredButtonId;
+	_hoveredButtonId = 0;
+	_pressedButtonId = 0;
+	clearSentenceObject();
+
+	const HudButton *btn = findHudButtonAt(pos);
+	if (btn != nullptr)
+		_hoveredButtonId = btn->buttonId;
+
+	if (oldHovered != _hoveredButtonId)
+		_view->presentFrame();
+}
+
+} // namespace Macs2
diff --git a/engines/macs2/scummui.h b/engines/macs2/actionbar.h
similarity index 74%
rename from engines/macs2/scummui.h
rename to engines/macs2/actionbar.h
index 7cfc2c439d3..9b6a52577be 100644
--- a/engines/macs2/scummui.h
+++ b/engines/macs2/actionbar.h
@@ -19,8 +19,8 @@
  *
  */
 
-#ifndef MACS2_SCUMMUI_H
-#define MACS2_SCUMMUI_H
+#ifndef MACS2_ACTIONBAR_H
+#define MACS2_ACTIONBAR_H
 
 #include "common/rect.h"
 #include "common/str.h"
@@ -32,10 +32,16 @@ namespace Macs2 {
 
 class View1;
 class GameObject;
+struct AnimFrame;
+struct HudButton;
 
-class ScummUI {
+/**
+ * Persistent bottom action bar: Scumm procedural strip (kEnhUIUX) or native
+ * megapic/button HUD when dialect-v2 panel assets are loaded.
+ */
+class ActionBar {
 public:
-	ScummUI(View1 *view);
+	ActionBar(View1 *view);
 
 	void draw(Graphics::ManagedSurface &s);
 	bool handleClick(const Common::Point &pos, bool scriptsRunning = false);
@@ -47,6 +53,11 @@ public:
 	void syncActiveVerbFromCursorMode();
 	void resetInventoryAfterLoad();
 
+	bool useScummSkin() const;
+	bool useNativeSkin() const;
+	/** Y where the interactive game area ends when this bar is shown. */
+	int gameAreaBottomY() const;
+
 private:
 	static constexpr int kSentenceH = 14;
 	static constexpr int kUITop = kGameHeight;
@@ -69,6 +80,10 @@ private:
 	};
 	static const VerbDef kVerbs[4];
 
+	// --- Scumm skin ---
+	void drawScumm(Graphics::ManagedSurface &s);
+	bool handleClickScumm(const Common::Point &pos, bool scriptsRunning);
+	void handleMouseMoveScumm(const Common::Point &pos);
 	void drawSentenceLine(Graphics::ManagedSurface &s);
 	void drawVerbBar(Graphics::ManagedSurface &s);
 	void drawInventoryStrip(Graphics::ManagedSurface &s);
@@ -88,6 +103,14 @@ private:
 	/** True if pos hits the inventory scroll/item area of the strip. */
 	bool isPointInInventoryStrip(const Common::Point &pos) const;
 
+	// --- Native skin (megapic + HudButton table) ---
+	void drawNative(Graphics::ManagedSurface &s);
+	bool handleClickNative(const Common::Point &pos);
+	void handleMouseMoveNative(const Common::Point &pos);
+	void refreshSaveSlotNames();
+	Common::String buildNativeSentenceLine() const;
+	const HudButton *findHudButtonAt(const Common::Point &pos, int *outIndex = nullptr) const;
+
 	View1 *_view;
 	int _activeVerbIndex;
 	int _hoveredVerb;
@@ -96,8 +119,11 @@ private:
 	int _inventoryScrollOffset;
 	Common::Array<GameObject *> _protagonistItems;
 	Common::String _sentenceObject;
+
+	uint16 _hoveredButtonId = 0;
+	uint16 _pressedButtonId = 0;
 };
 
 } // namespace Macs2
 
-#endif // MACS2_SCUMMUI_H
+#endif // MACS2_ACTIONBAR_H
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 645eba875aa..4e92c5cbb04 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -1340,6 +1340,18 @@ void Macs2Engine::nextCursorMode() {
 	}
 }
 
+void Macs2Engine::setBottomHudVisible(bool visible) {
+	_bottomHudVisible = visible;
+	if (hasNativeHudAssets()) {
+		if (visible) {
+			if (_menuMode == 0)
+				_menuMode = 1;
+		} else {
+			_menuMode = 0;
+		}
+	}
+}
+
 void Macs2Engine::setCursorMode(Script::MouseMode newMode) {
 	// setCursorMode (1008:3ea5): when the cursor image changes, keep the hotspot
 	// fixed on screen by compensating for the old/new image half-extents, clamp,
@@ -1362,19 +1374,20 @@ void Macs2Engine::setCursorMode(Script::MouseMode newMode) {
 	};
 
 	View1 *view = (View1 *)findView("View1");
-	const bool scummVerbUI = view && view->hasScummVerbUI();
+	const bool persistentBar = view && view->hasPersistentActionBar();
+	const int barTopY = view ? view->actionBarTopY() : gameHeight();
 
 	uint16 oldHalfW = 0, oldHalfH = 0, newHalfW = 0, newHalfH = 0;
 	cursorHalfSize(oldMode, oldHalfW, oldHalfH);
 
 	Common::Point mouse = g_system->getEventManager()->getMousePos();
-	const bool mouseInUiPanel = scummVerbUI && mouse.y >= gameHeight();
+	const bool mouseInUiPanel = persistentBar && mouse.y >= barTopY;
 
 	_scriptExecutor->_cursorMode = newMode;
 
 	// Keep the pointer on the verb/inventory panel when selecting verbs there, and
 	// skip hotspot compensation when the SCUMM UI shows the same walk cursor for all verbs.
-	if (!mouseInUiPanel && !(scummVerbUI && isGameplayVerb(oldMode) && isGameplayVerb(newMode))) {
+	if (!mouseInUiPanel && !(persistentBar && isGameplayVerb(oldMode) && isGameplayVerb(newMode))) {
 		mouse.x += oldHalfW;
 		mouse.y += oldHalfH;
 
@@ -1382,7 +1395,7 @@ void Macs2Engine::setCursorMode(Script::MouseMode newMode) {
 		mouse.x -= newHalfW;
 		mouse.y -= newHalfH;
 
-		const int maxY = scummVerbUI ? (kScreenHeightLast - (int)newHalfH)
+		const int maxY = persistentBar ? (kScreenHeightLast - (int)newHalfH)
 									: (gameHeightLast() - (int)newHalfH);
 		mouse.x = CLIP<int>(mouse.x, (int)newHalfW, screenWidthLast() - (int)newHalfW);
 		mouse.y = CLIP<int>(mouse.y, (int)newHalfH, maxY);
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index b692e855fe8..2e8fec651f1 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -122,6 +122,21 @@ struct AnimFrame : public Sprite {
 	Common::Point getBottomMiddleOffset(uint16 scale = 100) const;
 };
 
+/** Persistent native HUD button (megapic panel skin). */
+struct HudButton {
+	int16 x = 0;
+	int16 y = 0;
+	uint16 inactiveStep = 0;
+	uint16 activeStep = 0;
+	uint16 hoverStep = 0;
+	uint16 buttonId = 0; // 1=Walk, 2=Look, 3=Talk, 4=Use, 0x33=Options, …
+	uint16 menuId = 0;   // 1=main bar, 2=options, …
+	AnimFrame frame;
+	AnimFrame activeFrame;
+	AnimFrame hoverFrame;
+	Common::Array<uint8> animBlob;
+};
+
 struct BackgroundAnimation {
 	uint16 _x = 0;
 	uint16 _y = 0;
@@ -422,6 +437,44 @@ public:
 	Common::Array<byte> _amigaPendingSceneScript;
 	Common::Array<byte> _amigaPendingSceneStrings;
 
+	/** Bottom HUD visible (showActionBar/hideActionBar); default shown. */
+	bool _bottomHudVisible = true;
+
+	/**
+	 * Native persistent HUD button table and megapic panels.
+	 * Populated when dialect-v2 panel assets are loaded.
+	 */
+	Graphics::ManagedSurface _hudMegapics[6];
+	bool _hudMegapicLoaded[6] = {};
+	Common::Array<HudButton> _hudButtons;
+	/** Y where the bottom HUD starts; scene above this is interactive. */
+	uint16 _panelTopY = 0;
+	uint16 _panelHeight = 0;
+	/** 0=hidden, 1=main verbs/invent, 2=options, 4=dialogue list. */
+	uint16 _menuMode = 1;
+	/** 0=none, 1=save, 2=load (options submenu). */
+	uint16 _optionsSubMode = 0;
+	Script::MouseMode _savedMenuCursorMode = Script::MouseMode::Walk;
+	uint16 _inventScroll = 1;
+	uint16 _inventOriginX = 0;
+	uint16 _inventOriginY = 0;
+	uint16 _inventCols = 4;
+	uint16 _inventRows = 2;
+	uint16 _inventSlotW = 64;
+	uint16 _inventSlotH = 52;
+	uint16 _inventLayoutMode = 0;
+	/**
+	 * Text layout after invent grid:
+	 * [0..4] = options list X/Y/maxW/rows/pitch
+	 * [5..6] = dialogue list X/Y
+	 */
+	uint16 _hudTextLayout[7] = {};
+	uint16 _hudTextRecolor[4] = {};
+	uint16 _saveListScroll = 1;
+	Common::Array<Common::String> _saveSlotNames;
+	/** Cutscene skip-speed preference (1..4) from options HUD. */
+	uint16 _skipSpeed = 1;
+
 	void setCursorMode(Script::MouseMode newMode);
 	void nextCursorMode();
 
@@ -578,6 +631,19 @@ public:
 	int gameHeight() const { return kGameHeight; }
 	int gameHeightLast() const { return gameHeight() - 1; }
 
+	/**
+	 * Bottom HUD / action-bar visibility (dialect-neutral).
+	 * Driven by showActionBar / hideActionBar; Scumm verb strip and native
+	 * HUDs both respect this flag. Native skin also maps visible ↔ menuMode.
+	 */
+	bool isBottomHudVisible() const { return _bottomHudVisible; }
+	void setBottomHudVisible(bool visible);
+
+	/** True when dialect-v2 panel geometry/assets are available for native HUD. */
+	bool hasNativeHudAssets() const {
+		return _panelTopY != 0 && _panelHeight != 0;
+	}
+
 	// --- Layout / dialect facades (DOS defaults; other platforms override later) ---
 
 	/** Game-loop timer quantum in milliseconds. */
diff --git a/engines/macs2/module.mk b/engines/macs2/module.mk
index 84a299ad07a..aedf8a2e2a6 100644
--- a/engines/macs2/module.mk
+++ b/engines/macs2/module.mk
@@ -15,7 +15,7 @@ MODULE_OBJS = \
 	metaengine.o \
 	saveload.o \
 	scriptexecutor.o \
-	scummui.o \
+	actionbar.o \
 	view1.o
 
 ifdef USE_IMGUI
diff --git a/engines/macs2/saveload.cpp b/engines/macs2/saveload.cpp
index 3dd1ea0ae4c..c8c83fce90d 100644
--- a/engines/macs2/saveload.cpp
+++ b/engines/macs2/saveload.cpp
@@ -848,7 +848,7 @@ Common::Error Macs2Engine::syncGame(Common::Serializer &s) {
 		view1->rebuildCharacterLookupTable();
 		view1->refreshProtagonistInventoryAfterLoad(actorIndex);
 		view1->_uiPanelState = View1::kUiPanelNone;
-		view1->ensureScummVerbUI();
+		view1->ensureActionBar();
 
 		// Restore UseInventory cursor image after load.
 		// The cursor slot is only populated when clicking an inventory item in the panel;
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 02bd97720e5..99dd1117efd 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -3389,6 +3389,11 @@ OpcodeResult ScriptExecutor::scriptClearDeltaSfxList() {
 OpcodeResult ScriptExecutor::scriptShowActionBar() {
 	debugC(kDebugScript, "SCRIPT::showActionBar()");
 	View1 *currentView = (View1 *)_engine->findView("View1");
+	if (currentView != nullptr && currentView->hasPersistentActionBar()) {
+		_engine->setBottomHudVisible(true);
+		currentView->redraw();
+		return OpcodeResult::Continue;
+	}
 	if (currentView != nullptr) {
 		currentView->openScriptActionBar(
 			Common::Point(_engine->screenWidth() / 2, _engine->gameHeight() / 2), _cursorMode);
@@ -3399,6 +3404,11 @@ OpcodeResult ScriptExecutor::scriptShowActionBar() {
 OpcodeResult ScriptExecutor::scriptHideActionBar() {
 	debugC(kDebugScript, "SCRIPT::hideActionBar()");
 	View1 *currentView = (View1 *)_engine->findView("View1");
+	if (currentView != nullptr && currentView->hasPersistentActionBar()) {
+		_engine->setBottomHudVisible(false);
+		currentView->redraw();
+		return OpcodeResult::Continue;
+	}
 	if (currentView != nullptr) {
 		MouseMode savedMode = _cursorMode;
 		currentView->closeScriptActionBar(savedMode);
diff --git a/engines/macs2/scriptexecutor.h b/engines/macs2/scriptexecutor.h
index 2f204605db5..3bef09197fc 100644
--- a/engines/macs2/scriptexecutor.h
+++ b/engines/macs2/scriptexecutor.h
@@ -439,6 +439,8 @@ public:
 	bool _scriptSkippable = false;
 	bool _musicEnabled = true;
 	bool _soundSystemActive = true;
+	/** Dialogue/subtitle text display toggle (native options HUD). */
+	bool _textEnabled = true;
 	bool _overlayTextStageActive = false;
 	bool _inventoryActionFlag = false;
 	bool _inventoryCombineFlag = false;
diff --git a/engines/macs2/scummui.cpp b/engines/macs2/scummui.cpp
deleted file mode 100644
index de78c7a898d..00000000000
--- a/engines/macs2/scummui.cpp
+++ /dev/null
@@ -1,440 +0,0 @@
-/* ScummVM - Graphic Adventure Engine
- *
- * ScummVM is the legal property of its developers, whose names
- * are too numerous to list here. Please refer to the COPYRIGHT
- * file distributed with this source distribution.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-#include "macs2/scummui.h"
-
-#include "macs2/gameobjects.h"
-#include "macs2/macs2.h"
-#include "macs2/view1.h"
-
-namespace Macs2 {
-
-static Common::String getObjectDisplayName(const GameObject *obj) {
-	if (!obj)
-		return Common::String();
-
-	const GameObjects &objects = GameObjects::instance();
-	if (obj->_index < objects._objectNames.size() && !objects._objectNames[obj->_index].empty())
-		return objects._objectNames[obj->_index];
-
-	return Common::String();
-}
-
-const ScummUI::VerbDef ScummUI::kVerbs[4] = {
-	{"Walk", Script::MouseMode::Walk},
-	{"Look", Script::MouseMode::Look},
-	{"Use", Script::MouseMode::Use},
-	{"Talk", Script::MouseMode::Talk}
-};
-
-ScummUI::ScummUI(View1 *view)
-	: _view(view), _activeVerbIndex(0), _hoveredVerb(-1), _hoveredItemIndex(-1), _hoveredScrollButton(-1),
-	  _inventoryScrollOffset(0) {
-}
-
-bool ScummUI::isPointInUI(const Common::Point &pos) const {
-	return pos.y >= kUITop;
-}
-
-void ScummUI::syncInventory() {
-	rebuildProtagonistItems();
-	const int maxOffset = MAX(0, (int)_protagonistItems.size() - kInvCols * kInvRows);
-	if (_inventoryScrollOffset > maxOffset)
-		_inventoryScrollOffset = maxOffset;
-	if (_inventoryScrollOffset < 0)
-		_inventoryScrollOffset = 0;
-}
-
-void ScummUI::rebuildProtagonistItems() {
-	_protagonistItems.clear();
-
-	if (_view->isInventorySourceProtagonist()) {
-		_protagonistItems = _view->_inventoryItems;
-		return;
-	}
-
-	const uint16 invScene = Scenes::instance()._currentActorIndex + 0x400;
-	for (GameObject *obj : GameObjects::instance()._objects) {
-		if (obj && obj->_sceneIndex == invScene)
-			_protagonistItems.push_back(obj);
-	}
-}
-
-void ScummUI::resetInventoryAfterLoad() {
-	_inventoryScrollOffset = 0;
-	_hoveredItemIndex = -1;
-	_hoveredScrollButton = -1;
-	_hoveredVerb = -1;
-	_sentenceObject.clear();
-	rebuildProtagonistItems();
-	syncActiveVerbFromCursorMode();
-
-	if (_view->_activeInventoryItem) {
-		bool inInventory = false;
-		for (GameObject *obj : _protagonistItems) {
-			if (obj == _view->_activeInventoryItem) {
-				inInventory = true;
-				break;
-			}
-		}
-		if (!inInventory) {
-			_view->_activeInventoryItem = nullptr;
-			g_engine->_scriptExecutor->_interactedInventoryItemId = 0;
-		}
-	}
-}
-
-void ScummUI::syncActiveVerbFromCursorMode() {
-	const Script::MouseMode mode = g_engine->_scriptExecutor->_cursorMode;
-	if (mode == Script::MouseMode::UseInventory) {
-		_activeVerbIndex = 2;
-		return;
-	}
-
-	for (int i = 0; i < 4; i++) {
-		if (kVerbs[i].mode == mode) {
-			_activeVerbIndex = i;
-			return;
-		}
-	}
-}
-
-void ScummUI::draw(Graphics::ManagedSurface &s) {
-	syncActiveVerbFromCursorMode();
-	rebuildProtagonistItems();
-	_view->drawBorderSide(Common::Point(0, kUITop), Common::Point(kScreenWidth, kUIHeight), s);
-	drawSentenceLine(s);
-	drawVerbBar(s);
-	drawInventoryStrip(s);
-}
-
-void ScummUI::drawUIButton(const Common::Rect &rect, bool pressed, Graphics::ManagedSurface &s) {
-	_view->drawBorderSide(Common::Point(rect.left, rect.top), Common::Point(rect.width(), rect.height()), s);
-	const View1::BorderStyle &style = pressed ? View1::kBorderPressed : View1::kBorderRaised;
-	_view->drawNinePatchBorder(Common::Point(rect.left, rect.top), Common::Point(rect.width(), rect.height()),
-							   style, false, false, s);
-}
-
-void ScummUI::drawSentenceLine(Graphics::ManagedSurface &s) {
-	Common::String sentence;
-	const Script::MouseMode mode = g_engine->_scriptExecutor->_cursorMode;
-
-	if (mode == Script::MouseMode::UseInventory && _view->_activeInventoryItem) {
-		sentence = "Use";
-		const Common::String itemName = getObjectDisplayName(_view->_activeInventoryItem);
-		if (!itemName.empty())
-			sentence += " " + itemName;
-	} else if (_activeVerbIndex >= 0 && _activeVerbIndex < 4) {
-		sentence = kVerbs[_activeVerbIndex].label;
-	}
-	if (!_sentenceObject.empty()) {
-		if (!sentence.empty()) {
-			if (mode == Script::MouseMode::UseInventory)
-				sentence += " with ";
-			else
-				sentence += " ";
-		}
-		sentence += _sentenceObject;
-	}
-
-	if (sentence.empty())
-		return;
-
-	const bool usePanelFont = g_engine->numPanelGlyphs > 0;
-	const GlyphData *font = usePanelFont ? g_engine->_panelGlyphs : g_engine->_glyphs;
-	const uint16 fontCount = usePanelFont ? g_engine->numPanelGlyphs : g_engine->numGlyphs;
-	const int glyphH = usePanelFont ? g_engine->maxPanelGlyphHeight : g_engine->maxGlyphHeight;
-	if (usePanelFont)
-		sentence.toUppercase();
-	const int textY = kUITop + MAX(0, (kSentenceH - glyphH) / 2);
-	const int textX = MAX(0, (kScreenWidth - _view->measureStringWithFont(sentence, font, fontCount)) / 2);
-	_view->renderStringWithFontTo(textX, textY, sentence, font, fontCount, s);
-}
-
-void ScummUI::drawVerbBar(Graphics::ManagedSurface &s) {
-	for (int i = 0; i < 4; i++) {
-		const Common::Rect r = getVerbRect(i);
-		const bool isActive = (i == _activeVerbIndex);
-		const bool isHovered = (i == _hoveredVerb);
-
-		drawUIButton(r, isActive || isHovered, s);
-
-		const int textX = r.left + (r.width() - (int)strlen(kVerbs[i].label) * 6) / 2;
-		const int textY = r.top + (r.height() - (int)g_engine->maxGlyphHeight) / 2;
-		_view->renderStringTo(textX, textY, kVerbs[i].label, s);
-	}
-}
-
-int ScummUI::getScrollButtonWidth() const {
-	uint16 maxW = 0;
-	const Common::Array<uint16> &indices = g_engine->inventoryIconIndices;
-	for (int i = 2; i <= 3 && i < (int)indices.size(); i++) {
-		const int imgIdx = (int)indices[i] - 1;
-		if (imgIdx >= 0 && imgIdx < (int)g_engine->_imageResources.size())
-			maxW = MAX(maxW, g_engine->_imageResources[imgIdx]._width);
-	}
-	return MAX(22, (int)maxW + 6);
-}
-
-int ScummUI::getInvArrowX() const {
-	return kInvX;
-}
-
-void ScummUI::drawScrollButton(Graphics::ManagedSurface &s, const Common::Rect &rect,
-							   int iconResourceIndex, bool hovered) {
-	drawUIButton(rect, hovered, s);
-
-	if (iconResourceIndex < 0 || iconResourceIndex >= (int)g_engine->_imageResources.size())
-		return;
-
-	const AnimFrame &frame = g_engine->_imageResources[iconResourceIndex];
-	if (frame._data.empty() || frame._width == 0 || frame._height == 0)
-		return;
-
-	int iconX = rect.left + (rect.width() - frame._width) / 2;
-	int iconY = rect.top + (rect.height() - frame._height) / 2;
-	if (hovered) {
-		iconX++;
-		iconY++;
-	}
-	_view->drawSprite(iconX, iconY, frame, s, false);
-}
-
-void ScummUI::drawInventoryStrip(Graphics::ManagedSurface &s) {
-	const Common::Array<uint16> &indices = g_engine->inventoryIconIndices;
-	const int upIconIdx = (indices.size() > 2) ? (int)indices[2] - 1 : -1;
-	const int downIconIdx = (indices.size() > 3) ? (int)indices[3] - 1 : -1;
-
-	drawScrollButton(s, getInvScrollLeftRect(), upIconIdx, _hoveredScrollButton == 0);
-	drawScrollButton(s, getInvScrollRightRect(), downIconIdx, _hoveredScrollButton == 1);
-
-	const Common::Array<GameObject *> items = getProtagonistItems();
-	const int maxVisible = kInvCols * kInvRows;
-	for (int i = 0; i < maxVisible; i++) {
-		const Common::Rect r = getInvItemRect(i);
-		drawUIButton(r, true, s);
-
-		const int itemIdx = _inventoryScrollOffset + i;
-		if (itemIdx >= (int)items.size())
-			continue;
-
-		const bool isHovered = (i == _hoveredItemIndex);
-		const bool isActive = (_view->_activeInventoryItem == items[itemIdx]);
-
-		AnimFrame *icon = _view->getInventoryIcon(items[itemIdx]);
-		if (icon && !icon->_data.empty()) {
-			_view->drawSpriteFitted(r, *icon, s, kInvIconInset);
-		}
-		delete icon;
-
-		if (isActive) {
-			_view->renderStringTo(r.left + 1, r.top, "*", s);
-		} else if (isHovered) {
-			_view->renderStringTo(r.left + 1, r.top, ".", s);
-		}
-	}
-}
-
-Common::Array<GameObject *> ScummUI::getProtagonistItems() const {
-	return _protagonistItems;
-}
-
-bool ScummUI::handleClick(const Common::Point &pos, bool scriptsRunning) {
-	for (int i = 0; i < 4; i++) {
-		if (getVerbRect(i).contains(pos)) {
-			_activeVerbIndex = i;
-			g_engine->setCursorMode(kVerbs[i].mode);
-			_view->_activeInventoryItem = nullptr;
-			g_engine->_scriptExecutor->_interactedInventoryItemId = 0;
-			_view->updateCursor();
-			return true;
-		}
-	}
-
-	if (getInvScrollLeftRect().contains(pos)) {
-		if (_inventoryScrollOffset > 0)
-			_inventoryScrollOffset -= kInvCols * kInvRows;
-		return true;
-	}
-	if (getInvScrollRightRect().contains(pos)) {
-		const int maxItems = (int)getProtagonistItems().size();
-		const int maxVisible = kInvCols * kInvRows;
-		if (_inventoryScrollOffset + maxVisible < maxItems)
-			_inventoryScrollOffset += maxVisible;
-		return true;
-	}
-
-	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++) {
-		const int itemIdx = _inventoryScrollOffset + i;
-		if (itemIdx >= (int)items.size())
-			break;
-
-		if (!getInvItemRect(i).contains(pos))
-			continue;
-
-		GameObject *item = items[itemIdx];
-
-		if (mode == Script::MouseMode::Look) {
-			g_engine->_scriptExecutor->_interactedObjectID = 0x400 + item->_index;
-			g_engine->_scriptExecutor->_interactedInventoryItemId = 0;
-			_view->_pendingPanelRequest = View1::kPanelRequestInventory;
-			g_engine->runScriptExecutor(false);
-			_view->_pendingPanelRequest = View1::kPanelRequestNone;
-		} else if (mode == Script::MouseMode::Use) {
-			_view->_activeInventoryItem = item;
-			g_engine->_scriptExecutor->_interactedInventoryItemId = 0x400 + item->_index;
-			AnimFrame *icon = _view->getInventoryIcon(item);
-			if (icon != nullptr) {
-				const int cursorSlot = (int)Script::MouseMode::UseInventory - 1;
-				g_engine->_imageResources[cursorSlot] = *icon;
-				delete icon;
-			}
-			g_engine->setCursorMode(Script::MouseMode::UseInventory);
-			_view->updateCursor();
-		} else if (mode == Script::MouseMode::UseInventory && _view->_activeInventoryItem) {
-			g_engine->_scriptExecutor->_interactedObjectID = 0x400 + _view->_activeInventoryItem->_index;
-			g_engine->_scriptExecutor->_interactedInventoryItemId = 0x400 + item->_index;
-			_view->_pendingPanelRequest = View1::kPanelRequestInventory;
-			g_engine->runScriptExecutor(false);
-			_view->_pendingPanelRequest = View1::kPanelRequestNone;
-			_view->_activeInventoryItem = nullptr;
-			g_engine->setCursorMode(Script::MouseMode::Use);
-			_view->updateCursor();
-			syncInventory();
-		}
-		return true;
-	}
-
-	return true;
-}
-
-void ScummUI::handleMouseMove(const Common::Point &pos) {
-	const int oldHoveredVerb = _hoveredVerb;
-	const int oldHoveredItemIndex = _hoveredItemIndex;
-	const int oldHoveredScrollButton = _hoveredScrollButton;
-
-	_hoveredVerb = -1;
-	_hoveredItemIndex = -1;
-	_hoveredScrollButton = -1;
-	clearSentenceObject();
-
-	for (int i = 0; i < 4; i++) {
-		if (getVerbRect(i).contains(pos)) {
-			_hoveredVerb = i;
-			break;
-		}
-	}
-
-	if (_hoveredVerb < 0) {
-		if (getInvScrollLeftRect().contains(pos)) {
-			_hoveredScrollButton = 0;
-		} else if (getInvScrollRightRect().contains(pos)) {
-			_hoveredScrollButton = 1;
-		} else {
-			const Common::Array<GameObject *> items = getProtagonistItems();
-			const int maxVisible = kInvCols * kInvRows;
-			for (int i = 0; i < maxVisible; i++) {
-				const int itemIdx = _inventoryScrollOffset + i;
-				if (itemIdx >= (int)items.size())
-					break;
-				if (getInvItemRect(i).contains(pos)) {
-					_hoveredItemIndex = i;
-					break;
-				}
-			}
-		}
-	}
-
-	if (oldHoveredVerb != _hoveredVerb || oldHoveredItemIndex != _hoveredItemIndex ||
-		oldHoveredScrollButton != _hoveredScrollButton) {
-		_view->presentFrame();
-	}
-}
-
-void ScummUI::updateSentenceLine(const Common::String &objectName) {
-	_sentenceObject = objectName;
-}
-
-void ScummUI::clearSentenceObject() {
-	_sentenceObject.clear();
-}
-
-Common::Rect ScummUI::getVerbRect(int index) const {
-	const int col = index % kVerbCols;
-	const int row = index / kVerbCols;
-	const int x = col * kVerbW;
-	const int y = kVerbY + row * kVerbH;
-	return Common::Rect(x, y, x + kVerbW, y + kVerbH);
-}
-
-Common::Rect ScummUI::getInvItemRect(int index) const {
-	const int col = index % kInvCols;
-	const int row = index / kInvCols;
-	const int scrollW = getScrollButtonWidth();
-	const int x = getInvArrowX() + scrollW + col * kInvItemW;
-	const int y = kVerbY + row * kInvItemH;
-	return Common::Rect(x, y, x + kInvItemW, y + kInvItemH);
-}
-
-Common::Rect ScummUI::getInvScrollLeftRect() const {
-	const int scrollW = getScrollButtonWidth();
-	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;
-	return Common::Rect(x, kVerbY, x + scrollW, kVerbY + kVerbH * kVerbRows);
-}
-
-} // namespace Macs2
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index bd6eab42b1f..bbd43a1e005 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -34,7 +34,7 @@
 #include "macs2/gameobjects.h"
 #include "macs2/macs2.h"
 #include "macs2/music.h"
-#include "macs2/scummui.h"
+#include "macs2/actionbar.h"
 
 namespace Macs2 {
 namespace {
@@ -137,19 +137,19 @@ View1::View1() : UIElement("View1") {
 	_inventorySource = protagonist->_gameObject;
 	_inventoryButtonLocations.resize(6);
 
-	if (hasScummVerbUI()) {
-		_scummUI = new ScummUI(this);
+	if (hasPersistentActionBar()) {
+		_actionBar = new ActionBar(this);
 		_bounds = Common::Rect(0, 0, kScreenWidth, kScreenHeight);
 		_innerBounds = _bounds;
 		setInventorySource(_inventorySource);
 	}
 }
 
-void View1::ensureScummVerbUI() {
-	if (!hasScummVerbUI())
+void View1::ensureActionBar() {
+	if (!hasPersistentActionBar())
 		return;
-	if (!_scummUI) {
-		_scummUI = new ScummUI(this);
+	if (!_actionBar) {
+		_actionBar = new ActionBar(this);
 		if (_inventorySource)
 			setInventorySource(_inventorySource);
 	}
@@ -160,12 +160,22 @@ void View1::ensureScummVerbUI() {
 	}
 }
 
-bool View1::hasScummVerbUI() const {
-	return g_engine->enhancementEnabled(kEnhUIUX);
+bool View1::hasPersistentActionBar() const {
+	return g_engine->enhancementEnabled(kEnhUIUX) || g_engine->hasNativeHudAssets();
 }
 
-bool View1::shouldShowScummVerbUI() const {
-	if (!hasScummVerbUI())
+int View1::actionBarTopY() const {
+	if (_actionBar && shouldShowActionBar())
+		return _actionBar->gameAreaBottomY();
+	if (g_engine->hasNativeHudAssets() && g_engine->isBottomHudVisible() && g_engine->_menuMode != 0)
+		return (int)g_engine->_panelTopY;
+	return kGameHeight;
+}
+
+bool View1::shouldShowActionBar() const {
+	if (!hasPersistentActionBar())
+		return false;
+	if (!g_engine->isBottomHudVisible())
 		return false;
 	if (_currentMode == ViewMode::VM_HELP)
 		return false;
@@ -188,7 +198,7 @@ bool View1::shouldShowScummVerbUI() const {
 }
 
 View1::~View1() {
-	delete _scummUI;
+	delete _actionBar;
 	for (Character *c : _characters) {
 		delete c;
 	}
@@ -231,9 +241,9 @@ void View1::openInventory(GameObject *newInventorySource) {
 	_pendingPanelRequest = kPanelRequestNone; // Binary: g_wPendingPanelRequest = 0
 
 	// SCUMM verb UI: protagonist inventory is always visible in the strip.
-	if (hasScummVerbUI() && newInventorySource->_index == Scenes::instance()._currentActorIndex) {
-		if (_scummUI)
-			_scummUI->syncInventory();
+	if (hasPersistentActionBar() && newInventorySource->_index == Scenes::instance()._currentActorIndex) {
+		if (_actionBar)
+			_actionBar->syncInventory();
 		return;
 	}
 
@@ -302,8 +312,8 @@ void View1::setInventorySource(GameObject *newInventorySource) {
 			_inventoryItems.push_back(currentObject);
 		}
 	}
-	if (hasScummVerbUI() && _scummUI)
-		_scummUI->syncInventory();
+	if (hasPersistentActionBar() && _actionBar)
+		_actionBar->syncInventory();
 }
 
 void View1::refreshProtagonistInventoryAfterLoad(uint16 actorIndex) {
@@ -333,8 +343,8 @@ void View1::refreshProtagonistInventoryAfterLoad(uint16 actorIndex) {
 
 	_inventoryItems = validated;
 
-	if (hasScummVerbUI() && _scummUI)
-		_scummUI->resetInventoryAfterLoad();
+	if (hasPersistentActionBar() && _actionBar)
+		_actionBar->resetInventoryAfterLoad();
 }
 
 bool View1::isInventorySourceProtagonist() const {
@@ -345,8 +355,8 @@ void View1::transferInventoryItem(GameObject *item, GameObject *targetContainer)
 	int index = findInventoryItem(item);
 	_inventoryItems.remove_at(index);
 	item->_sceneIndex = targetContainer->_index + 0x400;
-	if (hasScummVerbUI() && _scummUI)
-		_scummUI->syncInventory();
+	if (hasPersistentActionBar() && _actionBar)
+		_actionBar->syncInventory();
 }
 
 int View1::findInventoryItem(const GameObject *item) {
@@ -430,7 +440,7 @@ void View1::updateCursor(const byte *palette) {
 	int mode = (int)g_engine->_scriptExecutor->_cursorMode - 1;
 
 	// SCUMM-style UI: gameplay verbs share the walk cursor; the sentence line shows the active verb.
-	if (hasScummVerbUI()) {
+	if (hasPersistentActionBar()) {
 		const Script::MouseMode cursorMode = g_engine->_scriptExecutor->_cursorMode;
 		switch (cursorMode) {
 		case Script::MouseMode::Talk:
@@ -836,7 +846,7 @@ void View1::layoutActionBarButtons() {
 }
 
 void View1::openMainMenu(Common::Point clickedPosition) {
-	if (hasScummVerbUI())
+	if (hasPersistentActionBar())
 		return;
 
 	// Binary handleInput: save cursor and set to PanelCursor (0x19)
@@ -875,7 +885,7 @@ void View1::openMainMenu(Common::Point clickedPosition) {
 }
 
 void View1::openScriptActionBar(const Common::Point &position, Script::MouseMode restoreCursorMode) {
-	if (_uiPanelState != kUiPanelNone || hasScummVerbUI())
+	if (_uiPanelState != kUiPanelNone || hasPersistentActionBar())
 		return;
 	openMainMenu(position);
 	g_engine->setCursorMode(restoreCursorMode);
@@ -1080,8 +1090,8 @@ void View1::transferPickupTarget(GameObject *targetObject) {
 		}
 	}
 
-	if (hasScummVerbUI() && _scummUI)
-		_scummUI->syncInventory();
+	if (hasPersistentActionBar() && _actionBar)
+		_actionBar->syncInventory();
 
 	// Binary sets g_wNeedsRedraw and restores scene background over the panel area.
 	redraw();
@@ -1475,8 +1485,8 @@ bool View1::handleContainerInventoryClick(const MouseDownMessage &msg) {
 				updateCursor();
 				g_engine->_scriptExecutor->_inventoryActionFlag = true;
 				setInventorySource(_inventorySource);
-				if (hasScummVerbUI() && _scummUI)
-					_scummUI->syncInventory();
+				if (hasPersistentActionBar() && _actionBar)
+					_actionBar->syncInventory();
 			}
 			break;
 		}
@@ -1516,8 +1526,8 @@ bool View1::handleContainerInventoryClick(const MouseDownMessage &msg) {
 		}
 		g_engine->setCursorMode(Script::MouseMode::UseInventory);
 		updateCursor();
-		if (hasScummVerbUI() && _scummUI)
-			_scummUI->syncInventory();
+		if (hasPersistentActionBar() && _actionBar)
+			_actionBar->syncInventory();
 		return true;
 	}
 
@@ -1644,9 +1654,9 @@ bool View1::handleInput(const MouseDownMessage &msg) {
 			return handleHelpClick(msg);
 		}
 
-		if (shouldShowScummVerbUI() && _scummUI && _scummUI->isPointInUI(msg._pos)) {
+		if (shouldShowActionBar() && _actionBar && _actionBar->isPointInUI(msg._pos)) {
 			if (g_engine->_scriptExecutor->_cursorMode != Script::MouseMode::Disabled) {
-				_scummUI->handleClick(msg._pos, g_engine->_scriptExecutor->isExecuting());
+				_actionBar->handleClick(msg._pos, g_engine->_scriptExecutor->isExecuting());
 				presentFrame();
 			}
 			return true;
@@ -1727,7 +1737,7 @@ bool View1::handleInput(const MouseDownMessage &msg) {
 		}
 
 		if (g_engine->_scriptExecutor->_cursorMode == Script::MouseMode::Walk) {
-			if (shouldShowScummVerbUI() && msg._pos.y >= kGameHeight)
+			if (shouldShowActionBar() && msg._pos.y >= actionBarTopY())
 				return true;
 
 			Character *protagonist = getCharacterByIndex(Scenes::instance()._currentActorIndex);
@@ -1770,7 +1780,7 @@ bool View1::handleInput(const MouseDownMessage &msg) {
 		}
 
 		// Check if we hit something
-		if (shouldShowScummVerbUI() && msg._pos.y >= kGameHeight)
+		if (shouldShowActionBar() && msg._pos.y >= actionBarTopY())
 			return true;
 
 		// Original order: getHotspotAtPoint first, then drawCharactersAndHitTest overrides.
@@ -1849,13 +1859,13 @@ bool View1::handleInput(const MouseDownMessage &msg) {
 		if (g_engine->_scriptExecutor->_cursorMode == Script::MouseMode::Disabled) {
 			return true;
 		}
-		if (hasScummVerbUI()) {
-			if (shouldShowScummVerbUI()) {
+		if (hasPersistentActionBar()) {
+			if (shouldShowActionBar()) {
 				g_engine->nextCursorMode();
 				_activeInventoryItem = nullptr;
 				g_engine->_scriptExecutor->_interactedInventoryItemId = 0;
-				if (_scummUI)
-					_scummUI->syncActiveVerbFromCursorMode();
+				if (_actionBar)
+					_actionBar->syncActiveVerbFromCursorMode();
 				updateCursor();
 				presentFrame();
 			}
@@ -1996,11 +2006,11 @@ bool View1::msgMouseMove(const MouseMoveMessage &msg) {
 	_hoverAreaId = g_engine->_scriptExecutor->getAreaAtPoint(msg._pos.x, msg._pos.y);
 	_hoverHotspotId = g_engine->getHotspotAtPoint(msg._pos);
 
-	if (shouldShowScummVerbUI() && _scummUI) {
-		if (_scummUI->isPointInUI(msg._pos)) {
-			_scummUI->handleMouseMove(msg._pos);
-		} else if (msg._pos.y < kGameHeight) {
-			_scummUI->clearSentenceObject();
+	if (shouldShowActionBar() && _actionBar) {
+		if (_actionBar->isPointInUI(msg._pos)) {
+			_actionBar->handleMouseMove(msg._pos);
+		} else if (msg._pos.y < actionBarTopY()) {
+			_actionBar->clearSentenceObject();
 			uint16 index = getHitObjectID(msg._pos);
 			if (index == 0)
 				index = g_engine->getHotspotAtPoint(msg._pos);
@@ -2009,7 +2019,7 @@ bool View1::msgMouseMove(const MouseMoveMessage &msg) {
 				if (objIndex < GameObjects::instance()._objectNames.size()) {
 					const Common::String &name = GameObjects::instance()._objectNames[objIndex];
 					if (!name.empty())
-						_scummUI->updateSentenceLine(name);
+						_actionBar->updateSentenceLine(name);
 				}
 			}
 		}
@@ -2087,7 +2097,7 @@ bool View1::msgKeypress(const KeypressMessage &msg) {
 	// Binary (handleInput 1008:edff): UI panels only open when not executing and cursor != Disabled.
 	if (!g_engine->_scriptExecutor->isExecuting() && g_engine->_scriptExecutor->_cursorMode != Script::MouseMode::Disabled) {
 		if (msg.ascii == (uint16)'i') {
-			if (hasScummVerbUI()) {
+			if (hasPersistentActionBar()) {
 				if (_uiPanelState == kUiPanelContainerInventory)
 					closeInventory();
 			} else if (_uiPanelState != kUiPanelInventory) {
@@ -2096,13 +2106,13 @@ bool View1::msgKeypress(const KeypressMessage &msg) {
 				closeInventory();
 			}
 		} else if (msg.ascii == 'n') {
-			if (hasScummVerbUI()) {
-				if (shouldShowScummVerbUI()) {
+			if (hasPersistentActionBar()) {
+				if (shouldShowActionBar()) {
 					g_engine->nextCursorMode();
 					_activeInventoryItem = nullptr;
 					g_engine->_scriptExecutor->_interactedInventoryItemId = 0;
-					if (_scummUI)
-						_scummUI->syncActiveVerbFromCursorMode();
+					if (_actionBar)
+						_actionBar->syncActiveVerbFromCursorMode();
 					updateCursor();
 					presentFrame();
 				}
@@ -2189,7 +2199,7 @@ void View1::draw() {
 
 	// We keep the inventory on but don't draw it in case we display a string
 	// i.e. a description of an item
-	const bool showProtagonistInventory = _uiPanelState == kUiPanelInventory && !hasScummVerbUI();
+	const bool showProtagonistInventory = _uiPanelState == kUiPanelInventory && !hasPersistentActionBar();
 	if ((showProtagonistInventory || _uiPanelState == kUiPanelContainerInventory) && !_isShowingTextBox && !_isShowingDialoguePanel) {
 		drawInventory(s);
 	}
@@ -2250,14 +2260,15 @@ void View1::draw() {
 		}
 	}
 
-	if (hasScummVerbUI()) {
-		ensureScummVerbUI();
-		if (_scummUI && !_isShowingTextBox && !_isShowingDialoguePanel) {
+	if (hasPersistentActionBar()) {
+		ensureActionBar();
+		if (_actionBar && !_isShowingTextBox && !_isShowingDialoguePanel) {
 			Graphics::ManagedSurface fullScreen(*g_events->getScreen(), Common::Rect(0, 0, kScreenWidth, kScreenHeight));
-			if (shouldShowScummVerbUI()) {
-				_scummUI->draw(fullScreen);
+			if (shouldShowActionBar()) {
+				_actionBar->draw(fullScreen);
 			} else {
-				fullScreen.fillRect(Common::Rect(0, kGameHeight, kScreenWidth, kScreenHeight), 0);
+				const int top = actionBarTopY();
+				fullScreen.fillRect(Common::Rect(0, top, kScreenWidth, kScreenHeight), 0);
 			}
 		}
 	}
@@ -2671,7 +2682,7 @@ void View1::drawAllCharacters(Graphics::ManagedSurface *surface, bool fullUpdate
 			const byte *pixelData = frame._data.data();
 
 			// drawAllCharacters @ 1008:9573-9754: drawAnimFrame / drawAnimFrameShaded / drawAnimFrameDepth
-			const bool clipGameArea = hasScummVerbUI();
+			const bool clipGameArea = hasPersistentActionBar();
 			if (obj->_hasScaling) {
 				drawSpriteTransparent(shadingTableOffset, depthThreshold, scalingFactor,
 									  drawX, drawY, frame._width, frame._height, pixelData, *surface);
@@ -2808,7 +2819,7 @@ void View1::drawInventory(Graphics::ManagedSurface &s) {
 
 	// Draw the buttons at the bottom
 	for (int i = 0; i < 6; i++) {
-		if (hasScummVerbUI() && !isInventorySourceProtagonist() && i == (int)InventoryButtonIndex::Drop)
+		if (hasPersistentActionBar() && !isInventorySourceProtagonist() && i == (int)InventoryButtonIndex::Drop)
 			continue;
 
 		uint16 index = iconIndices[i];
@@ -2904,7 +2915,7 @@ void View1::drawSprite(int16 x, int16 y, uint16 width, uint16 height, byte *data
 				int finalX = x + actualX;
 				int finalY = y + currentY;
 				if (finalX >= 0 && finalX < s.w && finalY >= 0 && finalY < s.h) {
-					if (clipToGameArea && finalY >= kGameHeight)
+					if (clipToGameArea && finalY >= actionBarTopY())
 						continue;
 					// Check for depth
 					uint8 bgDepth = g_engine->_depthMap.getPixel(finalX, finalY);
@@ -3004,7 +3015,7 @@ void View1::drawSpriteScaled(int shadingTableOffset, uint8 depthThreshold, int16
 	int srcRow = 0;
 	int remainingRows = srcHeight;
 	while (remainingRows > 0) {
-		if (screenY >= 0 && screenY < s.h && !(hasScummVerbUI() && screenY >= kGameHeight)) {
+		if (screenY >= 0 && screenY < s.h && !(hasPersistentActionBar() && screenY >= actionBarTopY())) {
 			int screenX = drawX;
 			for (uint16 srcX = 0; srcX < srcWidth; srcX++) {
 				if (screenX >= 0 && screenX < s.w) {
@@ -3034,7 +3045,7 @@ void View1::drawSpriteTransparent(int shadingTableOffset, uint8 depthThreshold,
 	uint16 yScaleAccum = 0;
 
 	while (remainingRows > 0) {
-		if (screenY >= 0 && screenY < s.h && !(hasScummVerbUI() && screenY >= kGameHeight)) {
+		if (screenY >= 0 && screenY < s.h && !(hasPersistentActionBar() && screenY >= actionBarTopY())) {
 			int screenX = drawX;
 			const byte *srcPtr = srcPixels + srcRowOffset;
 			int remainingSrcPixels = (int)srcWidth;
diff --git a/engines/macs2/view1.h b/engines/macs2/view1.h
index 77121774921..1b5351f7642 100644
--- a/engines/macs2/view1.h
+++ b/engines/macs2/view1.h
@@ -27,7 +27,7 @@
 
 namespace Macs2 {
 
-class ScummUI;
+class ActionBar;
 class GameObject;
 
 enum class ViewMode {
@@ -186,7 +186,7 @@ struct ScalingValues {
 };
 
 class View1 : public UIElement {
-	friend class ScummUI;
+	friend class ActionBar;
 
 private:
 	// drawSpriteTransparent @ 1010:0ed1 (drawAnimFrameDepth @ 1010:172c)
@@ -201,7 +201,7 @@ private:
 	// Set by action bar map button on press; enterMapMode() runs on panel release.
 	bool _pendingMapOpen = false;
 
-	ScummUI *_scummUI = nullptr;
+	ActionBar *_actionBar = nullptr;
 
 	// Saved scene visuals for help screen restore (avoids changeScene on exit)
 	byte _savedPalVanilla[256 * 3] = {0};
@@ -351,9 +351,11 @@ public:
 	View1();
 	virtual ~View1();
 
-	bool hasScummVerbUI() const;
-	bool shouldShowScummVerbUI() const;
-	void ensureScummVerbUI();
+	bool hasPersistentActionBar() const;
+	bool shouldShowActionBar() const;
+	void ensureActionBar();
+	/** Top Y of the persistent action bar (game area ends here when shown). */
+	int actionBarTopY() const;
 
 	// g_wHelpButtonDisabled (1020:23B4): when non-zero, help/map button is disabled
 	// and script scene changes use applyScenePaletteEffect instead of palette fades.


Commit: 9214414116f2a88ba3309943320ee628873f15db
    https://github.com/scummvm/scummvm/commit/9214414116f2a88ba3309943320ee628873f15db
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-21T10:43:52+02:00

Commit Message:
MACS2: implemented action bar opcodes

Changed paths:
    engines/macs2/scriptexecutor.cpp


diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 99dd1117efd..f00788f4680 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -3239,14 +3239,65 @@ OpcodeResult ScriptExecutor::scriptRemoveDeltaAnim() {
 }
 
 OpcodeResult ScriptExecutor::scriptSetButtonStep() {
-	debugC(kDebugScript, "SCRIPT::setButtonStep() [stub]");
+	const uint16 buttonIndex = (uint16)(scriptReadValue16() + 0xe000); // 0x2000-based → 1-based
+	const uint16 step = scriptReadValue16();
+	debugC(kDebugScript, "SCRIPT::setButtonStep(button=%u step=%u)", buttonIndex, step);
+	clearScriptError();
 	scriptSkipOpcodeRemainder(0x53);
+
+	if (!_engine->hasNativeHudAssets() || buttonIndex == 0 || buttonIndex > _engine->_hudButtons.size()) {
+		setScriptError(0x2b);
+		return OpcodeResult::Continue;
+	}
+	HudButton &btn = _engine->_hudButtons[buttonIndex - 1];
+	if (btn.animBlob.empty()) {
+		setScriptError(0x2b);
+		return OpcodeResult::Continue;
+	}
+
+	// Extract frame step+100 into the inactive display slot (VESA_AnimStep).
+	Common::Array<uint8> blob = btn.animBlob;
+	const uint32 offset = BackgroundAnimationBlob::advanceAnimFrame(blob, true, (uint16)(step + 0x64));
+	if (offset == 0 || offset + 10 > blob.size()) {
+		setScriptError(0x2c);
+		return OpcodeResult::Continue;
+	}
+	btn.frame._width = READ_LE_UINT16(&blob[offset + 6]);
+	btn.frame._height = READ_LE_UINT16(&blob[offset + 8]);
+	const uint32 pix = (uint32)btn.frame._width * (uint32)btn.frame._height;
+	if (btn.frame._width == 0 || btn.frame._height == 0 ||
+		btn.frame._width > 640 || btn.frame._height > 400 ||
+		offset + 10 + pix > blob.size()) {
+		setScriptError(0x2c);
+		return OpcodeResult::Continue;
+	}
+	btn.frame._data.resize(pix);
+	memcpy(btn.frame._data.data(), &blob[offset + 10], pix);
+	btn.inactiveStep = step;
 	return OpcodeResult::Continue;
 }
 
 OpcodeResult ScriptExecutor::scriptTestButtonAnimFrame() {
-	debugC(kDebugScript, "SCRIPT::testButtonAnimFrame() [stub]");
+	// buttonId is 0x2000-based; compare inactiveStep to [lo,hi].
+	const uint16 buttonIndex = (uint16)(scriptReadValue16() + 0xe000);
+	const uint16 lo = scriptReadValue16();
+	const uint16 hi = scriptReadValue16();
+	debugC(kDebugScript, "SCRIPT::testButtonAnimFrame(button=%u lo=%u hi=%u)", buttonIndex, lo, hi);
+	clearScriptError();
 	scriptSkipOpcodeRemainder(0x54);
+
+	_animBlobRangeTestResult = false;
+	if (!_engine->hasNativeHudAssets() || buttonIndex == 0 || buttonIndex > _engine->_hudButtons.size()) {
+		setScriptError(0x2b);
+		return OpcodeResult::Continue;
+	}
+	const HudButton &btn = _engine->_hudButtons[buttonIndex - 1];
+	if (btn.animBlob.empty()) {
+		setScriptError(0x2b);
+		return OpcodeResult::Continue;
+	}
+	const uint16 step = btn.inactiveStep;
+	_animBlobRangeTestResult = (step >= lo && step <= hi);
 	return OpcodeResult::Continue;
 }
 
@@ -3389,31 +3440,55 @@ OpcodeResult ScriptExecutor::scriptClearDeltaSfxList() {
 OpcodeResult ScriptExecutor::scriptShowActionBar() {
 	debugC(kDebugScript, "SCRIPT::showActionBar()");
 	View1 *currentView = (View1 *)_engine->findView("View1");
-	if (currentView != nullptr && currentView->hasPersistentActionBar()) {
+	if (currentView == nullptr)
+		return OpcodeResult::Continue;
+
+	if (_engine->hasNativeHudAssets()) {
+		// Dialect-v2: if MenuMode==0 → MenuMode=1, restore saved cursor, redraw.
+		if (_engine->_menuMode == 0) {
+			_engine->setBottomHudVisible(true);
+			_engine->setCursorMode(_engine->_savedMenuCursorMode);
+			currentView->updateCursor();
+			currentView->redraw();
+		}
+		return OpcodeResult::Continue;
+	}
+
+	if (currentView->hasPersistentActionBar()) {
 		_engine->setBottomHudVisible(true);
 		currentView->redraw();
 		return OpcodeResult::Continue;
 	}
-	if (currentView != nullptr) {
-		currentView->openScriptActionBar(
-			Common::Point(_engine->screenWidth() / 2, _engine->gameHeight() / 2), _cursorMode);
-	}
+
+	currentView->openScriptActionBar(
+		Common::Point(_engine->screenWidth() / 2, _engine->gameHeight() / 2), _cursorMode);
 	return OpcodeResult::Continue;
 }
 
 OpcodeResult ScriptExecutor::scriptHideActionBar() {
 	debugC(kDebugScript, "SCRIPT::hideActionBar()");
 	View1 *currentView = (View1 *)_engine->findView("View1");
-	if (currentView != nullptr && currentView->hasPersistentActionBar()) {
+	if (currentView == nullptr)
+		return OpcodeResult::Continue;
+
+	if (_engine->hasNativeHudAssets()) {
+		// Dialect-v2: save cursor when leaving main HUD, set MenuMode=0.
+		if (_engine->_menuMode == 1)
+			_engine->_savedMenuCursorMode = _cursorMode;
 		_engine->setBottomHudVisible(false);
 		currentView->redraw();
 		return OpcodeResult::Continue;
 	}
-	if (currentView != nullptr) {
-		MouseMode savedMode = _cursorMode;
-		currentView->closeScriptActionBar(savedMode);
-		_cursorMode = savedMode;
+
+	if (currentView->hasPersistentActionBar()) {
+		_engine->setBottomHudVisible(false);
+		currentView->redraw();
+		return OpcodeResult::Continue;
 	}
+
+	MouseMode savedMode = _cursorMode;
+	currentView->closeScriptActionBar(savedMode);
+	_cursorMode = savedMode;
 	return OpcodeResult::Continue;
 }
 


Commit: 6198477c1fa58edf0a6d5e8f931cd051a9559ef2
    https://github.com/scummvm/scummvm/commit/6198477c1fa58edf0a6d5e8f931cd051a9559ef2
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-21T10:43:52+02:00

Commit Message:
MACS2: implemented sound related v2 opcodes

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


diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 4e92c5cbb04..edb5b8b41a1 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -20,11 +20,13 @@
  */
 
 #include "macs2/macs2.h"
+#include "audio/decoders/wave.h"
 #include "audio/fmopl.h"
 #include "audio/mixer.h"
 #include "common/archive.h"
 #include "common/config-manager.h"
 #include "common/debug.h"
+#include "common/file.h"
 #include "common/ptr.h"
 #include "common/savefile.h"
 #include "common/scummsys.h"
@@ -2228,6 +2230,29 @@ bool Macs2Engine::isSamplePlaying() const {
 	return g_system->getMixer()->isSoundHandleActive(_currentSoundHandle);
 }
 
+void Macs2Engine::playWaveFile(const Common::Path &path) {
+	Common::File *file = new Common::File();
+	if (!file->open(path)) {
+		warning("playWaveFile: cannot open %s", path.toString().c_str());
+		delete file;
+		return;
+	}
+
+	Audio::SeekableAudioStream *stream = Audio::makeWAVStream(file, DisposeAfterUse::YES);
+	if (stream == nullptr) {
+		warning("playWaveFile: not a WAV: %s", path.toString().c_str());
+		return;
+	}
+
+	stopSample();
+	const Common::String pathStr = path.toString('/');
+	const bool isSpeech = pathStr.hasPrefixIgnoreCase("SPEECH/") ||
+						  pathStr.contains("/SPEECH/");
+	const Audio::Mixer::SoundType type =
+		isSpeech ? Audio::Mixer::kSpeechSoundType : Audio::Mixer::kSFXSoundType;
+	g_system->getMixer()->playStream(type, &_currentSoundHandle, stream);
+}
+
 Common::String Macs2Engine::getGameId() const {
 	return _gameDescription->gameId;
 }
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index 2e8fec651f1..f6efd0cd10e 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -534,6 +534,11 @@ public:
 	void playSample();
 	void stopSample();
 	bool isSamplePlaying() const;
+	/**
+	 * Play a WAV from disk (SPEECH/SOUNDFX). Missing/invalid files are ignored
+	 * after a warning. Uses the same mixer handle as playSample.
+	 */
+	void playWaveFile(const Common::Path &path);
 
 	// 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 f00788f4680..c792065a594 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -22,7 +22,9 @@
 #include "macs2/scriptexecutor.h"
 #include "audio/mixer.h"
 #include "common/debug.h"
+#include "common/file.h"
 #include "common/memstream.h"
+#include "common/path.h"
 #include "common/system.h"
 #include "macs2/amiga_archive.h"
 #include "macs2/amiga_decode.h"
@@ -3167,6 +3169,40 @@ void ScriptExecutor::scriptSkipOpcodeRemainder(uint8 opcode) {
 	}
 }
 
+Common::String ScriptExecutor::scriptReadFixedFileName() {
+	Common::String name;
+	for (int i = 0; i < 13 && _stream != nullptr && (uint32)_stream->pos() < _expectedEndLocation; ++i) {
+		const byte c = readByte();
+		if (c != 0)
+			name += (char)c;
+	}
+	return name;
+}
+
+Common::String ScriptExecutor::scriptParsePascalFileName(const Common::String &raw) {
+	if (raw.empty())
+		return Common::String();
+	const uint8 len = (uint8)raw[0];
+	if (len > 0 && len < raw.size())
+		return Common::String(raw.c_str() + 1, len);
+	return raw;
+}
+
+Common::Path ScriptExecutor::resolveAudioFilePath(const Common::String &fileName, bool preferSpeech) const {
+	if (fileName.empty())
+		return Common::Path();
+
+	const char *first = preferSpeech ? "SPEECH" : "SOUNDFX";
+	const char *second = preferSpeech ? "SOUNDFX" : "SPEECH";
+	Common::Path path = Common::Path(first).join(fileName);
+	if (Common::File::exists(path))
+		return path;
+	path = Common::Path(second).join(fileName);
+	if (Common::File::exists(path))
+		return path;
+	return Common::Path();
+}
+
 OpcodeResult ScriptExecutor::scriptNopSkipRemainder() {
 	debugC(kDebugScript, "SCRIPT::%s() [v2 nop]", opcodeName(_lastOpcode));
 	scriptSkipOpcodeRemainder(_lastOpcode);
@@ -3174,20 +3210,52 @@ OpcodeResult ScriptExecutor::scriptNopSkipRemainder() {
 }
 
 OpcodeResult ScriptExecutor::scriptPlaySfx() {
-	debugC(kDebugScript, "SCRIPT::playSfx() [stub]");
+	const Common::String fileName = scriptParsePascalFileName(scriptReadFixedFileName());
+	debugC(kDebugScript, "SCRIPT::playSfx(%s)", fileName.c_str());
 	scriptSkipOpcodeRemainder(0x40);
+
+	if (fileName.empty() || !_soundEnabled)
+		return OpcodeResult::Continue;
+
+	const Common::Path path = resolveAudioFilePath(fileName, false);
+	if (path.empty()) {
+		warning("playSfx: missing %s (looked in SOUNDFX/SPEECH)", fileName.c_str());
+		return OpcodeResult::Continue;
+	}
+	_engine->playWaveFile(path);
 	return OpcodeResult::Continue;
 }
 
 OpcodeResult ScriptExecutor::scriptPlaySong() {
-	debugC(kDebugScript, "SCRIPT::playSong() [stub]");
+	// Dialect-v2 file music (MUSICGS/MUSICOPL). SMF playback is not wired yet;
+	// consume the filename so scripts stay in sync when assets are absent.
+	const Common::String fileName = scriptParsePascalFileName(scriptReadFixedFileName());
+	debugC(kDebugScript, "SCRIPT::playSong(%s)", fileName.c_str());
 	scriptSkipOpcodeRemainder(0x44);
+
+	if (fileName.empty() || !_musicEnabled)
+		return OpcodeResult::Continue;
+
+	Common::Path path = Common::Path("MUSICGS").join(fileName);
+	if (!Common::File::exists(path))
+		path = Common::Path("MUSICOPL").join(fileName);
+	if (!Common::File::exists(path)) {
+		warning("playSong: missing %s (looked in MUSICGS/MUSICOPL)", fileName.c_str());
+		return OpcodeResult::Continue;
+	}
+
+	warning("playSong: MIDI file present but playback not implemented yet (%s)",
+			path.toString().c_str());
 	return OpcodeResult::Continue;
 }
 
 OpcodeResult ScriptExecutor::scriptStopSong() {
-	debugC(kDebugScript, "SCRIPT::stopSong() [stub]");
+	debugC(kDebugScript, "SCRIPT::stopSong()");
 	scriptSkipOpcodeRemainder(0x45);
+	_activeMusicSlot = 0;
+	_waitForAdlibReady = false;
+	if (_engine->getMusic() != nullptr)
+		_engine->getMusic()->stopMusic();
 	return OpcodeResult::Continue;
 }
 
@@ -3536,9 +3604,96 @@ OpcodeResult ScriptExecutor::scriptLoadShadowMask() {
 }
 
 OpcodeResult ScriptExecutor::scriptTalkTo() {
-	debugC(kDebugScript, "SCRIPT::talkTo() [stub]");
+	(void)scriptReadValue32();
+	const int16 x = scriptReadCoord16();
+	const int16 y = scriptReadCoord16();
+	const Common::String voiceFile = scriptParsePascalFileName(scriptReadFixedFileName());
+	const uint16 strOffset = readUint16();
+	const uint16 numLines = readUint16();
+	const uint16 talkTime = scriptReadValue16();
+	debugC(kDebugScript,
+		   "SCRIPT::talkTo(x=%d, y=%d, voice=\"%s\", strOffset=%u, numLines=%u, talkTime=%u)",
+		   x, y, voiceFile.c_str(), strOffset, numLines, talkTime);
 	scriptSkipOpcodeRemainder(0x6D);
-	return OpcodeResult::Continue;
+
+	const uint32 actorIndex = Scenes::instance()._currentActorIndex;
+	clearScriptError();
+	if (actorIndex < 1 || actorIndex > 0x200) {
+		setScriptError(2);
+		endBuffering(_lastOpcodeTriggeredSkip);
+		return OpcodeResult::FinishScript;
+	}
+	GameObject *speaker = GameObjects::getObjectByIndex(actorIndex);
+	if (speaker == nullptr) {
+		setScriptError(0x19);
+		endBuffering(_lastOpcodeTriggeredSkip);
+		return OpcodeResult::FinishScript;
+	}
+	if (speaker->_dataOffset == 0) {
+		setScriptError(2);
+		endBuffering(_lastOpcodeTriggeredSkip);
+		return OpcodeResult::FinishScript;
+	}
+	if (speaker->_blobs.size() < 19 || speaker->_blobs[17].empty() || speaker->_blobs[18].empty()) {
+		setScriptError(6);
+		endBuffering(_lastOpcodeTriggeredSkip);
+		return OpcodeResult::FinishScript;
+	}
+
+	View1 *currentView = (View1 *)_engine->findView("View1");
+	Common::Array<Common::String> strings;
+	if (_executingScriptObjectId == 0) {
+		strings = g_engine->decodeStrings(Scenes::instance()._currentSceneStrings, strOffset, numLines,
+										  Scenes::instance()._currentSceneIndex, 0);
+	} else {
+		Common::MemoryReadStream *s = GameObjects::readGameObjectStrings(_executingScriptObjectId, g_engine->_fileStream);
+		strings = g_engine->decodeStrings(s, strOffset, numLines, 0, _executingScriptObjectId);
+		delete s;
+	}
+
+	_dialogueSpeakerObjectID = (uint16)actorIndex;
+	if (currentView != nullptr) {
+		if (_textEnabled)
+			currentView->showSpeechAct((uint16)actorIndex, strings, Common::Point(x, y), false);
+		else
+			currentView->showSpeechAct((uint16)actorIndex, Common::Array<Common::String>(), Common::Point(x, y), false);
+	}
+
+	if (_cursorMode == MouseMode::Disabled) {
+		_engine->setCursorMode(MouseMode::Walk);
+		if (currentView != nullptr)
+			currentView->updateCursor();
+	}
+
+	bool playingVoice = false;
+	if (_soundEnabled && !voiceFile.empty()) {
+		const Common::Path path = resolveAudioFilePath(voiceFile, true);
+		if (!path.empty()) {
+			_engine->playWaveFile(path);
+			playingVoice = _engine->isSamplePlaying();
+		} else {
+			warning("talkTo: missing voice %s (looked in SPEECH/SOUNDFX)", voiceFile.c_str());
+		}
+	}
+
+	endTimer();
+	endBuffering(_lastOpcodeTriggeredSkip);
+	enterBlockingWaitCursor();
+
+	if (playingVoice) {
+		_waitForPcmSound = true;
+		endFrameWait();
+		return OpcodeResult::WaitForCallback;
+	}
+
+	uint16 waitFrames = talkTime;
+	if (waitFrames == 0) {
+		waitFrames = 0x12;
+		for (const Common::String &line : strings)
+			waitFrames = (uint16)(waitFrames + line.size());
+	}
+	startFrameWait(waitFrames);
+	return OpcodeResult::WaitForCallback;
 }
 
 // Script dialect v2: v1 handlers for 0x01..0x4E with audio remaps, plus 0x4F..0x6D stubs.
diff --git a/engines/macs2/scriptexecutor.h b/engines/macs2/scriptexecutor.h
index 3bef09197fc..f7d695dcfe9 100644
--- a/engines/macs2/scriptexecutor.h
+++ b/engines/macs2/scriptexecutor.h
@@ -23,6 +23,7 @@
 #define MACS2_SCRIPTEXECUTOR_H
 
 #include "common/array.h"
+#include "common/path.h"
 #include "common/rect.h"
 #include "common/scummsys.h"
 #include "common/str-array.h"
@@ -219,6 +220,11 @@ public:
 	OpcodeResult scriptPlaySfx();
 	OpcodeResult scriptPlaySong();
 	OpcodeResult scriptStopSong();
+	/** Fixed 13-byte Pascal-style filename field from the script stream. */
+	Common::String scriptReadFixedFileName();
+	Common::String scriptParsePascalFileName(const Common::String &raw);
+	/** Resolve SPEECH/SOUNDFX path; empty if missing. preferSpeech picks search order. */
+	Common::Path resolveAudioFilePath(const Common::String &fileName, bool preferSpeech) const;
 
 	// Dialect v2: extended opcodes 0x4F..0x6D (stubs until backends exist).
 	OpcodeResult scriptSetMainActor();


Commit: 71edc0dc64f0fb41750c813e04f6f22c2ce85c7b
    https://github.com/scummvm/scummvm/commit/71edc0dc64f0fb41750c813e04f6f22c2ce85c7b
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-21T10:43:52+02:00

Commit Message:
MACS2: implemented talky version support for v1

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


diff --git a/engines/macs2/dialogs.cpp b/engines/macs2/dialogs.cpp
index 57f06f89d51..ac3d15e769c 100644
--- a/engines/macs2/dialogs.cpp
+++ b/engines/macs2/dialogs.cpp
@@ -50,10 +50,12 @@ Macs2OptionsWidget::Macs2OptionsWidget(GuiObject *boss, const Common::String &na
 		_("Fix original bugs"),
 		_("Fixes bugs which were present in the original release, and noticeable graphical/audio glitches."),
 		kEnhancementGroup1Cmd);
+#endif
 	GUI::CheckboxWidget *enh2 = new GUI::CheckboxWidget(widgetsBoss(), _dialogLayout + ".enhancementGroup2",
 		_("Audio-visual improvements"),
-		_("Makes adjustments not related to bugs for certain audio and graphics elements (e.g. version consistency changes)."),
+		_("Optional generated dialogue speech (SPEECH/*.wav) and other audio/visual polish."),
 		kEnhancementGroup2Cmd);
+#if 0
 	GUI::CheckboxWidget *enh3 = new GUI::CheckboxWidget(widgetsBoss(), _dialogLayout + ".enhancementGroup3",
 		_("Restored content"),
 		_("Restores dialogs, graphics, and audio elements which were originally cut in the original release."),
@@ -66,7 +68,9 @@ Macs2OptionsWidget::Macs2OptionsWidget(GuiObject *boss, const Common::String &na
 
 #if 0
 	_enhancementsCheckboxes.push_back(enh1);
+#endif
 	_enhancementsCheckboxes.push_back(enh2);
+#if 0
 	_enhancementsCheckboxes.push_back(enh3);
 #endif
 	_enhancementsCheckboxes.push_back(enh4);
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index edb5b8b41a1..8cf1a179bdf 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -20,7 +20,7 @@
  */
 
 #include "macs2/macs2.h"
-#include "audio/decoders/wave.h"
+#include "audio/audiostream.h"
 #include "audio/fmopl.h"
 #include "audio/mixer.h"
 #include "common/archive.h"
@@ -426,8 +426,7 @@ void Macs2Engine::readImageResources(Common::MemoryReadStream *stream) {
 Macs2Engine::Macs2Engine(OSystem *syst, const ADGameDescription *gameDesc) : Engine(syst),
 																			 _gameDescription(gameDesc) {
 	g_engine = this;
-	_scriptExecutor = new Script::ScriptExecutor();
-	_scriptExecutor->_engine = this;
+	_scriptExecutor = new Script::ScriptExecutor(this);
 	_music = new Music();
 
 	_hotspotOverrides.resize(0x21);
@@ -2230,27 +2229,31 @@ bool Macs2Engine::isSamplePlaying() const {
 	return g_system->getMixer()->isSoundHandleActive(_currentSoundHandle);
 }
 
-void Macs2Engine::playWaveFile(const Common::Path &path) {
-	Common::File *file = new Common::File();
-	if (!file->open(path)) {
-		warning("playWaveFile: cannot open %s", path.toString().c_str());
-		delete file;
-		return;
-	}
+void Macs2Engine::stopSpeech() {
+	Audio::Mixer *mixer = g_system->getMixer();
+	if (mixer->isSoundHandleActive(_speechSoundHandle))
+		mixer->stopHandle(_speechSoundHandle);
+}
 
-	Audio::SeekableAudioStream *stream = Audio::makeWAVStream(file, DisposeAfterUse::YES);
+bool Macs2Engine::isSpeechPlaying() const {
+	return g_system->getMixer()->isSoundHandleActive(_speechSoundHandle);
+}
+
+void Macs2Engine::playDigitalAudioFile(const Common::Path &basename, bool speechBus) {
+	Audio::SeekableAudioStream *stream = Audio::SeekableAudioStream::openStreamFile(basename);
 	if (stream == nullptr) {
-		warning("playWaveFile: not a WAV: %s", path.toString().c_str());
+		debugC(kDebugScript, "playDigitalAudioFile: no audio for %s",
+			   basename.toString().c_str());
 		return;
 	}
 
-	stopSample();
-	const Common::String pathStr = path.toString('/');
-	const bool isSpeech = pathStr.hasPrefixIgnoreCase("SPEECH/") ||
-						  pathStr.contains("/SPEECH/");
-	const Audio::Mixer::SoundType type =
-		isSpeech ? Audio::Mixer::kSpeechSoundType : Audio::Mixer::kSFXSoundType;
-	g_system->getMixer()->playStream(type, &_currentSoundHandle, stream);
+	if (speechBus) {
+		stopSpeech();
+		g_system->getMixer()->playStream(Audio::Mixer::kSpeechSoundType, &_speechSoundHandle, stream);
+	} else {
+		stopSample();
+		g_system->getMixer()->playStream(Audio::Mixer::kSFXSoundType, &_currentSoundHandle, stream);
+	}
 }
 
 Common::String Macs2Engine::getGameId() const {
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index f6efd0cd10e..6cbbb157ed6 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -129,8 +129,8 @@ struct HudButton {
 	uint16 inactiveStep = 0;
 	uint16 activeStep = 0;
 	uint16 hoverStep = 0;
-	uint16 buttonId = 0; // 1=Walk, 2=Look, 3=Talk, 4=Use, 0x33=Options, …
-	uint16 menuId = 0;   // 1=main bar, 2=options, …
+	uint16 buttonId = 0; // 1=Walk, 2=Look, 3=Talk, 4=Use, 0x33=Options, ...
+	uint16 menuId = 0;   // 1=main bar, 2=options, ...
 	AnimFrame frame;
 	AnimFrame activeFrame;
 	AnimFrame hoverFrame;
@@ -535,10 +535,13 @@ public:
 	void stopSample();
 	bool isSamplePlaying() const;
 	/**
-	 * Play a WAV from disk (SPEECH/SOUNDFX). Missing/invalid files are ignored
-	 * after a warning. Uses the same mixer handle as playSample.
+	 * Play digital audio by basename (no extension). Tries flac/ogg/mp3/m4a/wav
+	 * via SeekableAudioStream::openStreamFile (codec #ifdefs live in audio/).
+	 * SPEECH paths use the speech mixer handle; others use the SFX handle.
 	 */
-	void playWaveFile(const Common::Path &path);
+	void playDigitalAudioFile(const Common::Path &basename, bool speechBus);
+	void stopSpeech();
+	bool isSpeechPlaying() const;
 
 	// Offset 50D3h - This is used in 0037:10C4 to terminate the loop
 	uint16 _numHotspots;
@@ -595,7 +598,10 @@ public:
 	Common::Array<uint8> _currentSoundData;
 	int _currentSoundRate = 0x1F40;
 	int _currentSoundHeaderSkip = 2;
+	/** One-shot PCM / SOUNDFX WAV (script SFX). */
 	Audio::SoundHandle _currentSoundHandle;
+	/** Optional SPEECH WAV dialogue (independent of environment SFX). */
+	Audio::SoundHandle _speechSoundHandle;
 
 	// Schedules a run of the script the next time the executor is ticked
 	void scheduleRun(bool initScene = false);
@@ -639,7 +645,7 @@ public:
 	/**
 	 * Bottom HUD / action-bar visibility (dialect-neutral).
 	 * Driven by showActionBar / hideActionBar; Scumm verb strip and native
-	 * HUDs both respect this flag. Native skin also maps visible ↔ menuMode.
+	 * HUDs both respect this flag. Native skin also maps visible <-> menuMode.
 	 */
 	bool isBottomHudVisible() const { return _bottomHudVisible; }
 	void setBottomHudVisible(bool visible);
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index c792065a594..4e1d39dbdc5 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -20,12 +20,14 @@
  */
 
 #include "macs2/scriptexecutor.h"
+#include "audio/audiostream.h"
 #include "audio/mixer.h"
 #include "common/debug.h"
 #include "common/file.h"
 #include "common/memstream.h"
 #include "common/path.h"
 #include "common/system.h"
+#include "engines/enhancements.h"
 #include "macs2/amiga_archive.h"
 #include "macs2/amiga_decode.h"
 #include "macs2/debugtools.h"
@@ -51,7 +53,8 @@ static Common::String joinDebugStrings(const Common::StringArray &strings) {
 #define ScriptNoEntry debugC(kDebugScript, "Unhandled case in script handling.");
 #define STR_HELPER(x) #x
 
-ScriptExecutor::ScriptExecutor() {
+ScriptExecutor::ScriptExecutor(Macs2::Macs2Engine *engine) : _engine(engine) {
+	assert(_engine != nullptr);
 	// Binary: script variable block is 0x2000 bytes = 2048 entries of {uint16 a,
 	// uint16 b}. scriptReadValue (1008:9f4d) accepts indices 1..0x800 and reads
 	// at _g_pScriptVariables + value*4 - 4, so there are exactly 0x800 (2048)
@@ -368,7 +371,7 @@ bool ScriptExecutor::loadSoundResource(Common::Array<uint8> &outData, uint8 reso
 	rateHz = 0x1F40;
 	headerSkip = 2;
 
-	if (_engine != nullptr && _engine->isAmiga()) {
+	if (_engine->isAmiga()) {
 		outData.clear();
 		headerSkip = 0;
 		if (resourceIndex == 0 || _engine->getAmigaArchive() == nullptr)
@@ -406,7 +409,7 @@ bool ScriptExecutor::loadSoundResource(Common::Array<uint8> &outData, uint8 reso
 
 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()) {
+	if (_engine->isAmiga()) {
 		outData.clear();
 		return true;
 	}
@@ -1354,6 +1357,7 @@ OpcodeResult Script::ScriptExecutor::scriptShowDialogue() {
 
 	_dialogueSpeakerObjectID = objectID;
 	currentView->showSpeechAct(objectID, strings, Common::Point(x, y), side);
+	tryPlayGeneratedDialogueSpeech((uint16)offset);
 
 	_waitingForUiClick = true;
 
@@ -3188,21 +3192,61 @@ Common::String ScriptExecutor::scriptParsePascalFileName(const Common::String &r
 	return raw;
 }
 
+Common::String ScriptExecutor::stripAudioExtension(const Common::String &fileName) {
+	static const char *const kExts[] = {
+		".wav", ".ogg", ".mp3", ".flac", ".fla", ".m4a"
+	};
+	for (const char *ext : kExts) {
+		if (fileName.hasSuffixIgnoreCase(ext))
+			return Common::String(fileName.c_str(), fileName.size() - strlen(ext));
+	}
+	return fileName;
+}
+
 Common::Path ScriptExecutor::resolveAudioFilePath(const Common::String &fileName, bool preferSpeech) const {
 	if (fileName.empty())
 		return Common::Path();
 
+	const Common::String base = stripAudioExtension(fileName);
 	const char *first = preferSpeech ? "SPEECH" : "SOUNDFX";
 	const char *second = preferSpeech ? "SOUNDFX" : "SPEECH";
-	Common::Path path = Common::Path(first).join(fileName);
-	if (Common::File::exists(path))
-		return path;
-	path = Common::Path(second).join(fileName);
-	if (Common::File::exists(path))
-		return path;
+
+	// Probe via openStreamFile so flac/ogg/mp3/wav all count as present.
+	for (const char *dir : {first, second}) {
+		const Common::Path basename = Common::Path(dir).join(base);
+		Audio::SeekableAudioStream *probe = Audio::SeekableAudioStream::openStreamFile(basename);
+		if (probe != nullptr) {
+			delete probe;
+			return basename;
+		}
+	}
 	return Common::Path();
 }
 
+void ScriptExecutor::tryPlayGeneratedDialogueSpeech(uint16 stringOffset) {
+	const bool enhOn = _engine->enhancementEnabled(kEnhAudioChanges);
+	Common::String baseName;
+	if (_executingScriptObjectId == 0) {
+		baseName = Common::String::format("s%02x_%04x",
+										  Scenes::instance()._currentSceneIndex, stringOffset);
+	} else {
+		baseName = Common::String::format("o%03x_%04x",
+										  _executingScriptObjectId, stringOffset);
+	}
+	const Common::Path basename = Common::Path("SPEECH").join(baseName);
+
+	debugC(kDebugScript,
+		   "tryPlayGeneratedDialogueSpeech: looking for '%s'.* (enhAudio=%d soundEnabled=%d "
+		   "scriptObject=%u scene=%u offset=%u)",
+		   basename.toString().c_str(), enhOn ? 1 : 0, _soundEnabled ? 1 : 0,
+		   _executingScriptObjectId, Scenes::instance()._currentSceneIndex, stringOffset);
+
+	if (!_soundEnabled || !enhOn)
+		return;
+
+	_engine->playDigitalAudioFile(basename, true);
+}
+
 OpcodeResult ScriptExecutor::scriptNopSkipRemainder() {
 	debugC(kDebugScript, "SCRIPT::%s() [v2 nop]", opcodeName(_lastOpcode));
 	scriptSkipOpcodeRemainder(_lastOpcode);
@@ -3222,7 +3266,8 @@ OpcodeResult ScriptExecutor::scriptPlaySfx() {
 		warning("playSfx: missing %s (looked in SOUNDFX/SPEECH)", fileName.c_str());
 		return OpcodeResult::Continue;
 	}
-	_engine->playWaveFile(path);
+	const bool speechBus = path.toString('/').hasPrefixIgnoreCase("SPEECH/");
+	_engine->playDigitalAudioFile(path, speechBus);
 	return OpcodeResult::Continue;
 }
 
@@ -3307,7 +3352,7 @@ OpcodeResult ScriptExecutor::scriptRemoveDeltaAnim() {
 }
 
 OpcodeResult ScriptExecutor::scriptSetButtonStep() {
-	const uint16 buttonIndex = (uint16)(scriptReadValue16() + 0xe000); // 0x2000-based → 1-based
+	const uint16 buttonIndex = (uint16)(scriptReadValue16() + 0xe000); // 0x2000-based -> 1-based
 	const uint16 step = scriptReadValue16();
 	debugC(kDebugScript, "SCRIPT::setButtonStep(button=%u step=%u)", buttonIndex, step);
 	clearScriptError();
@@ -3512,7 +3557,7 @@ OpcodeResult ScriptExecutor::scriptShowActionBar() {
 		return OpcodeResult::Continue;
 
 	if (_engine->hasNativeHudAssets()) {
-		// Dialect-v2: if MenuMode==0 → MenuMode=1, restore saved cursor, redraw.
+		// Dialect-v2: if MenuMode==0 -> MenuMode=1, restore saved cursor, redraw.
 		if (_engine->_menuMode == 0) {
 			_engine->setBottomHudVisible(true);
 			_engine->setCursorMode(_engine->_savedMenuCursorMode);
@@ -3669,8 +3714,8 @@ OpcodeResult ScriptExecutor::scriptTalkTo() {
 	if (_soundEnabled && !voiceFile.empty()) {
 		const Common::Path path = resolveAudioFilePath(voiceFile, true);
 		if (!path.empty()) {
-			_engine->playWaveFile(path);
-			playingVoice = _engine->isSamplePlaying();
+			_engine->playDigitalAudioFile(path, true);
+			playingVoice = _engine->isSpeechPlaying();
 		} else {
 			warning("talkTo: missing voice %s (looked in SPEECH/SOUNDFX)", voiceFile.c_str());
 		}
diff --git a/engines/macs2/scriptexecutor.h b/engines/macs2/scriptexecutor.h
index f7d695dcfe9..b6b79cefdc4 100644
--- a/engines/macs2/scriptexecutor.h
+++ b/engines/macs2/scriptexecutor.h
@@ -223,8 +223,16 @@ public:
 	/** Fixed 13-byte Pascal-style filename field from the script stream. */
 	Common::String scriptReadFixedFileName();
 	Common::String scriptParsePascalFileName(const Common::String &raw);
-	/** Resolve SPEECH/SOUNDFX path; empty if missing. preferSpeech picks search order. */
+	/** Resolve SPEECH/SOUNDFX basename (no extension) for openStreamFile; empty if none. */
 	Common::Path resolveAudioFilePath(const Common::String &fileName, bool preferSpeech) const;
+	/** Strip a trailing audio extension (.wav/.ogg/...) if present. */
+	static Common::String stripAudioExtension(const Common::String &fileName);
+	/**
+	 * Optional generated dialogue audio (kEnhAudioChanges):
+	 * scene -> SPEECH/sSS_OOOO.*, object -> SPEECH/oOOO_OOOO.*.
+	 * Missing files are ignored.
+	 */
+	void tryPlayGeneratedDialogueSpeech(uint16 stringOffset);
 
 	// Dialect v2: extended opcodes 0x4F..0x6D (stubs until backends exist).
 	OpcodeResult scriptSetMainActor();
@@ -378,7 +386,7 @@ private:
 	uint16 _executingScriptObjectId = 0;
 
 public:
-	ScriptExecutor();
+	ScriptExecutor(Macs2::Macs2Engine *engine);
 	~ScriptExecutor();
 
 	void setIdle() { _state = ExecutorState::Idle; }
@@ -471,7 +479,7 @@ public:
 	// Mutex indicating if the A3D2 function is active
 	bool _isSkipping = false;
 
-	Macs2::Macs2Engine *_engine = nullptr;
+	Macs2::Macs2Engine *_engine;
 
 	// Button 8 skip from handleInput (1008:e8bf)
 	bool skipToEndOfSkippableSection();
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index bbd43a1e005..cabb067c955 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -951,6 +951,10 @@ void View1::handleTextBoxInput() {
 	// then sets g_wIsShowingTextBox = 0. Nothing else.
 	_isShowingTextBox = false;
 	g_engine->_scriptExecutor->_waitingForUiClick = false;
+	// Stop dialogue speech only - leave environment PCM/SFX on _currentSoundHandle.
+	g_engine->stopSpeech();
+	if (!g_engine->isSamplePlaying() && !g_engine->isSpeechPlaying())
+		g_engine->_scriptExecutor->_waitForPcmSound = false;
 	redraw();
 }
 
@@ -959,6 +963,9 @@ void View1::dismissDialoguePanel() {
 	// then sets g_wIsShowingDialoguePanel = 0. Does NOT touch scene+0x53B9.
 	_isShowingDialoguePanel = false;
 	g_engine->_scriptExecutor->_waitingForUiClick = false;
+	g_engine->stopSpeech();
+	if (!g_engine->isSamplePlaying() && !g_engine->isSpeechPlaying())
+		g_engine->_scriptExecutor->_waitForPcmSound = false;
 	redraw();
 }
 
@@ -2469,7 +2476,7 @@ bool View1::tick() {
 				}
 			} else if (executor->_waitForPcmSound) {
 				drawSceneUpdate();
-				if (!g_engine->isSamplePlaying()) {
+				if (!g_engine->isSamplePlaying() && !g_engine->isSpeechPlaying()) {
 					debugC(kDebugScript, "waitForSound complete");
 					executor->debugLogActorWalkState("waitForSound complete");
 					executor->_waitForPcmSound = false;


Commit: 83214e6a544386f3b56f909648d4caaff5c284b6
    https://github.com/scummvm/scummvm/commit/83214e6a544386f3b56f909648d4caaff5c284b6
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-21T10:43:52+02:00

Commit Message:
MACS2: sound opcodes for v2

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


diff --git a/engines/macs2/actionbar.cpp b/engines/macs2/actionbar.cpp
index 9d2cb641823..36dff8db5a5 100644
--- a/engines/macs2/actionbar.cpp
+++ b/engines/macs2/actionbar.cpp
@@ -897,8 +897,7 @@ bool ActionBar::handleClickNative(const Common::Point &pos) {
 		g_engine->_scriptExecutor->_musicEnabled = true;
 	} else if (id == 0x3d) {
 		g_engine->_scriptExecutor->_musicEnabled = false;
-		if (g_engine->getMusic())
-			g_engine->getMusic()->stopMusic();
+		g_engine->getMusic()->stopMusic();
 	} else if (id == 0x3e) {
 		g_engine->_scriptExecutor->_soundEnabled = true;
 	} else if (id == 0x3f) {
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 8cf1a179bdf..030e28db533 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -480,6 +480,14 @@ void Macs2Engine::syncSoundSettings() {
 		_mixer->muteSoundType(Audio::Mixer::kPlainSoundType,
 							  (musicVolume == 0) || (ConfMan.hasKey("mute") && ConfMan.getBool("mute")));
 		_music->setVolume(scaledMusicVolume(_scriptExecutor->_musicControlVolume));
+		_music->setSmfVolumeFromAttenuation(_scriptExecutor->_musicControlVolume);
+	}
+
+	// TalkVol (setWaveVolume): percent of speech loudness when set.
+	if (_talkVol > 0 && _talkVol <= 100) {
+		const int speechVolume = ConfMan.getInt("speech_volume");
+		const int combined = MIN(255, (speechVolume * (int)_talkVol) / 100);
+		_mixer->setVolumeForSoundType(Audio::Mixer::kSpeechSoundType, combined);
 	}
 }
 
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index 6cbbb157ed6..1a7fc4452b2 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -522,6 +522,11 @@ 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;
+	/**
+	 * TalkVol / setWaveVolume percent (0..100). Used to duck SMF while speech plays.
+	 * 0 means unset (duck uses a 50% default).
+	 */
+	uint16 _talkVol = 0;
 	/**
 	 * Install PCM for opcode 0x3E / playSample.
 	 * @param rateHz sample rate (DOS Sound Blaster path uses 8000)
diff --git a/engines/macs2/music.cpp b/engines/macs2/music.cpp
index df6e575327a..5722afcc8f4 100644
--- a/engines/macs2/music.cpp
+++ b/engines/macs2/music.cpp
@@ -22,8 +22,11 @@
 #include "engines/macs2/music.h"
 #include "audio/fmopl.h"
 #include "audio/midiparser.h"
+#include "common/config-manager.h"
 #include "common/endian.h"
+#include "common/file.h"
 #include "common/memstream.h"
+#include "common/util.h"
 #include "engines/macs2/midiparser_macs2.h"
 
 #define CALLBACKS_PER_SECOND 120
@@ -31,7 +34,8 @@
 namespace Macs2 {
 
 Music::Music() : _opl(nullptr), _parser(nullptr), _playing(false),
-				 _masterVolume(0), _numOplChannels(9), _instrumentDataOffset(0) {
+				 _masterVolume(0), _numOplChannels(9), _instrumentDataOffset(0),
+				 _smf(nullptr), _smfDucked(false), _smfVolumeBeforeDuck(192) {
 	memset(_regShadow, 0, sizeof(_regShadow));
 	memset(_voiceAge, 1, sizeof(_voiceAge));
 	memset(_voiceMidiChannel, 0xFF, sizeof(_voiceMidiChannel));
@@ -55,6 +59,8 @@ void Music::init() {
 
 void Music::deinit() {
 	stopMusic();
+	delete _smf;
+	_smf = nullptr;
 	if (_opl) {
 		_opl->stop();
 		delete _opl;
@@ -94,7 +100,8 @@ void Music::silenceAll() {
 }
 
 bool Music::playSongData(const Common::Array<uint8> &data) {
-	stopMusic();
+	stopSmfPlayback();
+	stopAdlibPlayback();
 
 	if (_opl == nullptr)
 		return false;
@@ -135,7 +142,22 @@ bool Music::playSongData(const Common::Array<uint8> &data) {
 	return true;
 }
 
-void Music::stopMusic() {
+bool Music::ensureSmfPlayer() {
+	if (_smf != nullptr)
+		return true;
+	_smf = new SmfMidiPlayer();
+	return _smf != nullptr;
+}
+
+bool Music::playMidiFile(const Common::Path &path, bool loop) {
+	stopAdlibPlayback();
+	if (!ensureSmfPlayer())
+		return false;
+	_smf->playFile(path, loop);
+	return _smf->isPlaying();
+}
+
+void Music::stopAdlibPlayback() {
 	_playing = false;
 	_adlibPlaybackReady = true;
 	if (_parser) {
@@ -149,6 +171,58 @@ void Music::stopMusic() {
 	_masterVolume = 0;
 }
 
+void Music::stopSmfPlayback() {
+	if (_smf != nullptr)
+		_smf->stop();
+	_smfDucked = false;
+}
+
+void Music::stopMusic() {
+	stopAdlibPlayback();
+	stopSmfPlayback();
+}
+
+bool Music::isMidiFilePlaying() const {
+	return _smf != nullptr && _smf->isPlaying();
+}
+
+void Music::setSmfVolumeFromAttenuation(uint16 gameAttenuation) {
+	if (_smf == nullptr || _smfDucked)
+		return;
+	if (ConfMan.hasKey("mute") && ConfMan.getBool("mute")) {
+		_smf->setVolume(0);
+		return;
+	}
+	const int musicVolume = ConfMan.getInt("music_volume");
+	const uint16 atten = (gameAttenuation > 0x3F) ? 0x3F : gameAttenuation;
+	const int vol = musicVolume * (0x3F - atten) / 0x3F;
+	_smf->setVolume(vol);
+}
+
+void Music::syncSmfVolume() {
+	if (_smf == nullptr || _smfDucked)
+		return;
+	_smf->syncVolume();
+}
+
+void Music::setSmfDucked(bool ducked, uint16 talkVolPercent) {
+	if (_smf == nullptr)
+		return;
+	if (ducked) {
+		if (!_smfDucked) {
+			_smfVolumeBeforeDuck = ConfMan.getInt("music_volume");
+			_smfDucked = true;
+		}
+		const int talk = (talkVolPercent > 0 && talkVolPercent <= 100) ? (int)talkVolPercent : 50;
+		const int duckedVol = CLIP((_smfVolumeBeforeDuck * (100 - talk)) / 100, 0, 255);
+		_smf->setVolume(duckedVol);
+	} else if (_smfDucked) {
+		_smfDucked = false;
+		_smf->setVolume(_smfVolumeBeforeDuck);
+		_smf->syncVolume();
+	}
+}
+
 void Music::setVolume(uint16 volume) {
 	if (_opl == nullptr)
 		return;
@@ -455,4 +529,69 @@ void Music::loadData(Common::MemoryReadStream *fileStream, int64 pos, uint16 siz
 	fileStream->read(target, size);
 }
 
+SmfMidiPlayer::SmfMidiPlayer() {
+	createDriver();
+	if (_driver == nullptr)
+		return;
+
+	if (_driver->open() != 0) {
+		warning("SmfMidiPlayer: failed to open MIDI driver");
+		delete _driver;
+		_driver = nullptr;
+		return;
+	}
+
+	if (_nativeMT32)
+		_driver->sendMT32Reset();
+	else
+		_driver->sendGMReset();
+
+	_driver->setTimerCallback(this, &timerCallback);
+}
+
+void SmfMidiPlayer::playFile(const Common::Path &path, bool loop) {
+	if (_driver == nullptr)
+		return;
+
+	Common::File file;
+	if (!file.open(path)) {
+		warning("SmfMidiPlayer: cannot open %s", path.toString().c_str());
+		return;
+	}
+
+	const uint32 size = (uint32)file.size();
+	if (size == 0)
+		return;
+
+	byte *data = (byte *)malloc(size);
+	if (data == nullptr)
+		return;
+	if (file.read(data, size) != size) {
+		free(data);
+		return;
+	}
+
+	// stop() takes the mutex; do not hold it across syncVolume().
+	stop();
+
+	_midiData = data;
+	MidiParser *parser = MidiParser::createParser_SMF();
+	if (!parser->loadMusic(_midiData, size)) {
+		warning("SmfMidiPlayer: not SMF: %s", path.toString().c_str());
+		delete parser;
+		free(_midiData);
+		_midiData = nullptr;
+		return;
+	}
+
+	parser->setTrack(0);
+	parser->setMidiDriver(this);
+	parser->setTimerRate(_driver->getBaseTempo());
+	parser->property(MidiParser::mpCenterPitchWheelOnUnload, 1);
+	_parser = parser;
+	_isLooping = loop;
+	syncVolume();
+	_isPlaying = true;
+}
+
 } // End of namespace Macs2
diff --git a/engines/macs2/music.h b/engines/macs2/music.h
index 64c5a21db10..b74ee6fb150 100644
--- a/engines/macs2/music.h
+++ b/engines/macs2/music.h
@@ -23,7 +23,9 @@
 #define MACS2_MUSIC_H
 
 #include "audio/mididrv.h"
+#include "audio/midiplayer.h"
 #include "common/array.h"
+#include "common/path.h"
 #include "common/scummsys.h"
 
 class MidiParser;
@@ -38,13 +40,27 @@ class OPL;
 
 namespace Macs2 {
 
+/**
+ * Standard MIDI File player (MUSICGS / MUSICOPL .MID) via host MIDI/AdLib driver
+ * Used by dialect-v2 playSong / stopSong
+ */
+class SmfMidiPlayer : public Audio::MidiPlayer {
+public:
+	SmfMidiPlayer();
+	~SmfMidiPlayer() override = default;
+
+	void playFile(const Common::Path &path, bool loop = false);
+};
+
 /**
  * Music facade for the macs2 engine.
  *
  * 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).
+ * Backends:
+ * - MidiParser_Macs2 + direct OPL register writes (DOS music slots)
+ * - SmfMidiPlayer for SMF files (dialect-v2 / Windows MUSICGS|MUSICOPL)
  */
 class Music : public MidiDriver_BASE {
 public:
@@ -54,11 +70,28 @@ public:
 	void init();
 	void deinit();
 
-	/** Start song data on the active backend. Returns false if unavailable or load failed. */
+	/** Start DOS AdLib song data. Returns false if unavailable or load failed. */
 	bool playSongData(const Common::Array<uint8> &data);
+	/** Start an SMF .MID from path (lazy-inits SmfMidiPlayer). Stops AdLib playback. */
+	bool playMidiFile(const Common::Path &path, bool loop = false);
+	/** Stop AdLib and SMF playback. */
 	void stopMusic();
+	/** OPL attenuation volume (0 = loud, 0x3F = silent). */
 	void setVolume(uint16 volume);
+	/**
+	 * Apply game music attenuation to the SMF player (0 = loud, 0x3F = silent),
+	 * scaled by ConfMan music_volume. No-op if SMF backend is not open.
+	 */
+	void setSmfVolumeFromAttenuation(uint16 gameAttenuation);
+	/** Re-read ConfMan music_volume into the SMF player (unless ducked). */
+	void syncSmfVolume();
+	/**
+	 * Duck/restore SMF volume while speech plays (TalkVol / ReduceVol path).
+	 * talkVolPercent: 0..100 speech loudness; higher values duck music more.
+	 */
+	void setSmfDucked(bool ducked, uint16 talkVolPercent = 50);
 	bool isPlaybackReady() const { return _adlibPlaybackReady; }
+	bool isMidiFilePlaying() const;
 	bool hasAdlibBackend() const { return _opl != nullptr; }
 
 	void readDataFromExecutable(Common::MemoryReadStream *fileStream);
@@ -129,6 +162,14 @@ private:
 	Common::Array<uint8> _instrumentData;
 	uint16 _instrumentDataOffset;
 
+	SmfMidiPlayer *_smf;
+	bool _smfDucked;
+	int _smfVolumeBeforeDuck;
+
+	void stopAdlibPlayback();
+	void stopSmfPlayback();
+	bool ensureSmfPlayer();
+
 	// Lookup tables from EXE
 	Common::Array<uint8> _opSlotTable;
 	Common::Array<uint8> _opMap1;
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 4e1d39dbdc5..94ab4cff15a 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -3223,6 +3223,19 @@ Common::Path ScriptExecutor::resolveAudioFilePath(const Common::String &fileName
 	return Common::Path();
 }
 
+Common::Path ScriptExecutor::resolveMidiFilePath(const Common::String &fileName) const {
+	if (fileName.empty())
+		return Common::Path();
+
+	Common::Path path = Common::Path("MUSICGS").join(fileName);
+	if (Common::File::exists(path))
+		return path;
+	path = Common::Path("MUSICOPL").join(fileName);
+	if (Common::File::exists(path))
+		return path;
+	return Common::Path();
+}
+
 void ScriptExecutor::tryPlayGeneratedDialogueSpeech(uint16 stringOffset) {
 	const bool enhOn = _engine->enhancementEnabled(kEnhAudioChanges);
 	Common::String baseName;
@@ -3272,8 +3285,6 @@ OpcodeResult ScriptExecutor::scriptPlaySfx() {
 }
 
 OpcodeResult ScriptExecutor::scriptPlaySong() {
-	// Dialect-v2 file music (MUSICGS/MUSICOPL). SMF playback is not wired yet;
-	// consume the filename so scripts stay in sync when assets are absent.
 	const Common::String fileName = scriptParsePascalFileName(scriptReadFixedFileName());
 	debugC(kDebugScript, "SCRIPT::playSong(%s)", fileName.c_str());
 	scriptSkipOpcodeRemainder(0x44);
@@ -3281,16 +3292,21 @@ OpcodeResult ScriptExecutor::scriptPlaySong() {
 	if (fileName.empty() || !_musicEnabled)
 		return OpcodeResult::Continue;
 
-	Common::Path path = Common::Path("MUSICGS").join(fileName);
-	if (!Common::File::exists(path))
-		path = Common::Path("MUSICOPL").join(fileName);
-	if (!Common::File::exists(path)) {
+	const Common::Path path = resolveMidiFilePath(fileName);
+	if (path.empty()) {
 		warning("playSong: missing %s (looked in MUSICGS/MUSICOPL)", fileName.c_str());
 		return OpcodeResult::Continue;
 	}
 
-	warning("playSong: MIDI file present but playback not implemented yet (%s)",
-			path.toString().c_str());
+	if (!_engine->getMusic()->playMidiFile(path, false)) {
+		warning("playSong: failed to play %s", path.toString().c_str());
+		return OpcodeResult::Continue;
+	}
+
+	_activeMusicSlot = 1;
+	_musicControlMode = 0;
+	_musicControlVolume = 0;
+	_engine->getMusic()->setSmfVolumeFromAttenuation(_musicControlVolume);
 	return OpcodeResult::Continue;
 }
 
@@ -3298,9 +3314,9 @@ OpcodeResult ScriptExecutor::scriptStopSong() {
 	debugC(kDebugScript, "SCRIPT::stopSong()");
 	scriptSkipOpcodeRemainder(0x45);
 	_activeMusicSlot = 0;
+	_musicControlMode = 0;
 	_waitForAdlibReady = false;
-	if (_engine->getMusic() != nullptr)
-		_engine->getMusic()->stopMusic();
+	_engine->getMusic()->stopMusic();
 	return OpcodeResult::Continue;
 }
 
@@ -3486,10 +3502,10 @@ OpcodeResult ScriptExecutor::scriptSetMidiVolume() {
 		setScriptError(0x30);
 		return OpcodeResult::Continue;
 	}
-	// Maps 0=loud..100=silent to OPL attenuation (0..0x3F).
+	// Maps percent (0=silent..100=loud) to OPL attenuation (0x3F..0).
 	_musicControlVolume = (uint16)((100 - volumePercent) * 0x3F / 100);
-	if (_engine->getMusic() != nullptr)
-		_engine->getMusic()->setVolume(_engine->scaledMusicVolume(_musicControlVolume));
+	_engine->getMusic()->setVolume(_engine->scaledMusicVolume(_musicControlVolume));
+	_engine->getMusic()->setSmfVolumeFromAttenuation(_musicControlVolume);
 	return OpcodeResult::Continue;
 }
 
@@ -3502,6 +3518,8 @@ OpcodeResult ScriptExecutor::scriptSetWaveVolume() {
 		setScriptError(0x30);
 		return OpcodeResult::Continue;
 	}
+	// TalkVol / wave volume percent (also used while talking to duck SMF).
+	_engine->_talkVol = volumePercent;
 	const int mixerVolume = volumePercent * 255 / 100;
 	g_system->getMixer()->setVolumeForSoundType(Audio::Mixer::kSFXSoundType, mixerVolume);
 	g_system->getMixer()->setVolumeForSoundType(Audio::Mixer::kSpeechSoundType, mixerVolume);
@@ -3726,6 +3744,7 @@ OpcodeResult ScriptExecutor::scriptTalkTo() {
 	enterBlockingWaitCursor();
 
 	if (playingVoice) {
+		_engine->getMusic()->setSmfDucked(true, _engine->_talkVol);
 		_waitForPcmSound = true;
 		endFrameWait();
 		return OpcodeResult::WaitForCallback;
diff --git a/engines/macs2/scriptexecutor.h b/engines/macs2/scriptexecutor.h
index b6b79cefdc4..51df9c35b29 100644
--- a/engines/macs2/scriptexecutor.h
+++ b/engines/macs2/scriptexecutor.h
@@ -225,6 +225,8 @@ public:
 	Common::String scriptParsePascalFileName(const Common::String &raw);
 	/** Resolve SPEECH/SOUNDFX basename (no extension) for openStreamFile; empty if none. */
 	Common::Path resolveAudioFilePath(const Common::String &fileName, bool preferSpeech) const;
+	/** Resolve MUSICGS then MUSICOPL; empty if neither exists. */
+	Common::Path resolveMidiFilePath(const Common::String &fileName) const;
 	/** Strip a trailing audio extension (.wav/.ogg/...) if present. */
 	static Common::String stripAudioExtension(const Common::String &fileName);
 	/**
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index cabb067c955..f233fe8d89b 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -953,8 +953,10 @@ void View1::handleTextBoxInput() {
 	g_engine->_scriptExecutor->_waitingForUiClick = false;
 	// Stop dialogue speech only - leave environment PCM/SFX on _currentSoundHandle.
 	g_engine->stopSpeech();
-	if (!g_engine->isSamplePlaying() && !g_engine->isSpeechPlaying())
+	if (!g_engine->isSamplePlaying() && !g_engine->isSpeechPlaying()) {
 		g_engine->_scriptExecutor->_waitForPcmSound = false;
+		g_engine->getMusic()->setSmfDucked(false);
+	}
 	redraw();
 }
 
@@ -964,8 +966,10 @@ void View1::dismissDialoguePanel() {
 	_isShowingDialoguePanel = false;
 	g_engine->_scriptExecutor->_waitingForUiClick = false;
 	g_engine->stopSpeech();
-	if (!g_engine->isSamplePlaying() && !g_engine->isSpeechPlaying())
+	if (!g_engine->isSamplePlaying() && !g_engine->isSpeechPlaying()) {
 		g_engine->_scriptExecutor->_waitForPcmSound = false;
+		g_engine->getMusic()->setSmfDucked(false);
+	}
 	redraw();
 }
 
@@ -2480,6 +2484,7 @@ bool View1::tick() {
 					debugC(kDebugScript, "waitForSound complete");
 					executor->debugLogActorWalkState("waitForSound complete");
 					executor->_waitForPcmSound = false;
+					g_engine->getMusic()->setSmfDucked(false);
 					g_engine->runScriptExecutor();
 				}
 			} else if (executor->_waitForMusicControl) {
@@ -2490,7 +2495,9 @@ bool View1::tick() {
 				}
 			} else if (executor->_waitForAdlibReady) {
 				drawSceneUpdate();
-				if (g_engine->getMusic()->isPlaybackReady()) {
+				Music *music = g_engine->getMusic();
+				const bool ready = music->isMidiFilePlaying() ? false : music->isPlaybackReady();
+				if (ready) {
 					executor->_waitForAdlibReady = false;
 					g_engine->runScriptExecutor();
 				}


Commit: 81ad5de3f0b21363bfef7c0bdbcacd25d9649487
    https://github.com/scummvm/scummvm/commit/81ad5de3f0b21363bfef7c0bdbcacd25d9649487
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-21T10:43:52+02:00

Commit Message:
MACS2: started with v2 anim opcodes

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


diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 030e28db533..42c6b95f361 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -2415,14 +2415,15 @@ Common::Point AnimFrame::getBottomMiddleOffset(uint16 scale) const {
 
 AnimFrame BackgroundAnimationBlob::getCurrentFrame() {
 	// Mode 0: read current frame without advancing (draw path uses mode 2 in drawBackgroundAnimations)
-	uint16 offset = advanceAnimFrame(_blob, false, 0x0);
+	Common::Array<uint8> &blob = activeBlob();
+	uint16 offset = advanceAnimFrame(blob, false, 0x0);
 	// offset points to per-frame header: offsetX(2), offsetY(2), unknown(2), width(2), height(2), pixels
 	offset += 6; // skip offsetX, offsetY, unknown
 	AnimFrame result;
-	result._width = READ_LE_UINT16(&_blob[offset]);
-	result._height = READ_LE_UINT16(&_blob[offset + 2]);
+	result._width = READ_LE_UINT16(&blob[offset]);
+	result._height = READ_LE_UINT16(&blob[offset + 2]);
 	result._data.resize(result._width * result._height);
-	memcpy(result._data.data(), &_blob[offset + 4], result._width * result._height);
+	memcpy(result._data.data(), &blob[offset + 4], result._width * result._height);
 	return result;
 }
 
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index 1a7fc4452b2..88a8d5e55c9 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -148,9 +148,22 @@ struct BackgroundAnimationBlob {
 	uint16 _x = 0;
 	uint16 _y = 0;
 	Common::Array<uint8> _blob;
+	/** Dialect-v2 LoadSpecAnimAnim extra slots (1..8); primary remains _blob. */
+	Common::Array<uint8> _extraBlobs[8];
+	uint16 _activeExtraSlot = 0; // 0 = primary _blob; 1..8 = _extraBlobs[slot-1]
 	uint16 _unknown0C = 0; // +0x50F3: purpose unknown (word, read from file, not used at runtime)
 	uint8 _unknown0E = 0;  // +0x50F5: purpose unknown (byte, read from file, not used at runtime)
 	uint8 _unknown0F = 0;  // +0x50F6: purpose unknown (byte, read from file, not used at runtime)
+	Common::Array<uint8> &activeBlob() {
+		if (_activeExtraSlot >= 1 && _activeExtraSlot <= 8)
+			return _extraBlobs[_activeExtraSlot - 1];
+		return _blob;
+	}
+	const Common::Array<uint8> &activeBlob() const {
+		if (_activeExtraSlot >= 1 && _activeExtraSlot <= 8)
+			return _extraBlobs[_activeExtraSlot - 1];
+		return _blob;
+	}
 	AnimFrame getCurrentFrame();
 	static uint16 advanceAnimFrame(Common::Array<uint8> &blob, bool bpp6, uint16 bpp8);
 	static uint16 getAnimFrameCount(Common::Array<uint8> &blob);
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 94ab4cff15a..96130b835be 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -2726,11 +2726,12 @@ OpcodeResult Script::ScriptExecutor::scriptTestSceneAnimFrame() {
 		return OpcodeResult::Continue;
 	}
 	const BackgroundAnimationBlob &blob = _engine->_backgroundAnimationsBlobs[sceneAnimIndex - 1];
-	if (blob._blob.empty()) {
+	const Common::Array<uint8> &active = blob.activeBlob();
+	if (active.empty()) {
 		setScriptError(8);
 		return OpcodeResult::Continue;
 	}
-	AnimBlobView view(blob._blob);
+	AnimBlobView view(active);
 	if (!view.isValid()) {
 		setScriptError(8);
 		return OpcodeResult::Continue;
@@ -3437,15 +3438,62 @@ OpcodeResult ScriptExecutor::scriptScreenShot() {
 }
 
 OpcodeResult ScriptExecutor::scriptWaitObjectAnimStep() {
-	debugC(kDebugScript, "SCRIPT::waitObjectAnimStep() [stub]");
+	const uint32 objectID = scriptReadValue32() - 0x400;
+	const uint16 animNr = scriptReadValue16();
+	const uint16 animStep = scriptReadValue16();
+	debugC(kDebugScript, "SCRIPT::waitObjectAnimStep(objectID=%u, animNr=%u, animStep=%u)",
+		   objectID, animNr, animStep);
 	scriptSkipOpcodeRemainder(0x56);
-	return OpcodeResult::Continue;
+
+	clearScriptError();
+	if (objectID < 1 || objectID > 0x200) {
+		setScriptError(2);
+		return OpcodeResult::Continue;
+	}
+	GameObject *object = GameObjects::getObjectByIndex(objectID);
+	if (object == nullptr) {
+		setScriptError(0x19);
+		return OpcodeResult::Continue;
+	}
+	if (object->_dataOffset == 0) {
+		setScriptError(2);
+		return OpcodeResult::Continue;
+	}
+	if (animNr < 1 || animNr > 0x26) {
+		setScriptError(0x10);
+		return OpcodeResult::Continue;
+	}
+
+	_waitObjectAnimObjectId = (uint16)objectID;
+	_waitObjectAnimSlot = animNr;
+	_waitObjectAnimTargetStep = animStep;
+	_waitForObjectAnimStep = true;
+	endTimer();
+	endBuffering(_lastOpcodeTriggeredSkip);
+	enterBlockingWaitCursor();
+	return OpcodeResult::WaitForCallback;
 }
 
 OpcodeResult ScriptExecutor::scriptWaitSpecialAnimStep() {
-	debugC(kDebugScript, "SCRIPT::waitSpecialAnimStep() [stub]");
+	const uint32 sceneAnimIndex = scriptReadValue32() - 0x1000;
+	const uint16 animStep = scriptReadValue16();
+	debugC(kDebugScript, "SCRIPT::waitSpecialAnimStep(sceneAnimIndex=%u, animStep=%u)",
+		   sceneAnimIndex, animStep);
 	scriptSkipOpcodeRemainder(0x57);
-	return OpcodeResult::Continue;
+
+	clearScriptError();
+	if (sceneAnimIndex == 0 || sceneAnimIndex > _engine->_backgroundAnimationsBlobs.size()) {
+		setScriptError(8);
+		return OpcodeResult::Continue;
+	}
+
+	_waitSpecialAnimIndex = (uint16)sceneAnimIndex;
+	_waitSpecialAnimTargetStep = animStep;
+	_waitForSpecialAnimStep = true;
+	endTimer();
+	endBuffering(_lastOpcodeTriggeredSkip);
+	enterBlockingWaitCursor();
+	return OpcodeResult::WaitForCallback;
 }
 
 OpcodeResult ScriptExecutor::scriptSetObjectAdjust() {
@@ -3475,8 +3523,19 @@ OpcodeResult ScriptExecutor::scriptSetObjectAdjust() {
 }
 
 OpcodeResult ScriptExecutor::scriptReloadSpecialAnim() {
-	debugC(kDebugScript, "SCRIPT::reloadSpecialAnim() [stub]");
+	const uint32 sceneAnimIndex = scriptReadValue32() - 0x1000;
+	const uint8 resourceIndex = readByte();
+	debugC(kDebugScript, "SCRIPT::reloadSpecialAnim(anim=%u res=%u)", sceneAnimIndex, resourceIndex);
+	clearScriptError();
 	scriptSkipOpcodeRemainder(0x59);
+	if (sceneAnimIndex == 0 || sceneAnimIndex > _engine->_backgroundAnimationsBlobs.size()) {
+		setScriptError(8);
+		return OpcodeResult::Continue;
+	}
+	// Needs AHFFANIM resource loader (dialect-v2 / Windows MCS).
+	warning("reloadSpecialAnim: AHFFANIM loader not implemented (anim=%u res=%u)",
+			sceneAnimIndex, resourceIndex);
+	setScriptError(1);
 	return OpcodeResult::Continue;
 }
 
@@ -3527,20 +3586,70 @@ OpcodeResult ScriptExecutor::scriptSetWaveVolume() {
 }
 
 OpcodeResult ScriptExecutor::scriptLoadSpecAnimAnim() {
-	debugC(kDebugScript, "SCRIPT::loadSpecAnimAnim() [stub]");
+	const uint32 sceneAnimIndex = scriptReadValue32() - 0x1000;
+	const uint16 slot = scriptReadValue16();
+	const uint8 resourceIndex = readByte();
+	debugC(kDebugScript, "SCRIPT::loadSpecAnimAnim(anim=%u slot=%u res=%u)",
+		   sceneAnimIndex, slot, resourceIndex);
+	clearScriptError();
 	scriptSkipOpcodeRemainder(0x5E);
+	if (slot == 0 || slot > 8) {
+		setScriptError(0x31);
+		return OpcodeResult::Continue;
+	}
+	if (sceneAnimIndex == 0 || sceneAnimIndex > _engine->_backgroundAnimationsBlobs.size()) {
+		setScriptError(8);
+		return OpcodeResult::Continue;
+	}
+	// Needs AHFFANIM resource loader (dialect-v2 / Windows MCS).
+	warning("loadSpecAnimAnim: AHFFANIM loader not implemented (anim=%u slot=%u res=%u)",
+			sceneAnimIndex, slot, resourceIndex);
+	setScriptError(1);
 	return OpcodeResult::Continue;
 }
 
 OpcodeResult ScriptExecutor::scriptSetSpecAnimAnim() {
-	debugC(kDebugScript, "SCRIPT::setSpecAnimAnim() [stub]");
+	const uint32 sceneAnimIndex = scriptReadValue32() - 0x1000;
+	const uint16 slot = scriptReadValue16();
+	debugC(kDebugScript, "SCRIPT::setSpecAnimAnim(anim=%u slot=%u)", sceneAnimIndex, slot);
+	clearScriptError();
 	scriptSkipOpcodeRemainder(0x5F);
+	if (slot > 8) {
+		setScriptError(0x31);
+		return OpcodeResult::Continue;
+	}
+	if (sceneAnimIndex == 0 || sceneAnimIndex > _engine->_backgroundAnimationsBlobs.size()) {
+		setScriptError(8);
+		return OpcodeResult::Continue;
+	}
+	BackgroundAnimationBlob &blob = _engine->_backgroundAnimationsBlobs[sceneAnimIndex - 1];
+	if (slot != 0 && blob._extraBlobs[slot - 1].empty()) {
+		setScriptError(0x32);
+		return OpcodeResult::Continue;
+	}
+	blob._activeExtraSlot = slot;
 	return OpcodeResult::Continue;
 }
 
 OpcodeResult ScriptExecutor::scriptClearSpecAnimAnim() {
-	debugC(kDebugScript, "SCRIPT::clearSpecAnimAnim() [stub]");
+	const uint32 sceneAnimIndex = scriptReadValue32() - 0x1000;
+	const uint16 slot = scriptReadValue16();
+	debugC(kDebugScript, "SCRIPT::clearSpecAnimAnim(anim=%u slot=%u)", sceneAnimIndex, slot);
+	clearScriptError();
 	scriptSkipOpcodeRemainder(0x60);
+	if (slot > 8) {
+		setScriptError(0x31);
+		return OpcodeResult::Continue;
+	}
+	if (sceneAnimIndex == 0 || sceneAnimIndex > _engine->_backgroundAnimationsBlobs.size()) {
+		setScriptError(8);
+		return OpcodeResult::Continue;
+	}
+	BackgroundAnimationBlob &blob = _engine->_backgroundAnimationsBlobs[sceneAnimIndex - 1];
+	if (slot >= 1 && slot <= 8)
+		blob._extraBlobs[slot - 1].clear();
+	if (blob._activeExtraSlot == slot)
+		blob._activeExtraSlot = 0;
 	return OpcodeResult::Continue;
 }
 
@@ -3964,11 +4073,13 @@ void ScriptExecutor::run(bool firstRun) {
 	// Binary runScriptExecutor (1008:e50c) entry guard:
 	// Returns immediately if ANY wait condition is active.
 	if (_frameWaitTicksRemaining != 0 || _walkTargetObjectIndex != 0 ||
-		_waitForPcmSound || _waitForMusicControl || _waitForAdlibReady) {
-		debugC(kDebugScript, "run() blocked by entry guard: frameWait=%d walkTarget=%d sound=%d music=%d adlib=%d",
+		_waitForPcmSound || _waitForMusicControl || _waitForAdlibReady ||
+		_waitForObjectAnimStep || _waitForSpecialAnimStep) {
+		debugC(kDebugScript, "run() blocked by entry guard: frameWait=%d walkTarget=%d sound=%d music=%d adlib=%d objAnim=%d specAnim=%d",
 			   _frameWaitTicksRemaining, _walkTargetObjectIndex,
 			   _waitForPcmSound ? 1 : 0, _waitForMusicControl ? 1 : 0,
-			   _waitForAdlibReady ? 1 : 0);
+			   _waitForAdlibReady ? 1 : 0,
+			   _waitForObjectAnimStep ? 1 : 0, _waitForSpecialAnimStep ? 1 : 0);
 		return;
 	}
 
diff --git a/engines/macs2/scriptexecutor.h b/engines/macs2/scriptexecutor.h
index 51df9c35b29..8b12642eb49 100644
--- a/engines/macs2/scriptexecutor.h
+++ b/engines/macs2/scriptexecutor.h
@@ -472,6 +472,14 @@ public:
 	bool _waitForPcmSound = false;
 	bool _waitForMusicControl = false;
 	bool _waitForAdlibReady = false;
+	// Dialect-v2: wait until an object/scene anim sequencePosition reaches a target.
+	bool _waitForObjectAnimStep = false;
+	uint16 _waitObjectAnimObjectId = 0;
+	uint16 _waitObjectAnimSlot = 0;
+	uint16 _waitObjectAnimTargetStep = 0;
+	bool _waitForSpecialAnimStep = false;
+	uint16 _waitSpecialAnimIndex = 0;
+	uint16 _waitSpecialAnimTargetStep = 0;
 	bool _debugPaused = false;
 	bool _pickupInProgress = false;
 	uint16 _pickupActorObjectID = 0;
@@ -519,7 +527,8 @@ public:
 	bool isScriptWaitDeferred() const {
 		return _state == ExecutorState::WaitingForCallback ||
 			   _frameWaitTicksRemaining != 0 || _walkTargetObjectIndex != 0 ||
-			   _waitForPcmSound || _waitForMusicControl || _waitForAdlibReady;
+			   _waitForPcmSound || _waitForMusicControl || _waitForAdlibReady ||
+			   _waitForObjectAnimStep || _waitForSpecialAnimStep;
 	}
 
 	bool isExecuting() const {
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index f233fe8d89b..5c20b84d6bc 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -536,27 +536,28 @@ void View1::drawBackgroundAnimations(Graphics::ManagedSurface &s) {
 	for (int i = 0; i < (int)g_engine->_backgroundAnimations.size(); i++) {
 		BackgroundAnimation &current = g_engine->_backgroundAnimations[i];
 		BackgroundAnimationBlob &currentBlob = g_engine->_backgroundAnimationsBlobs[i];
+		Common::Array<uint8> &blob = currentBlob.activeBlob();
 		// Binary drawAllCharacters (1008:90a2): null bg-anim blob -> error 0x08;
 		// zero frame count -> error 0x0B; aborts entire draw pass.
-		if (currentBlob._blob.empty()) {
+		if (blob.empty()) {
 			g_engine->_scriptExecutor->setScriptError(8);
 			return;
 		}
-		AnimBlobView view(currentBlob._blob);
+		AnimBlobView view(blob);
 		if (!view.isValid() || view.frameCount() == 0) {
 			g_engine->_scriptExecutor->setScriptError(view.frameCount() == 0 ? 0x0B : 8);
 			return;
 		}
 		// Binary drawAllCharacters (1008:929c): drawAnimFrame(2, y, x+1, blob) - one
 		// advanceAnimFrame(save=1, mode=2) per frame, not a separate tick advance.
-		uint16 frameStart = BackgroundAnimationBlob::advanceAnimFrame(currentBlob._blob, true, 2);
-		int16 frameOffsetX = (int16)READ_LE_UINT16(&currentBlob._blob[frameStart]);
-		int16 frameOffsetY = (int16)READ_LE_UINT16(&currentBlob._blob[frameStart + 2]);
+		uint16 frameStart = BackgroundAnimationBlob::advanceAnimFrame(blob, true, 2);
+		int16 frameOffsetX = (int16)READ_LE_UINT16(&blob[frameStart]);
+		int16 frameOffsetY = (int16)READ_LE_UINT16(&blob[frameStart + 2]);
 		AnimFrame currentFrame;
-		currentFrame._width = READ_LE_UINT16(&currentBlob._blob[frameStart + 6]);
-		currentFrame._height = READ_LE_UINT16(&currentBlob._blob[frameStart + 8]);
+		currentFrame._width = READ_LE_UINT16(&blob[frameStart + 6]);
+		currentFrame._height = READ_LE_UINT16(&blob[frameStart + 8]);
 		currentFrame._data.resize(currentFrame._width * currentFrame._height);
-		memcpy(currentFrame._data.data(), &currentBlob._blob[frameStart + 10],
+		memcpy(currentFrame._data.data(), &blob[frameStart + 10],
 			   currentFrame._width * currentFrame._height);
 		drawSprite(current._x + 1 + frameOffsetX, current._y + frameOffsetY, currentFrame, s, false);
 	}
@@ -1849,6 +1850,8 @@ bool View1::handleInput(const MouseDownMessage &msg) {
 				!g_engine->_scriptExecutor->_waitForPcmSound &&
 				!g_engine->_scriptExecutor->_waitForMusicControl &&
 				!g_engine->_scriptExecutor->_waitForAdlibReady &&
+				!g_engine->_scriptExecutor->_waitForObjectAnimStep &&
+				!g_engine->_scriptExecutor->_waitForSpecialAnimStep &&
 				g_engine->_scriptExecutor->canOpenSaveMenu()) {
 				if (ConfMan.getBool("original_menus")) {
 					// Binary handleInput (1008:f2af): saves cursor mode before opening panel
@@ -2501,6 +2504,44 @@ bool View1::tick() {
 					executor->_waitForAdlibReady = false;
 					g_engine->runScriptExecutor();
 				}
+			} else if (executor->_waitForObjectAnimStep) {
+				drawSceneUpdate();
+				bool animStepReached = false;
+				GameObject *waitObject = GameObjects::getObjectByIndex(executor->_waitObjectAnimObjectId);
+				if (waitObject != nullptr && waitObject->_dataOffset != 0) {
+					const Common::Array<uint8> *blob = waitObject->getAnimSlotBlob(executor->_waitObjectAnimSlot);
+					if (blob != nullptr && !blob->empty()) {
+						AnimBlobView view(*blob);
+						if (view.isValid())
+							animStepReached = view.sequencePosition() >= executor->_waitObjectAnimTargetStep;
+					}
+				}
+				if (animStepReached) {
+					debugC(kDebugScript, "waitObjectAnimStep complete obj=%u slot=%u step=%u",
+						   executor->_waitObjectAnimObjectId, executor->_waitObjectAnimSlot,
+						   executor->_waitObjectAnimTargetStep);
+					executor->_waitForObjectAnimStep = false;
+					g_engine->runScriptExecutor();
+				}
+			} else if (executor->_waitForSpecialAnimStep) {
+				drawSceneUpdate();
+				bool animStepReached = false;
+				const uint16 animIndex = executor->_waitSpecialAnimIndex;
+				if (animIndex > 0 && animIndex <= g_engine->_backgroundAnimationsBlobs.size()) {
+					const BackgroundAnimationBlob &blob = g_engine->_backgroundAnimationsBlobs[animIndex - 1];
+					const Common::Array<uint8> &active = blob.activeBlob();
+					if (!active.empty()) {
+						AnimBlobView view(active);
+						if (view.isValid())
+							animStepReached = view.sequencePosition() >= executor->_waitSpecialAnimTargetStep;
+					}
+				}
+				if (animStepReached) {
+					debugC(kDebugScript, "waitSpecialAnimStep complete anim=%u step=%u",
+						   executor->_waitSpecialAnimIndex, executor->_waitSpecialAnimTargetStep);
+					executor->_waitForSpecialAnimStep = false;
+					g_engine->runScriptExecutor();
+				}
 			}
 		} else {
 			drawSceneUpdate();


Commit: 1e44c0ed297013052b4f9e0ba1672787632f68f0
    https://github.com/scummvm/scummvm/commit/1e44c0ed297013052b4f9e0ba1672787632f68f0
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-21T10:43:52+02:00

Commit Message:
MACS2: v2 anim loading

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


diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 42c6b95f361..d274547bab2 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -926,41 +926,371 @@ void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
 	}
 }
 
-bool Macs2Engine::loadOverlayFont(uint8 resourceIndex, uint16 executingObjectID) {
-	if (isAmiga())
-		return loadAmigaOverlayFont(resourceIndex);
+bool Macs2Engine::resolveResourceFileOffset(uint8 resourceIndex, uint16 executingObjectId, uint32 &outOffset) const {
+	outOffset = 0;
+	if (resourceIndex == 0 || _fileStream == nullptr)
+		return false;
+
+	if (executingObjectId == 0) {
+		if (resourceIndex > _sceneResourceOffsets.size())
+			return false;
+		outOffset = _sceneResourceOffsets[resourceIndex - 1];
+	} else {
+		GameObject *object = GameObjects::getObjectByIndex(executingObjectId);
+		if (object == nullptr || object->_dataOffset == 0)
+			return false;
+		if ((uint)(resourceIndex - 1) >= maxObjectResources())
+			return false;
+		outOffset = object->_resourceOffsets[resourceIndex - 1];
+	}
+	return outOffset != 0 && outOffset < (uint32)_fileStream->size();
+}
 
-	// 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.
-	if (resourceIndex == 0)
+bool Macs2Engine::loadSizedResourcePayload(uint8 resourceIndex, uint16 executingObjectId,
+										   Common::Array<uint8> &outPayload) {
+	outPayload.clear();
+	uint32 address = 0;
+	if (!resolveResourceFileOffset(resourceIndex, executingObjectId, address))
 		return false;
 
 	const int64 oldPos = _fileStream->pos();
-	uint32 address = 0;
+	_fileStream->seek(address, SEEK_SET);
+	const uint32 size = _fileStream->readUint32LE();
+	if (size == 0 || size > 0x1000000) {
+		_fileStream->seek(oldPos, SEEK_SET);
+		return false;
+	}
+	outPayload.resize(size);
+	if (_fileStream->read(outPayload.data(), size) != size) {
+		outPayload.clear();
+		_fileStream->seek(oldPos, SEEK_SET);
+		return false;
+	}
+	_fileStream->seek(oldPos, SEEK_SET);
+	return !outPayload.empty();
+}
 
-	if (executingObjectID == 0) {
-		if (resourceIndex > _sceneResourceOffsets.size()) {
-			_fileStream->seek(oldPos, SEEK_SET);
+bool Macs2Engine::loadAhffAnimResource(uint8 resourceIndex, uint16 executingObjectId,
+									   Common::Array<uint8> &outBlob) {
+	Common::Array<uint8> payload;
+	if (!loadSizedResourcePayload(resourceIndex, executingObjectId, payload))
+		return false;
+	if (payload.size() < 12 || memcmp(payload.data(), "AHFFANIM0100", 12) != 0)
+		return false;
+	outBlob.clear();
+	outBlob.resize(payload.size() - 12);
+	if (!outBlob.empty())
+		memcpy(outBlob.data(), payload.data() + 12, outBlob.size());
+	return !outBlob.empty();
+}
+
+bool Macs2Engine::readMegaPicImage(Common::SeekableReadStream *stream, int width, int height,
+								   Graphics::ManagedSurface &out) {
+	if (stream == nullptr || width <= 0 || height <= 0)
+		return false;
+
+	out.create(width, height, Graphics::PixelFormat::createFormatCLUT8());
+	Common::Array<byte> rowBuf;
+	rowBuf.resize(3000);
+
+	for (int y = 0; y < height; y++) {
+		uint16 packedLen = stream->readUint16LE();
+		if (packedLen == 0 || packedLen > 2999)
 			return false;
-		}
-		address = _sceneResourceOffsets[resourceIndex - 1];
-	} else {
-		GameObject *object = GameObjects::getObjectByIndex(executingObjectID);
-		if (object == nullptr || object->_dataOffset == 0) {
-			_fileStream->seek(oldPos, SEEK_SET);
+		if (stream->read(rowBuf.data(), packedLen) != packedLen)
 			return false;
+
+		int x = 0;
+		uint i = 0;
+		while (x < width && i < packedLen) {
+			const byte code = rowBuf[i++];
+			if (code < 0x80) {
+				const uint run = code;
+				for (uint n = 0; n < run && x < width; n++) {
+					if (i >= packedLen)
+						return false;
+					out.setPixel(x++, y, rowBuf[i++]);
+				}
+			} else {
+				if (i >= packedLen)
+					return false;
+				const byte value = rowBuf[i++];
+				const uint run = code & 0x7F;
+				for (uint n = 0; n < run && x < width; n++)
+					out.setPixel(x++, y, value);
+			}
 		}
-		_fileStream->seek(object->_dataOffset + 0x189 + (resourceIndex - 1) * 4, SEEK_SET);
-		address = _fileStream->readUint32LE();
 	}
+	return true;
+}
 
-	if (address == 0) {
+bool Macs2Engine::loadMaskFromResource(uint8 resourceIndex, uint16 executingObjectId,
+									   Graphics::ManagedSurface &dest, int megapicW, int megapicH,
+									   bool upscaleHalfRes) {
+	uint32 address = 0;
+	if (!resolveResourceFileOffset(resourceIndex, executingObjectId, address))
+		return false;
+
+	const int64 oldPos = _fileStream->pos();
+	_fileStream->seek(address, SEEK_SET);
+	(void)_fileStream->readUint32LE(); // size header skipped by Load*Mask
+	Graphics::ManagedSurface half;
+	Graphics::ManagedSurface &target = upscaleHalfRes ? half : dest;
+	if (!readMegaPicImage(_fileStream, megapicW, megapicH, target)) {
+		_fileStream->seek(oldPos, SEEK_SET);
+		return false;
+	}
+	if (upscaleHalfRes) {
+		dest.create(megapicW * 2, megapicH * 2, Graphics::PixelFormat::createFormatCLUT8());
+		for (int y = 0; y < half.h; y++) {
+			for (int x = 0; x < half.w; x++) {
+				const byte p = half.getPixel(x, y);
+				const int dx = x * 2;
+				const int dy = y * 2;
+				dest.setPixel(dx, dy, p);
+				dest.setPixel(dx + 1, dy, p);
+				dest.setPixel(dx, dy + 1, p);
+				dest.setPixel(dx + 1, dy + 1, p);
+			}
+		}
+	}
+	_fileStream->seek(oldPos, SEEK_SET);
+	return true;
+}
+
+void Macs2Engine::clearDeltaAnim() {
+	_deltaAnim.clear(screenWidth(), gameHeight());
+}
+
+bool Macs2Engine::loadDeltaAnimResource(uint8 resourceIndex, uint16 executingObjectId, bool forceSkipSpeed1) {
+	uint32 address = 0;
+	if (!resolveResourceFileOffset(resourceIndex, executingObjectId, address))
+		return false;
+
+	const int64 oldPos = _fileStream->pos();
+	_fileStream->seek(address, SEEK_SET);
+	const uint32 size = _fileStream->readUint32LE();
+	char magic[8];
+	if (_fileStream->read(magic, 8) != 8 || memcmp(magic, "AHFFDLTA", 8) != 0) {
+		_fileStream->seek(oldPos, SEEK_SET);
+		return false;
+	}
+	_fileStream->skip(4); // remainder of 16-byte header after size
+
+	// LoadDeltaAnim SkipSpeed layouts:
+	//   1: frameCount, 0x1000 offset table, skip 0x2000, palette, frames
+	//   2: frameCount, skip 0x1000, 0x1000 table, skip 0x1000; frame counts halved
+	//   else: frameCount, skip 0x2000, 0x1000 table; frame counts / 3
+	// CheckDeltaSpeed always uses layout 1 regardless of SkipSpeed.
+	uint16 frameCount = _fileStream->readUint16LE();
+	if (frameCount == 0 || frameCount > 512) {
 		_fileStream->seek(oldPos, SEEK_SET);
 		return false;
 	}
 
-	// Seek to address + 0x10 (original skips 16-byte resource header)
+	uint16 skipSpeed = (_skipSpeed >= 1 && _skipSpeed <= 4) ? _skipSpeed : 1;
+	if (forceSkipSpeed1)
+		skipSpeed = 1;
+	Common::Array<uint32> relOffsets;
+	relOffsets.resize(512);
+	// FBlockRead(0x1000): 512 uint32 offsets (0x800) plus 0x800 trailing bytes.
+	auto readOffsetTable1000 = [&]() {
+		for (uint i = 0; i < 512; i++)
+			relOffsets[i] = _fileStream->readUint32LE();
+		_fileStream->skip(0x800);
+	};
+	if (skipSpeed == 1) {
+		readOffsetTable1000();
+		_fileStream->skip(0x2000);
+	} else if (skipSpeed == 2) {
+		_fileStream->skip(0x1000);
+		readOffsetTable1000();
+		_fileStream->skip(0x1000);
+		frameCount = (uint16)(((uint32)frameCount + 1) >> 1);
+		if (frameCount > 0)
+			frameCount--;
+	} else {
+		_fileStream->skip(0x2000);
+		readOffsetTable1000();
+		frameCount = (uint16)(((uint32)frameCount + 1) / 3);
+		if (frameCount > 0)
+			frameCount--;
+	}
+	if (frameCount == 0 || frameCount > 512) {
+		_fileStream->seek(oldPos, SEEK_SET);
+		return false;
+	}
+
+	// Scripts call addDeltaSfx before playDiskDelta; keep the pending SFX list.
+	Common::Array<DeltaSfxEvent> savedSfx = Common::move(_deltaAnim.sfxEvents);
+	clearDeltaAnim();
+	_deltaAnim.sfxEvents = Common::move(savedSfx);
+	_fileStream->read(_deltaAnim.palette, 0x300);
+	_deltaAnim.frames.resize(frameCount);
+	_deltaAnim.frameCount = frameCount;
+	_deltaAnim.loaded = true;
+
+	const uint32 base = address + 4;
+	for (uint16 fi = 0; fi < frameCount; fi++) {
+		const uint32 absOff = relOffsets[fi] + base;
+		if (absOff >= (uint32)_fileStream->size())
+			continue;
+		_fileStream->seek(absOff, SEEK_SET);
+		const uint16 stripCount = _fileStream->readUint16LE();
+		DeltaFrame &frame = _deltaAnim.frames[fi];
+		frame.strips.clear();
+		if (stripCount == 0 || stripCount > 400)
+			continue;
+		frame.strips.resize(stripCount);
+		for (uint16 si = 0; si < stripCount; si++) {
+			frame.strips[si].y = _fileStream->readUint16LE();
+			const uint16 rleSize = _fileStream->readUint16LE();
+			if (rleSize == 0 || rleSize > 0x8000)
+				break;
+			frame.strips[si].rle.resize(rleSize);
+			if (_fileStream->read(frame.strips[si].rle.data(), rleSize) != rleSize) {
+				frame.strips[si].rle.clear();
+				break;
+			}
+		}
+	}
+
+	(void)size;
+	_fileStream->seek(oldPos, SEEK_SET);
+	return _deltaAnim.loaded;
+}
+
+void Macs2Engine::applyDeltaFrameToBackground(const DeltaFrame &frame) {
+	if (_sceneBackground.w <= 0 || _sceneBackground.h <= 0)
+		return;
+
+	for (const DeltaStrip &strip : frame.strips) {
+		const int y = (int)strip.y;
+		if (y < (int)_deltaAnim.clipMiY || y > (int)_deltaAnim.clipMaY)
+			continue;
+		if (y < 0 || y >= _sceneBackground.h)
+			continue;
+		if (strip.rle.empty())
+			continue;
+
+		const uint8 *p = strip.rle.data();
+		const uint8 *end = p + strip.rle.size();
+		int x = 0;
+		while (p + 4 <= end) {
+			const int16 skip = (int16)READ_LE_UINT16(p);
+			p += 2;
+			uint16 runLen = READ_LE_UINT16(p);
+			p += 2;
+			x += skip;
+			if (runLen == 0)
+				break;
+			while (runLen != 0 && p < end) {
+				uint8 code = *p++;
+				if (code < 0x80) {
+					uint16 n = code;
+					if (n > runLen)
+						n = runLen;
+					for (uint16 i = 0; i < n && p < end; i++, x++) {
+						if (x >= (int)_deltaAnim.clipMiX && x <= (int)_deltaAnim.clipMaX &&
+							x >= 0 && x < _sceneBackground.w)
+							_sceneBackground.setPixel(x, y, *p);
+						p++;
+					}
+					runLen = (uint16)(runLen - n);
+				} else {
+					uint16 n = (uint16)(code - 0x80);
+					if (n > runLen)
+						n = runLen;
+					if (p >= end)
+						break;
+					const uint8 val = *p++;
+					for (uint16 i = 0; i < n; i++, x++) {
+						if (x >= (int)_deltaAnim.clipMiX && x <= (int)_deltaAnim.clipMaX &&
+							x >= 0 && x < _sceneBackground.w)
+							_sceneBackground.setPixel(x, y, val);
+					}
+					runLen = (uint16)(runLen - n);
+				}
+			}
+		}
+	}
+}
+
+void Macs2Engine::playDeltaFrameSfx(uint16 displayFrame) {
+	for (const DeltaSfxEvent &ev : _deltaAnim.sfxEvents) {
+		if (ev.frameIndex != displayFrame || ev.fileName.empty())
+			continue;
+		if (ev.duckMusic)
+			getMusic()->setSmfDucked(true, _talkVol);
+		const Common::String base = Script::ScriptExecutor::stripAudioExtension(ev.fileName);
+		playDigitalAudioFile(Common::Path("SOUNDFX").join(base), false);
+	}
+}
+
+bool Macs2Engine::startDeltaPlayback(uint16 startFrame, uint16 endFrame, uint16 speedTicks, bool applyPalette) {
+	if (!_deltaAnim.loaded || _deltaAnim.frameCount == 0)
+		return false;
+	uint16 start = startFrame ? startFrame : 1;
+	uint16 end = endFrame;
+	if (end == 0 || end > _deltaAnim.frameCount)
+		end = _deltaAnim.frameCount;
+	if (start > end)
+		start = end;
+	_deltaAnim.startFrame = (uint16)(start - 1);
+	_deltaAnim.endFrame = (uint16)(end - 1);
+	_deltaAnim.currentFrame = _deltaAnim.startFrame;
+	_deltaAnim.speedTicks = speedTicks ? speedTicks : 1;
+	_deltaAnim.tickCounter = 0;
+	_deltaAnim.playing = true;
+	_deltaAnim.applyPaletteOnStart = applyPalette;
+	if (applyPalette || _deltaAnim.currentFrame == 0) {
+		memcpy(_palVanilla, _deltaAnim.palette, 0x300);
+		memcpy(_pal, _deltaAnim.palette, 0x300);
+		for (int i = 0; i < 256 * 3; i++)
+			_pal[i] = (_pal[i] * 259 + 33) >> 6;
+		g_system->getPaletteManager()->setPalette(_pal, 0, 256);
+	}
+	const uint16 displayFrame = _deltaAnim.currentFrame;
+	playDeltaFrameSfx(displayFrame);
+	if (displayFrame < _deltaAnim.frames.size())
+		applyDeltaFrameToBackground(_deltaAnim.frames[displayFrame]);
+	_deltaAnim.currentFrame++;
+	if (_deltaAnim.currentFrame > _deltaAnim.endFrame)
+		_deltaAnim.playing = false;
+	return true;
+}
+
+bool Macs2Engine::tickDeltaPlayback() {
+	if (!_deltaAnim.playing)
+		return false;
+	_deltaAnim.tickCounter++;
+	if (_deltaAnim.tickCounter < _deltaAnim.speedTicks)
+		return true;
+	_deltaAnim.tickCounter = 0;
+
+	const uint16 displayFrame = _deltaAnim.currentFrame;
+	playDeltaFrameSfx(displayFrame);
+	if (displayFrame < _deltaAnim.frames.size())
+		applyDeltaFrameToBackground(_deltaAnim.frames[displayFrame]);
+	_deltaAnim.currentFrame++;
+	if (_deltaAnim.currentFrame > _deltaAnim.endFrame) {
+		_deltaAnim.playing = false;
+		getMusic()->setSmfDucked(false);
+		return false;
+	}
+	return true;
+}
+
+bool Macs2Engine::loadOverlayFont(uint8 resourceIndex, uint16 executingObjectID) {
+	if (isAmiga())
+		return loadAmigaOverlayFont(resourceIndex);
+
+	// Original (1008:d749): resource table offset, then seek address+0x10 and loadFontData.
+	uint32 address = 0;
+	if (!resolveResourceFileOffset(resourceIndex, executingObjectID, address))
+		return false;
+
+	const int64 oldPos = _fileStream->pos();
 	_fileStream->seek(address + 0x10, SEEK_SET);
 	const uint16 glyphCount = _fileStream->readUint16LE();
 	if (glyphCount == 0 || glyphCount > 256) {
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index 88a8d5e55c9..a0540c6b0c5 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -148,7 +148,7 @@ struct BackgroundAnimationBlob {
 	uint16 _x = 0;
 	uint16 _y = 0;
 	Common::Array<uint8> _blob;
-	/** Dialect-v2 LoadSpecAnimAnim extra slots (1..8); primary remains _blob. */
+	/** Dialect-v2 special-anim extra slots (1..8); primary remains _blob. */
 	Common::Array<uint8> _extraBlobs[8];
 	uint16 _activeExtraSlot = 0; // 0 = primary _blob; 1..8 = _extraBlobs[slot-1]
 	uint16 _unknown0C = 0; // +0x50F3: purpose unknown (word, read from file, not used at runtime)
@@ -415,6 +415,78 @@ public:
 	uint16 numPanelGlyphs = 0;
 	uint16 maxPanelGlyphHeight = 0;
 	bool loadOverlayFont(uint8 resourceIndex, uint16 executingObjectID);
+	/**
+	 * Resolve scene/object resource table entry to an absolute MCS file offset.
+	 * Shared by sized-resource loads, AHFFANIM, AHFFDLTA, and MegaPic masks.
+	 */
+	bool resolveResourceFileOffset(uint8 resourceIndex, uint16 executingObjectId, uint32 &outOffset) const;
+	/** Read size-prefixed resource payload (size dword excluded from outPayload). */
+	bool loadSizedResourcePayload(uint8 resourceIndex, uint16 executingObjectId, Common::Array<uint8> &outPayload);
+	/** Decode AHFFANIM0100 body into a runtime anim blob. */
+	bool loadAhffAnimResource(uint8 resourceIndex, uint16 executingObjectId, Common::Array<uint8> &outBlob);
+	/** Decode MegaPic row-RLE into dest (width x height). */
+	bool readMegaPicImage(Common::SeekableReadStream *stream, int width, int height, Graphics::ManagedSurface &out);
+	/**
+	 * Load a MegaPic mask resource into dest.
+	 * If upscaleHalfRes, decode at (width x height) then nearest-neighbor 2x into dest
+	 * sized (width*2 x height*2). Otherwise dest is created at width x height.
+	 */
+	bool loadMaskFromResource(uint8 resourceIndex, uint16 executingObjectId, Graphics::ManagedSurface &dest,
+							  int megapicW, int megapicH, bool upscaleHalfRes = false);
+
+	/** Dialect-v2 AHFFDLTA cutscene state (load/play delta opcodes). */
+	struct DeltaStrip {
+		uint16 y = 0;
+		Common::Array<uint8> rle;
+	};
+	struct DeltaFrame {
+		Common::Array<DeltaStrip> strips;
+	};
+	struct DeltaSfxEvent {
+		uint16 frameIndex = 0;
+		Common::String fileName;
+		bool duckMusic = false;
+	};
+	struct DeltaAnimState {
+		bool loaded = false;
+		bool playing = false;
+		uint16 frameCount = 0;
+		uint16 startFrame = 0;
+		uint16 endFrame = 0;
+		uint16 speedTicks = 1;
+		uint16 tickCounter = 0;
+		uint16 currentFrame = 0;
+		uint16 clipMiX = 0;
+		uint16 clipMiY = 0;
+		uint16 clipMaX = 0;
+		uint16 clipMaY = 0;
+		byte palette[0x300] = {};
+		bool applyPaletteOnStart = false;
+		Common::Array<DeltaFrame> frames;
+		Common::Array<DeltaSfxEvent> sfxEvents;
+		void clear(int screenW, int screenH) {
+			loaded = false;
+			playing = false;
+			frameCount = 0;
+			startFrame = endFrame = currentFrame = 0;
+			speedTicks = 1;
+			tickCounter = 0;
+			applyPaletteOnStart = false;
+			frames.clear();
+			sfxEvents.clear();
+			clipMiX = clipMiY = 0;
+			clipMaX = (uint16)MAX(0, screenW - 1);
+			clipMaY = (uint16)MAX(0, screenH - 1);
+			memset(palette, 0, sizeof(palette));
+		}
+	};
+	DeltaAnimState _deltaAnim;
+	bool loadDeltaAnimResource(uint8 resourceIndex, uint16 executingObjectId, bool forceSkipSpeed1 = false);
+	void clearDeltaAnim();
+	bool startDeltaPlayback(uint16 startFrame, uint16 endFrame, uint16 speedTicks, bool applyPalette);
+	bool tickDeltaPlayback();
+	void applyDeltaFrameToBackground(const DeltaFrame &frame);
+	void playDeltaFrameSfx(uint16 displayFrame);
 	// Font glyph count (79 glyphs in the resource file's font data)
 	uint16 numGlyphs = 79;
 	uint16 maxGlyphHeight;
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 96130b835be..e08ffed3033 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -323,47 +323,13 @@ bool ScriptExecutor::loadIndexedResource(Common::Array<uint8> &outData, uint8 re
 		return false;
 	}
 
-	const int64 oldPos = g_engine->_fileStream->pos();
-	uint32 address = 0;
-
-	if (_executingScriptObjectId == 0) {
-		if (resourceIndex > _engine->_sceneResourceOffsets.size()) {
-			warning("Ignoring resource load for missing scene resource %u", resourceIndex);
-			return false;
-		}
-		address = _engine->_sceneResourceOffsets[resourceIndex - 1];
-	} else {
-		GameObject *object = GameObjects::getObjectByIndex(_executingScriptObjectId);
-		if (object == nullptr || object->_dataOffset == 0) {
-			warning("Ignoring resource load for missing object %u resource %u", _executingScriptObjectId, resourceIndex);
-			return false;
-		}
-		// Binary reads from runtime+0x18D table (loaded during loadObjectData).
-		// 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;
-		}
-		address = object->_resourceOffsets[resourceIndex - 1];
-	}
-
-	if (address == 0) {
-		warning("Ignoring resource load for empty resource %u", resourceIndex);
-		g_engine->_fileStream->seek(oldPos, SEEK_SET);
-		return false;
-	}
-
-	g_engine->_fileStream->seek(address, SEEK_SET);
-	const uint32 size = g_engine->_fileStream->readUint32LE();
-	if (size == 0) {
-		warning("Ignoring resource load for zero-sized resource %u", resourceIndex);
-		g_engine->_fileStream->seek(oldPos, SEEK_SET);
+	if (!_engine->loadSizedResourcePayload(resourceIndex, _executingScriptObjectId, outData)) {
+		warning("Ignoring resource load for missing/empty resource %u (object %u)",
+				resourceIndex, _executingScriptObjectId);
+		outData.clear();
 		return false;
 	}
-	outData.resize(size);
-	g_engine->_fileStream->read(outData.data(), size);
-	g_engine->_fileStream->seek(oldPos, SEEK_SET);
-	return !outData.empty();
+	return true;
 }
 
 bool ScriptExecutor::loadSoundResource(Common::Array<uint8> &outData, uint8 resourceIndex,
@@ -3351,20 +3317,46 @@ OpcodeResult ScriptExecutor::scriptSetMainActor() {
 }
 
 OpcodeResult ScriptExecutor::scriptLoadDeltaAnim() {
-	debugC(kDebugScript, "SCRIPT::loadDeltaAnim() [stub]");
+	const uint8 resourceIndex = readByte();
+	debugC(kDebugScript, "SCRIPT::loadDeltaAnim(index=%u)", resourceIndex);
+	clearScriptError();
 	scriptSkipOpcodeRemainder(0x50);
+	if (!_engine->loadDeltaAnimResource(resourceIndex, _executingScriptObjectId)) {
+		warning("loadDeltaAnim: failed resource %u", resourceIndex);
+		setScriptError(1);
+	}
 	return OpcodeResult::Continue;
 }
 
 OpcodeResult ScriptExecutor::scriptPlayDeltaAnim() {
-	debugC(kDebugScript, "SCRIPT::playDeltaAnim() [stub]");
+	const uint16 startFrame = scriptReadValue16();
+	const uint16 endFrame = scriptReadValue16();
+	const uint16 speedTicks = scriptReadValue16();
+	(void)scriptReadValue16();
+	debugC(kDebugScript, "SCRIPT::playDeltaAnim(start=%u end=%u speed=%u)", startFrame, endFrame, speedTicks);
+	clearScriptError();
 	scriptSkipOpcodeRemainder(0x51);
-	return OpcodeResult::Continue;
+	if (!_engine->startDeltaPlayback(startFrame, endFrame, speedTicks, startFrame <= 1)) {
+		debugC(kDebugScript, "SCRIPT::playDeltaAnim() [no delta loaded - skip wait]");
+		endTimer();
+		endBuffering(_lastOpcodeTriggeredSkip);
+		return OpcodeResult::Continue;
+	}
+	View1 *currentView = (View1 *)_engine->findView("View1");
+	if (currentView != nullptr)
+		currentView->_backgroundSurface.copyFrom(_engine->_sceneBackground);
+	_waitForDeltaAnim = true;
+	endTimer();
+	endBuffering(_lastOpcodeTriggeredSkip);
+	enterBlockingWaitCursor();
+	return OpcodeResult::WaitForCallback;
 }
 
 OpcodeResult ScriptExecutor::scriptRemoveDeltaAnim() {
-	debugC(kDebugScript, "SCRIPT::removeDeltaAnim() [stub]");
+	debugC(kDebugScript, "SCRIPT::removeDeltaAnim()");
 	scriptSkipOpcodeRemainder(0x52);
+	_engine->clearDeltaAnim();
+	_waitForDeltaAnim = false;
 	return OpcodeResult::Continue;
 }
 
@@ -3532,17 +3524,43 @@ OpcodeResult ScriptExecutor::scriptReloadSpecialAnim() {
 		setScriptError(8);
 		return OpcodeResult::Continue;
 	}
-	// Needs AHFFANIM resource loader (dialect-v2 / Windows MCS).
-	warning("reloadSpecialAnim: AHFFANIM loader not implemented (anim=%u res=%u)",
-			sceneAnimIndex, resourceIndex);
-	setScriptError(1);
+	BackgroundAnimationBlob &blob = _engine->_backgroundAnimationsBlobs[sceneAnimIndex - 1];
+	if (!_engine->loadAhffAnimResource(resourceIndex, _executingScriptObjectId, blob._blob)) {
+		warning("reloadSpecialAnim: failed anim=%u res=%u", sceneAnimIndex, resourceIndex);
+		setScriptError(1);
+		return OpcodeResult::Continue;
+	}
+	blob._activeExtraSlot = 0;
 	return OpcodeResult::Continue;
 }
 
 OpcodeResult ScriptExecutor::scriptPlayDiskDelta() {
-	debugC(kDebugScript, "SCRIPT::playDiskDelta() [stub]");
+	const uint8 resourceIndex = readByte();
+	const uint16 startFrame = scriptReadValue16();
+	const uint16 endFrame = scriptReadValue16();
+	const uint16 speedTicks = scriptReadValue16();
+	(void)scriptReadValue16();
+	const uint16 cacheHint = scriptReadValue16();
+	const uint16 applyPal = scriptReadValue16();
+	debugC(kDebugScript, "SCRIPT::playDiskDelta(res=%u start=%u end=%u speed=%u cache=%u pal=%u)",
+		   resourceIndex, startFrame, endFrame, speedTicks, cacheHint, applyPal);
+	clearScriptError();
 	scriptSkipOpcodeRemainder(0x5A);
-	return OpcodeResult::Continue;
+	if (!_engine->loadDeltaAnimResource(resourceIndex, _executingScriptObjectId) ||
+		!_engine->startDeltaPlayback(startFrame, endFrame, speedTicks, applyPal != 0)) {
+		warning("playDiskDelta: failed resource %u", resourceIndex);
+		endTimer();
+		endBuffering(_lastOpcodeTriggeredSkip);
+		return OpcodeResult::Continue;
+	}
+	View1 *currentView = (View1 *)_engine->findView("View1");
+	if (currentView != nullptr)
+		currentView->_backgroundSurface.copyFrom(_engine->_sceneBackground);
+	_waitForDeltaSpeed = true;
+	endTimer();
+	endBuffering(_lastOpcodeTriggeredSkip);
+	enterBlockingWaitCursor();
+	return OpcodeResult::WaitForCallback;
 }
 
 OpcodeResult ScriptExecutor::scriptSetDiskCache() {
@@ -3585,11 +3603,11 @@ OpcodeResult ScriptExecutor::scriptSetWaveVolume() {
 	return OpcodeResult::Continue;
 }
 
-OpcodeResult ScriptExecutor::scriptLoadSpecAnimAnim() {
+OpcodeResult ScriptExecutor::scriptLoadSpecialAnimSlot() {
 	const uint32 sceneAnimIndex = scriptReadValue32() - 0x1000;
 	const uint16 slot = scriptReadValue16();
 	const uint8 resourceIndex = readByte();
-	debugC(kDebugScript, "SCRIPT::loadSpecAnimAnim(anim=%u slot=%u res=%u)",
+	debugC(kDebugScript, "SCRIPT::loadSpecialAnimSlot(anim=%u slot=%u res=%u)",
 		   sceneAnimIndex, slot, resourceIndex);
 	clearScriptError();
 	scriptSkipOpcodeRemainder(0x5E);
@@ -3601,17 +3619,18 @@ OpcodeResult ScriptExecutor::scriptLoadSpecAnimAnim() {
 		setScriptError(8);
 		return OpcodeResult::Continue;
 	}
-	// Needs AHFFANIM resource loader (dialect-v2 / Windows MCS).
-	warning("loadSpecAnimAnim: AHFFANIM loader not implemented (anim=%u slot=%u res=%u)",
-			sceneAnimIndex, slot, resourceIndex);
-	setScriptError(1);
+	BackgroundAnimationBlob &blob = _engine->_backgroundAnimationsBlobs[sceneAnimIndex - 1];
+	if (!_engine->loadAhffAnimResource(resourceIndex, _executingScriptObjectId, blob._extraBlobs[slot - 1])) {
+		warning("loadSpecialAnimSlot: failed anim=%u slot=%u res=%u", sceneAnimIndex, slot, resourceIndex);
+		setScriptError(1);
+	}
 	return OpcodeResult::Continue;
 }
 
-OpcodeResult ScriptExecutor::scriptSetSpecAnimAnim() {
+OpcodeResult ScriptExecutor::scriptSetSpecialAnimSlot() {
 	const uint32 sceneAnimIndex = scriptReadValue32() - 0x1000;
 	const uint16 slot = scriptReadValue16();
-	debugC(kDebugScript, "SCRIPT::setSpecAnimAnim(anim=%u slot=%u)", sceneAnimIndex, slot);
+	debugC(kDebugScript, "SCRIPT::setSpecialAnimSlot(anim=%u slot=%u)", sceneAnimIndex, slot);
 	clearScriptError();
 	scriptSkipOpcodeRemainder(0x5F);
 	if (slot > 8) {
@@ -3631,10 +3650,10 @@ OpcodeResult ScriptExecutor::scriptSetSpecAnimAnim() {
 	return OpcodeResult::Continue;
 }
 
-OpcodeResult ScriptExecutor::scriptClearSpecAnimAnim() {
+OpcodeResult ScriptExecutor::scriptClearSpecialAnimSlot() {
 	const uint32 sceneAnimIndex = scriptReadValue32() - 0x1000;
 	const uint16 slot = scriptReadValue16();
-	debugC(kDebugScript, "SCRIPT::clearSpecAnimAnim(anim=%u slot=%u)", sceneAnimIndex, slot);
+	debugC(kDebugScript, "SCRIPT::clearSpecialAnimSlot(anim=%u slot=%u)", sceneAnimIndex, slot);
 	clearScriptError();
 	scriptSkipOpcodeRemainder(0x60);
 	if (slot > 8) {
@@ -3654,26 +3673,49 @@ OpcodeResult ScriptExecutor::scriptClearSpecAnimAnim() {
 }
 
 OpcodeResult ScriptExecutor::scriptSetDeltaRange() {
-	debugC(kDebugScript, "SCRIPT::setDeltaRange() [stub]");
+	_engine->_deltaAnim.clipMiX = scriptReadValue16();
+	_engine->_deltaAnim.clipMiY = scriptReadValue16();
+	_engine->_deltaAnim.clipMaX = scriptReadValue16();
+	_engine->_deltaAnim.clipMaY = scriptReadValue16();
+	debugC(kDebugScript, "SCRIPT::setDeltaRange(%u,%u)-(%u,%u)",
+		   _engine->_deltaAnim.clipMiX, _engine->_deltaAnim.clipMiY,
+		   _engine->_deltaAnim.clipMaX, _engine->_deltaAnim.clipMaY);
 	scriptSkipOpcodeRemainder(0x61);
 	return OpcodeResult::Continue;
 }
 
 OpcodeResult ScriptExecutor::scriptClearDeltaRange() {
-	debugC(kDebugScript, "SCRIPT::clearDeltaRange() [stub]");
+	debugC(kDebugScript, "SCRIPT::clearDeltaRange()");
+	_engine->_deltaAnim.clipMiX = 0;
+	_engine->_deltaAnim.clipMiY = 0;
+	_engine->_deltaAnim.clipMaX = (uint16)_engine->screenWidthLast();
+	_engine->_deltaAnim.clipMaY = (uint16)_engine->gameHeightLast();
 	scriptSkipOpcodeRemainder(0x62);
 	return OpcodeResult::Continue;
 }
 
 OpcodeResult ScriptExecutor::scriptAddDeltaSfx() {
-	debugC(kDebugScript, "SCRIPT::addDeltaSfx() [stub]");
+	const uint16 frameIndex = scriptReadValue16();
+	const Common::String fileName = scriptParsePascalFileName(scriptReadFixedFileName());
+	const uint16 duckFlag = scriptReadValue16();
+	debugC(kDebugScript, "SCRIPT::addDeltaSfx(frame=%u file=%s duck=%u)", frameIndex, fileName.c_str(), duckFlag);
 	scriptSkipOpcodeRemainder(0x63);
+	if (_engine->_deltaAnim.sfxEvents.size() >= 0x20) {
+		setScriptError(0x33);
+		return OpcodeResult::Continue;
+	}
+	Macs2Engine::DeltaSfxEvent ev;
+	ev.frameIndex = frameIndex;
+	ev.fileName = fileName;
+	ev.duckMusic = duckFlag != 0;
+	_engine->_deltaAnim.sfxEvents.push_back(ev);
 	return OpcodeResult::Continue;
 }
 
 OpcodeResult ScriptExecutor::scriptClearDeltaSfxList() {
-	debugC(kDebugScript, "SCRIPT::clearDeltaSfxList() [nop]");
+	debugC(kDebugScript, "SCRIPT::clearDeltaSfxList()");
 	scriptSkipOpcodeRemainder(0x64);
+	_engine->_deltaAnim.sfxEvents.clear();
 	return OpcodeResult::Continue;
 }
 
@@ -3746,32 +3788,75 @@ OpcodeResult ScriptExecutor::scriptSetCursorType() {
 }
 
 OpcodeResult ScriptExecutor::scriptCheckDeltaSpeed() {
-	debugC(kDebugScript, "SCRIPT::checkDeltaSpeed() [stub]");
+	const uint8 resourceIndex = readByte();
+	const uint16 startFrame = scriptReadValue16();
+	const uint16 endFrame = scriptReadValue16();
+	const uint16 speedTicks = scriptReadValue16();
+	(void)scriptReadValue16();
+	const uint16 cacheHint = scriptReadValue16();
+	debugC(kDebugScript, "SCRIPT::checkDeltaSpeed(res=%u start=%u end=%u speed=%u cache=%u)",
+		   resourceIndex, startFrame, endFrame, speedTicks, cacheHint);
+	clearScriptError();
 	scriptSkipOpcodeRemainder(0x68);
-	return OpcodeResult::Continue;
+	// CheckDeltaSpeed always uses the SkipSpeed==1 AHFFDLTA layout and applies palette.
+	if (!_engine->loadDeltaAnimResource(resourceIndex, _executingScriptObjectId, true) ||
+		!_engine->startDeltaPlayback(startFrame, endFrame, speedTicks, true)) {
+		warning("checkDeltaSpeed: failed resource %u", resourceIndex);
+		endTimer();
+		endBuffering(_lastOpcodeTriggeredSkip);
+		return OpcodeResult::Continue;
+	}
+	View1 *currentView = (View1 *)_engine->findView("View1");
+	if (currentView != nullptr)
+		currentView->_backgroundSurface.copyFrom(_engine->_sceneBackground);
+	_waitForDeltaSpeed = true;
+	endTimer();
+	endBuffering(_lastOpcodeTriggeredSkip);
+	enterBlockingWaitCursor();
+	return OpcodeResult::WaitForCallback;
 }
 
 OpcodeResult ScriptExecutor::scriptLoadDistanceMask() {
-	debugC(kDebugScript, "SCRIPT::loadDistanceMask() [stub]");
+	const uint8 resourceIndex = readByte();
+	debugC(kDebugScript, "SCRIPT::loadDistanceMask(index=%u)", resourceIndex);
+	clearScriptError();
 	scriptSkipOpcodeRemainder(0x69);
+	if (!_engine->loadMaskFromResource(resourceIndex, _executingScriptObjectId, _engine->_depthMap,
+									   _engine->screenWidth(), _engine->gameHeight(), false))
+		warning("loadDistanceMask: failed resource %u", resourceIndex);
 	return OpcodeResult::Continue;
 }
 
 OpcodeResult ScriptExecutor::scriptLoadAreaMask() {
-	debugC(kDebugScript, "SCRIPT::loadAreaMask() [stub]");
+	const uint8 resourceIndex = readByte();
+	debugC(kDebugScript, "SCRIPT::loadAreaMask(index=%u)", resourceIndex);
+	clearScriptError();
 	scriptSkipOpcodeRemainder(0x6A);
+	if (!_engine->loadMaskFromResource(resourceIndex, _executingScriptObjectId, _engine->_hotspotMap,
+									   _engine->screenWidth(), _engine->gameHeight(), false))
+		warning("loadAreaMask: failed resource %u", resourceIndex);
 	return OpcodeResult::Continue;
 }
 
 OpcodeResult ScriptExecutor::scriptLoadWalkMask() {
-	debugC(kDebugScript, "SCRIPT::loadWalkMask() [stub]");
+	const uint8 resourceIndex = readByte();
+	debugC(kDebugScript, "SCRIPT::loadWalkMask(index=%u)", resourceIndex);
+	clearScriptError();
 	scriptSkipOpcodeRemainder(0x6B);
+	if (!_engine->loadMaskFromResource(resourceIndex, _executingScriptObjectId, _engine->_pathfindingMap,
+									   _engine->screenWidth(), _engine->gameHeight(), false))
+		warning("loadWalkMask: failed resource %u", resourceIndex);
 	return OpcodeResult::Continue;
 }
 
 OpcodeResult ScriptExecutor::scriptLoadShadowMask() {
-	debugC(kDebugScript, "SCRIPT::loadShadowMask() [stub]");
+	const uint8 resourceIndex = readByte();
+	debugC(kDebugScript, "SCRIPT::loadShadowMask(index=%u)", resourceIndex);
+	clearScriptError();
 	scriptSkipOpcodeRemainder(0x6C);
+	if (!_engine->loadMaskFromResource(resourceIndex, _executingScriptObjectId, _engine->_shadowMap,
+									   _engine->screenWidth(), _engine->gameHeight(), false))
+		warning("loadShadowMask: failed resource %u", resourceIndex);
 	return OpcodeResult::Continue;
 }
 
@@ -3966,9 +4051,9 @@ const ScriptExecutor::OpcodeEntry ScriptExecutor::kV2OpcodeTable[] = {
 	{"setDiskCache", &ScriptExecutor::scriptSetDiskCache},
 	{"setMidiVolume", &ScriptExecutor::scriptSetMidiVolume},
 	{"setWaveVolume", &ScriptExecutor::scriptSetWaveVolume},
-	{"loadSpecAnimAnim", &ScriptExecutor::scriptLoadSpecAnimAnim},
-	{"setSpecAnimAnim", &ScriptExecutor::scriptSetSpecAnimAnim},
-	{"clearSpecAnimAnim", &ScriptExecutor::scriptClearSpecAnimAnim},
+	{"loadSpecialAnimSlot", &ScriptExecutor::scriptLoadSpecialAnimSlot},
+	{"setSpecialAnimSlot", &ScriptExecutor::scriptSetSpecialAnimSlot},
+	{"clearSpecialAnimSlot", &ScriptExecutor::scriptClearSpecialAnimSlot},
 	{"setDeltaRange", &ScriptExecutor::scriptSetDeltaRange},
 	{"clearDeltaRange", &ScriptExecutor::scriptClearDeltaRange},
 	{"addDeltaSfx", &ScriptExecutor::scriptAddDeltaSfx},
@@ -4074,12 +4159,14 @@ void ScriptExecutor::run(bool firstRun) {
 	// Returns immediately if ANY wait condition is active.
 	if (_frameWaitTicksRemaining != 0 || _walkTargetObjectIndex != 0 ||
 		_waitForPcmSound || _waitForMusicControl || _waitForAdlibReady ||
-		_waitForObjectAnimStep || _waitForSpecialAnimStep) {
-		debugC(kDebugScript, "run() blocked by entry guard: frameWait=%d walkTarget=%d sound=%d music=%d adlib=%d objAnim=%d specAnim=%d",
+		_waitForObjectAnimStep || _waitForSpecialAnimStep ||
+		_waitForDeltaAnim || _waitForDeltaSpeed) {
+		debugC(kDebugScript, "run() blocked by entry guard: frameWait=%d walkTarget=%d sound=%d music=%d adlib=%d objAnim=%d specAnim=%d delta=%d/%d",
 			   _frameWaitTicksRemaining, _walkTargetObjectIndex,
 			   _waitForPcmSound ? 1 : 0, _waitForMusicControl ? 1 : 0,
 			   _waitForAdlibReady ? 1 : 0,
-			   _waitForObjectAnimStep ? 1 : 0, _waitForSpecialAnimStep ? 1 : 0);
+			   _waitForObjectAnimStep ? 1 : 0, _waitForSpecialAnimStep ? 1 : 0,
+			   _waitForDeltaAnim ? 1 : 0, _waitForDeltaSpeed ? 1 : 0);
 		return;
 	}
 
diff --git a/engines/macs2/scriptexecutor.h b/engines/macs2/scriptexecutor.h
index 8b12642eb49..410e9292f0f 100644
--- a/engines/macs2/scriptexecutor.h
+++ b/engines/macs2/scriptexecutor.h
@@ -227,8 +227,6 @@ public:
 	Common::Path resolveAudioFilePath(const Common::String &fileName, bool preferSpeech) const;
 	/** Resolve MUSICGS then MUSICOPL; empty if neither exists. */
 	Common::Path resolveMidiFilePath(const Common::String &fileName) const;
-	/** Strip a trailing audio extension (.wav/.ogg/...) if present. */
-	static Common::String stripAudioExtension(const Common::String &fileName);
 	/**
 	 * Optional generated dialogue audio (kEnhAudioChanges):
 	 * scene -> SPEECH/sSS_OOOO.*, object -> SPEECH/oOOO_OOOO.*.
@@ -252,9 +250,9 @@ public:
 	OpcodeResult scriptSetDiskCache();
 	OpcodeResult scriptSetMidiVolume();
 	OpcodeResult scriptSetWaveVolume();
-	OpcodeResult scriptLoadSpecAnimAnim();
-	OpcodeResult scriptSetSpecAnimAnim();
-	OpcodeResult scriptClearSpecAnimAnim();
+	OpcodeResult scriptLoadSpecialAnimSlot();
+	OpcodeResult scriptSetSpecialAnimSlot();
+	OpcodeResult scriptClearSpecialAnimSlot();
 	OpcodeResult scriptSetDeltaRange();
 	OpcodeResult scriptClearDeltaRange();
 	OpcodeResult scriptAddDeltaSfx();
@@ -391,6 +389,9 @@ public:
 	ScriptExecutor(Macs2::Macs2Engine *engine);
 	~ScriptExecutor();
 
+	/** Strip a trailing audio extension (.wav/.ogg/...) if present. */
+	static Common::String stripAudioExtension(const Common::String &fileName);
+
 	void setIdle() { _state = ExecutorState::Idle; }
 
 	Common::Array<uint16> _dialogueChoiceScriptIndices;
@@ -480,6 +481,8 @@ public:
 	bool _waitForSpecialAnimStep = false;
 	uint16 _waitSpecialAnimIndex = 0;
 	uint16 _waitSpecialAnimTargetStep = 0;
+	bool _waitForDeltaAnim = false;
+	bool _waitForDeltaSpeed = false;
 	bool _debugPaused = false;
 	bool _pickupInProgress = false;
 	uint16 _pickupActorObjectID = 0;
@@ -528,7 +531,8 @@ public:
 		return _state == ExecutorState::WaitingForCallback ||
 			   _frameWaitTicksRemaining != 0 || _walkTargetObjectIndex != 0 ||
 			   _waitForPcmSound || _waitForMusicControl || _waitForAdlibReady ||
-			   _waitForObjectAnimStep || _waitForSpecialAnimStep;
+			   _waitForObjectAnimStep || _waitForSpecialAnimStep ||
+			   _waitForDeltaAnim || _waitForDeltaSpeed;
 	}
 
 	bool isExecuting() const {
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 5c20b84d6bc..638936ce652 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -1852,6 +1852,8 @@ bool View1::handleInput(const MouseDownMessage &msg) {
 				!g_engine->_scriptExecutor->_waitForAdlibReady &&
 				!g_engine->_scriptExecutor->_waitForObjectAnimStep &&
 				!g_engine->_scriptExecutor->_waitForSpecialAnimStep &&
+				!g_engine->_scriptExecutor->_waitForDeltaAnim &&
+				!g_engine->_scriptExecutor->_waitForDeltaSpeed &&
 				g_engine->_scriptExecutor->canOpenSaveMenu()) {
 				if (ConfMan.getBool("original_menus")) {
 					// Binary handleInput (1008:f2af): saves cursor mode before opening panel
@@ -2542,6 +2544,28 @@ bool View1::tick() {
 					executor->_waitForSpecialAnimStep = false;
 					g_engine->runScriptExecutor();
 				}
+			} else if (executor->_waitForDeltaAnim) {
+				drawSceneUpdate();
+				if (!g_engine->tickDeltaPlayback()) {
+					debugC(kDebugScript, "waitForDeltaAnim complete");
+					executor->_waitForDeltaAnim = false;
+					_backgroundSurface.copyFrom(g_engine->_sceneBackground);
+					g_engine->runScriptExecutor();
+				} else {
+					_backgroundSurface.copyFrom(g_engine->_sceneBackground);
+					redraw();
+				}
+			} else if (executor->_waitForDeltaSpeed) {
+				drawSceneUpdate();
+				if (!g_engine->_deltaAnim.playing || !g_engine->tickDeltaPlayback()) {
+					debugC(kDebugScript, "waitForDeltaSpeed complete");
+					executor->_waitForDeltaSpeed = false;
+					_backgroundSurface.copyFrom(g_engine->_sceneBackground);
+					g_engine->runScriptExecutor();
+				} else {
+					_backgroundSurface.copyFrom(g_engine->_sceneBackground);
+					redraw();
+				}
 			}
 		} else {
 			drawSceneUpdate();


Commit: 624a678aad53cdd576392d0f7d51c2e4768a101a
    https://github.com/scummvm/scummvm/commit/624a678aad53cdd576392d0f7d51c2e4768a101a
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-21T10:43:52+02:00

Commit Message:
MACS2: updated comment

Changed paths:
    engines/macs2/scriptexecutor.h


diff --git a/engines/macs2/scriptexecutor.h b/engines/macs2/scriptexecutor.h
index 410e9292f0f..5b4fc6ff911 100644
--- a/engines/macs2/scriptexecutor.h
+++ b/engines/macs2/scriptexecutor.h
@@ -123,8 +123,7 @@ public:
 	static const uint kV1OpcodeTableSize;
 	/**
 	 * Script dialect v2 opcode table (extends v1 through 0x6D).
-	 * Remaps a few audio slots and adds 0x4F..0x6D; new handlers are stubs
-	 * that consume the length-prefixed payload via scriptSkipOpcodeRemainder().
+	 * Remaps a few audio slots and adds 0x4F..0x6D
 	 */
 	static const OpcodeEntry kV2OpcodeTable[];
 	static const uint kV2OpcodeTableSize;


Commit: 4775cf2528014ef5cb15b901dbee82b4154a957a
    https://github.com/scummvm/scummvm/commit/4775cf2528014ef5cb15b901dbee82b4154a957a
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-21T10:43:52+02:00

Commit Message:
MACS2: replaced constants for screen dimensions with engine getters

Changed paths:
    engines/macs2/actionbar.cpp


diff --git a/engines/macs2/actionbar.cpp b/engines/macs2/actionbar.cpp
index 36dff8db5a5..9874e9b76e4 100644
--- a/engines/macs2/actionbar.cpp
+++ b/engines/macs2/actionbar.cpp
@@ -139,7 +139,7 @@ bool ActionBar::useNativeSkin() const {
 int ActionBar::gameAreaBottomY() const {
 	if (useNativeSkin())
 		return (int)g_engine->_panelTopY;
-	return kGameHeight;
+	return g_engine->gameHeight();
 }
 
 void ActionBar::draw(Graphics::ManagedSurface &s) {
@@ -666,14 +666,14 @@ void ActionBar::drawNative(Graphics::ManagedSurface &s) {
 		if (fontCount != 0) {
 			Common::String sentence = buildNativeSentenceLine();
 			if (!sentence.empty()) {
-				const uint16 maxW = (uint16)(kScreenWidth - 16);
+				const uint16 maxW = (uint16)(g_engine->screenWidth() - 16);
 				while (sentence.size() > 1) {
 					if ((uint16)_view->measureStringWithFont(sentence, font, fontCount) <= maxW)
 						break;
 					sentence.deleteLastChar();
 				}
 				const int textW = _view->measureStringWithFont(sentence, font, fontCount);
-				const int textX = MAX(0, (kScreenWidth - textW) / 2);
+				const int textX = MAX(0, (g_engine->screenWidth() - textW) / 2);
 				const int glyphH = g_engine->maxGlyphHeight ? (int)g_engine->maxGlyphHeight : 12;
 				const int textY = MAX(0, (int)panelTop - glyphH - 2);
 				_view->renderStringWithFontTo((uint16)textX, (uint16)textY, sentence, font, fontCount, s);


Commit: 7308bcc606b0880fdb6c36a0fbb833d7f179a279
    https://github.com/scummvm/scummvm/commit/7308bcc606b0880fdb6c36a0fbb833d7f179a279
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-21T10:43:52+02:00

Commit Message:
MACS2: replace constants with engine methods for width and height

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


diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index d274547bab2..9f845300802 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -130,6 +130,8 @@ Macs2Engine::McsFileVersion Macs2Engine::detectMcsFileVersion(Common::SeekableRe
 
 	if (memcmp(magic, kMcsMagicV1, kMcsMagicSize) == 0)
 		return McsFileVersion::V1;
+	if (memcmp(magic, kMcsMagicV2, kMcsMagicSize) == 0)
+		return McsFileVersion::V2;
 	return McsFileVersion::Unknown;
 }
 
@@ -294,11 +296,13 @@ void Macs2Engine::loadResourceFileV1() {
 	// is loaded before the game loop processes any input.
 	// The original allocates the 0x75E0-byte scene data buffer (which includes space for
 	// all RLE-decoded maps) before calling changeScene. Create the surfaces here.
-	_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());
+	const int sw = screenWidth();
+	const int gh = gameHeight();
+	_sceneBackground.create(sw, gh, Graphics::PixelFormat::createFormatCLUT8());
+	_depthMap.create(sw, gh, Graphics::PixelFormat::createFormatCLUT8());
+	_pathfindingMap.create(sw, gh, Graphics::PixelFormat::createFormatCLUT8());
+	_shadowMap.create(sw, gh, Graphics::PixelFormat::createFormatCLUT8());
+	_hotspotMap.create(sw, gh, Graphics::PixelFormat::createFormatCLUT8());
 	changeScene(Scenes::instance()._currentSceneIndex);
 }
 
@@ -1320,13 +1324,13 @@ bool Macs2Engine::findGlyph(char c, GlyphData &out) const {
 
 // getWalkabilityAt (1008:0e8c)
 // Params: (param_1=y, param_2=x)
-// Bounds: x<0 || x>=kScreenWidth || y<0 || y>=kGameHeight -> return 0
+// Bounds: x<0 || x>=screenWidth || y<0 || y>=gameHeight -> return 0
 // Lookup: scene[y*4 + 0x2017] -> row pointer, then byte at [rowPtr + x]
 // Values 0xC8..0xEF: override range - checks scene[value*5 + 0x4EA5]:
 //   If override disabled (flag==0): returns 0xFF
 //   If override enabled (flag!=0): returns scene[value*5 + 0x4EA6]
 uint16 Macs2Engine::getWalkabilityAt(int16 y, int16 x) {
-	if (x < 0 || x >= kScreenWidth || y < 0 || y >= kGameHeight || _pathfindingMap.w == 0) {
+	if (x < 0 || x >= screenWidth() || y < 0 || y >= gameHeight() || _pathfindingMap.w == 0) {
 		return 0;
 	}
 	uint16 value = _pathfindingMap.getPixel(x, y);
@@ -1346,6 +1350,8 @@ uint16 Macs2Engine::getWalkabilityAt(int16 y, int16 x) {
 void Macs2Engine::snapToWalkablePosition(int16 *pTargetY, int16 *pTargetX, int16 charY, int16 charX) {
 	int16 savedX = *pTargetX;
 	int16 savedY = *pTargetY;
+	const int16 maxY = (int16)gameHeightLast();
+	const int16 maxX = (int16)screenWidthLast();
 
 	// Phase 1: Scan downward with depth constraint
 	// Condition: walkability >= 200 OR (targetY - walkability) < savedY
@@ -1354,7 +1360,7 @@ void Macs2Engine::snapToWalkablePosition(int16 *pTargetY, int16 *pTargetX, int16
 		if (isWalkabilityWalkable(w) && (*pTargetY - (int16)w >= savedY)) {
 			break;
 		}
-		if (*pTargetY >= kGameHeightLast) {
+		if (*pTargetY >= maxY) {
 			break;
 		}
 		*pTargetY = *pTargetY + 1;
@@ -1362,19 +1368,19 @@ void Macs2Engine::snapToWalkablePosition(int16 *pTargetY, int16 *pTargetX, int16
 
 	// Phase 2: Continue scanning to bottom for best depth match
 	int16 scanY = *pTargetY;
-	while (scanY <= kGameHeightLast) {
+	while (scanY <= maxY) {
 		uint16 w = getWalkabilityAt(scanY, *pTargetX);
 		if (scanY - (int16)w == savedY) {
 			*pTargetY = scanY;
 		}
-		if (scanY == kGameHeightLast) {
+		if (scanY == maxY) {
 			break;
 		}
 		scanY++;
 	}
 
 	// Phase 3: If at screen bottom and still non-walkable, scan upward
-	if (*pTargetY == kGameHeightLast) {
+	if (*pTargetY == maxY) {
 		uint16 w = getWalkabilityAt(*pTargetY, *pTargetX);
 		if (isWalkabilityBlocking(w)) {
 			while (isWalkabilityBlocking(w) && *pTargetY > 0) {
@@ -1402,7 +1408,7 @@ void Macs2Engine::snapToWalkablePosition(int16 *pTargetY, int16 *pTargetX, int16
 				uint16 w2 = getWalkabilityAt(*pTargetY, *pTargetX);
 				if (isWalkabilityWalkable(w2))
 					break;
-				if (*pTargetX >= kScreenWidthLast)
+				if (*pTargetX >= maxX)
 					break;
 				*pTargetX = *pTargetX + 1;
 			}
@@ -1947,7 +1953,7 @@ void Macs2Engine::getHotspotPositions(Common::Array<Graphics::HotspotInfo> &hots
 			continue;
 
 		const Common::Point &center = entry.center;
-		if (center.x < 0 || center.x >= kScreenWidth || center.y < 0 || center.y >= kGameHeight)
+		if (center.x < 0 || center.x >= screenWidth() || center.y < 0 || center.y >= gameHeight())
 			continue;
 
 		const uint16 sceneIndex = (uint16)Scenes::instance()._currentSceneIndex;
@@ -1963,7 +1969,7 @@ void Macs2Engine::getHotspotPositions(Common::Array<Graphics::HotspotInfo> &hots
 			continue;
 
 		const Common::Point &screenPos = entry.position;
-		if (screenPos.x < 0 || screenPos.x >= kScreenWidth || screenPos.y < 0 || screenPos.y >= kGameHeight)
+		if (screenPos.x < 0 || screenPos.x >= screenWidth() || screenPos.y < 0 || screenPos.y >= gameHeight())
 			continue;
 
 		Character *character = view ? view->getCharacterByIndex(entry.index) : nullptr;
@@ -2624,8 +2630,9 @@ Common::Error Macs2Engine::run() {
 		loadTranslation();
 	}
 
-	// Initialize graphics mode (taller framebuffer when SCUMM verb UI is enabled)
-	initGraphics(kScreenWidth, enhancementEnabled(kEnhUIUX) ? kScreenHeight : kGameHeight);
+	// Initialize graphics mode (taller framebuffer when action bar verb UI is enabled)
+	int gfxH = screenHeight();
+	initGraphics(screenWidth(), gfxH);
 
 	CursorMan.showMouse(false);
 
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index a0540c6b0c5..ff9a457e337 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -283,12 +283,13 @@ protected:
 public:
 	Graphics::ManagedSurface readRLEImage(int64 offs, Common::MemoryReadStream *stream);
 
-	/** Open RESOURCE.MCS, check magic, dispatch to loadResourceFileV1. */
+	/** Open primary MCS archive, check magic, load v1 or v2 layout. */
 	void readResourceFile();
 	/** MCS dialect from the 12-byte file magic. */
 	enum class McsFileVersion {
 		Unknown = 0,
-		V1 // AHFFMACS0100
+		V1, // AHFFMACS0100
+		V2  // AHFFMACS0200
 	};
 	McsFileVersion detectMcsFileVersion(Common::SeekableReadStream &stream) const;
 	/** Load AHFFMACS0100 layout (loadResourceFile @ 1008:2e8d). */
@@ -726,11 +727,29 @@ public:
 
 	/** MCS directory base (v1: file+0x10 after magic + actor/scene words). */
 	uint32 getMcsDirectoryOffset() const { return kMcsV1DirectoryOffset; }
+
+	/** AHFFMACS0200 dialect (directory at 0x212) vs AHFFMACS0100 (0x10). */
+	bool isV2() const { return _mcsFileVersion == McsFileVersion::V2; }
 	McsFileVersion getMcsFileVersion() const { return _mcsFileVersion; }
-	int screenWidth() const { return kScreenWidth; }
+	int screenWidth() const { return isV2() ? kWinScreenWidth : kScreenWidth; }
 	int screenWidthLast() const { return screenWidth() - 1; }
-	int gameHeight() const { return kGameHeight; }
+	int gameHeight() const { return isV2() ? kWinGameHeight : kGameHeight; }
 	int gameHeightLast() const { return gameHeight() - 1; }
+	/**
+	 * Full framebuffer height (playfield + bottom HUD / Scumm strip).
+	 * Matches initGraphics height used at startup.
+	 */
+	int screenHeight() {
+		if (isV2()) {
+			if (_panelTopY == 0 || _panelHeight == 0)
+				return kWinGameHeight;
+			return (int)_panelTopY + (int)_panelHeight;
+		}
+		if (enhancementEnabled(kEnhUIUX))
+			return gameHeight() + kUIHeight;
+		return gameHeight();
+	}
+	int screenHeightLast() { return screenHeight() - 1; }
 
 	/**
 	 * Bottom HUD / action-bar visibility (dialect-neutral).
@@ -745,32 +764,32 @@ public:
 		return _panelTopY != 0 && _panelHeight != 0;
 	}
 
-	// --- Layout / dialect facades (DOS defaults; other platforms override later) ---
+	// --- Layout / dialect facades ---
 
 	/** Game-loop timer quantum in milliseconds. */
-	uint32 timerTickMs() const { return 46; }
+	uint32 timerTickMs() const { return isV2() ? 55 : 46; }
 	/** Normal-speed: game frames per that many timer ticks. */
-	uint16 ticksPerGameFrame() const { return 2; }
+	uint16 ticksPerGameFrame() const { return isV2() ? 1 : 2; }
 
 	/** ReadyObject anim slots (1-based inclusive max). */
-	uint16 maxAnimSlots() const { return 0x15; }
+	uint16 maxAnimSlots() const { return isV2() ? 0x26 : 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; }
+	uint16 maxOrientations() const { return isV2() ? 0x25 : 0x14; }
+	/** Overload / special-anim slot index. */
+	uint16 overloadAnimSlot() const { return maxAnimSlots(); }
 	/** 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). */
+	/** Anim slot used for the current orientation (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; }
+	bool scriptValuesHaveHighWord() const { return isV2(); }
 	/** 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; }
+	bool scriptVarIndexHasPaddingWord() const { return isV2(); }
+	/** Script coordinates -> screen/runtime coordinates (x2 on v2). */
+	int16 scaleScriptCoord(int16 coord) const { return isV2() ? (int16)(coord * 2) : coord; }
 
 	/** Dialogue / text-box chrome (DOS l0037_B368 / B462). */
 	int dialogPadW() const { return isAmiga() ? 0x08 : 0x12; }
@@ -791,10 +810,12 @@ public:
 		return (int)maxGlyphHeight + dialogLineGap();
 	}
 
-	/** Depth-map compare Y for sprite occlusion (full Y on DOS). */
-	uint8 depthThresholdForY(int16 charY) const { return (uint8)charY; }
+	/** Depth-map compare Y for sprite occlusion (halved on v2 full-res depth). */
+	uint8 depthThresholdForY(int16 charY) const {
+		return isV2() ? (uint8)((uint16)charY >> 1) : (uint8)charY;
+	}
 
-	/** Resource bootstrap (DOS MCS or Amiga DataA/Mdir). */
+	/** Resource bootstrap (MCS or Amiga DataA/Mdir). */
 	void loadBootstrapResources();
 	/**
 	 * Load scene background, maps, pathfinding, and related scene tables.
diff --git a/engines/macs2/macs2_constants.h b/engines/macs2/macs2_constants.h
index 817f7bcb574..bc6539ac1f0 100644
--- a/engines/macs2/macs2_constants.h
+++ b/engines/macs2/macs2_constants.h
@@ -24,24 +24,31 @@
 
 namespace Macs2 {
 
-// Original game viewport dimensions (all scene maps and buffers use these).
+// V1 (AHFFMACS0100) viewport - default engine dimensions.
 static constexpr int kScreenWidth     = 320;
 static constexpr int kScreenWidthLast = kScreenWidth - 1;
 static constexpr int kGameHeight      = 200;
 static constexpr int kGameHeightLast  = kGameHeight - 1;
 
+// V2 dimensions.
+static constexpr int kWinScreenWidth     = 640;
+static constexpr int kWinScreenWidthLast = kWinScreenWidth - 1;
+static constexpr int kWinGameHeight      = 400;
+static constexpr int kWinGameHeightLast  = kWinGameHeight - 1;
+
 // SCUMM-style verb/inventory strip (kEnhUIUX enhancement only).
-static constexpr int kUIHeight       = 64;
-static constexpr int kScreenHeight   = kGameHeight + kUIHeight;
+static constexpr int kUIHeight         = 64;
+static constexpr int kScreenHeight     = kGameHeight + kUIHeight;
 static constexpr int kScreenHeightLast = kScreenHeight - 1;
+static constexpr int kWinScreenHeight  = kWinGameHeight + kUIHeight;
 
 /**
- * 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.)
+ * MCS file layout.
+ * Magic is 12 ASCII bytes "AHFFMACS0100" (v1) or "AHFFMACS0200" (v2).
  */
 static constexpr uint kMcsMagicSize = 12;
 static constexpr const char *kMcsMagicV1 = "AHFFMACS0100";
+static constexpr const char *kMcsMagicV2 = "AHFFMACS0200";
 
 /** MCS v1 absolute file offsets (after validating AHFFMACS0100). */
 static constexpr uint32 kMcsV1ActorIndexOffset = 0x0C;
@@ -59,6 +66,9 @@ static constexpr uint kMcsV1CursorImageCount = 0x21;
 static constexpr uint kMcsV1MapSceneOffsetCount = 256;
 static constexpr uint32 kMcsV1MapSceneOffsetsSize = kMcsV1MapSceneOffsetCount * 4; // 0x400
 
+static constexpr uint32 kMcsV2ActorIndexOffset = 0x20E;
+static constexpr uint32 kMcsV2DirectoryOffset = 0x212;
+
 } // namespace Macs2
 
 #endif // MACS2_CONSTANTS_H
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index e08ffed3033..d132bc37310 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -304,7 +304,8 @@ OpcodeResult ScriptExecutor::scriptChangeAnimation() {
 uint16 ScriptExecutor::getAreaAtPoint(uint16 x, uint16 y) {
 	// getAreaAtPoint (1008:101d). Reads the pathfinding map pixel and applies
 	// the area override table at sceneData + value*5 + 0x4EA8.
-	if (x >= kScreenWidth || y >= kGameHeight || _engine->_pathfindingMap.w == 0) {
+	if (x >= (uint16)_engine->screenWidth() || y >= (uint16)_engine->gameHeight() ||
+		_engine->_pathfindingMap.w == 0) {
 		return 0;
 	}
 	uint16 result = _engine->_pathfindingMap.getPixel(x, y);
@@ -4327,11 +4328,15 @@ uint32 ScriptExecutor::getSpecialValue(uint16 value) {
 	case 0x24: {
 		const GameObject *actor = GameObjects::instance().getObjectByIndex(Scenes::instance()._currentActorIndex);
 		out1 = actor ? actor->_position.x : 0;
+		if (_engine->isV2())
+			out1 /= 2;
 		break;
 	}
 	case 0x25: {
 		const GameObject *actor = GameObjects::instance().getObjectByIndex(Scenes::instance()._currentActorIndex);
 		out1 = actor ? actor->_position.y : 0;
+		if (_engine->isV2())
+			out1 /= 2;
 		break;
 	}
 	case 0x26:
@@ -4382,9 +4387,14 @@ uint32 ScriptExecutor::getSpecialValue(uint16 value) {
 	case 0x31:
 		out1 = (_soundEnabled && _soundSystemActive) ? 1 : 0;
 		break;
+	case 0x32:
+		out1 = Scenes::instance()._currentActorIndex + 0x400;
+		break;
 	default:
 		if (value >= 0x0E && value <= 0x22) {
 			out1 = value - 0x0D;
+		} else if (value >= 0x33 && value <= 0x43) {
+			out1 = value - 0x1D;
 		} else {
 			warning("getSpecialValue: unknown special value 0x%02x", value);
 		}
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 638936ce652..e994b9c589e 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -127,6 +127,11 @@ View1::View1() : UIElement("View1") {
 	_paletteDirty = false;
 	CursorMan.showMouse(true);
 
+	const int sw = g_engine->screenWidth();
+	const int sh = g_engine->screenHeight();
+	_bounds = Common::Rect(0, 0, sw, sh);
+	_innerBounds = _bounds;
+
 	// TODO: Check if this works like this
 	Character *protagonist = new Character();
 	// TODO: Need to properly handle the offset
@@ -139,8 +144,6 @@ View1::View1() : UIElement("View1") {
 
 	if (hasPersistentActionBar()) {
 		_actionBar = new ActionBar(this);
-		_bounds = Common::Rect(0, 0, kScreenWidth, kScreenHeight);
-		_innerBounds = _bounds;
 		setInventorySource(_inventorySource);
 	}
 }
@@ -153,10 +156,12 @@ void View1::ensureActionBar() {
 		if (_inventorySource)
 			setInventorySource(_inventorySource);
 	}
-	if (_bounds.height() != kScreenHeight) {
-		_bounds = Common::Rect(0, 0, kScreenWidth, kScreenHeight);
+	const int sw = g_engine->screenWidth();
+	const int sh = g_engine->screenHeight();
+	if (_innerBounds.width() != sw || _innerBounds.height() != sh) {
+		_bounds = Common::Rect(0, 0, sw, sh);
 		_innerBounds = _bounds;
-		::initGraphics(kScreenWidth, kScreenHeight);
+		::initGraphics(sw, sh);
 	}
 }
 
@@ -169,7 +174,7 @@ int View1::actionBarTopY() const {
 		return _actionBar->gameAreaBottomY();
 	if (g_engine->hasNativeHudAssets() && g_engine->isBottomHudVisible() && g_engine->_menuMode != 0)
 		return (int)g_engine->_panelTopY;
-	return kGameHeight;
+	return g_engine->gameHeight();
 }
 
 bool View1::shouldShowActionBar() const {
@@ -526,7 +531,7 @@ void View1::drawDarkRectangle(uint16 x, uint16 y, uint16 width, uint16 height) {
 			const uint16 currentY = y + yOffset;
 			const uint8 currentValue = (uint8)s.getPixel(currentX, currentY);
 			const uint8 newValue = g_engine->_panelRemapTable[currentValue];
-			if (currentX < kScreenWidth && currentY < kGameHeight)
+			if (currentX < (uint16)g_engine->screenWidth() && currentY < (uint16)g_engine->gameHeight())
 				s.setPixel(currentX, currentY, newValue);
 		}
 	}
@@ -798,9 +803,11 @@ void View1::drawPathfindingPoints(Graphics::ManagedSurface &s) {
 		return;
 	}
 	const Common::Array<uint8> &overlay = c->_pathfindingOverlay;
-	for (int y = 0; y < kGameHeight; y++) {
-		for (int x = 0; x < kScreenWidth; x++) {
-			const uint8 currentValue = overlay[y * kScreenWidth + x];
+	const int sw = g_engine->screenWidth();
+	const int gh = g_engine->gameHeight();
+	for (int y = 0; y < gh; y++) {
+		for (int x = 0; x < sw; x++) {
+			const uint8 currentValue = overlay[y * sw + x];
 			if (currentValue != 0) {
 				s.setPixel(x, y, currentValue);
 			}
@@ -872,11 +879,13 @@ void View1::openMainMenu(Common::Point clickedPosition) {
 		upperLeft.y = 0;
 	}
 	// Binary openActionBarAtPosition (1008:3fba): clamp to screen bounds.
-	if ((int)(upperLeft.x + panelSize.x) >= kScreenWidth) {
-		upperLeft.x = kScreenWidth - panelSize.x - 1;
+	const int sw = g_engine->screenWidth();
+	const int gh = g_engine->gameHeight();
+	if ((int)(upperLeft.x + panelSize.x) >= sw) {
+		upperLeft.x = sw - panelSize.x - 1;
 	}
-	if ((int)(upperLeft.y + panelSize.y) >= kGameHeight) {
-		upperLeft.y = kGameHeight - panelSize.y - 1;
+	if ((int)(upperLeft.y + panelSize.y) >= gh) {
+		upperLeft.y = gh - panelSize.y - 1;
 	}
 
 	_mainMenuRect = Common::Rect(upperLeft, upperLeft + panelSize);
@@ -1622,7 +1631,7 @@ bool View1::handleActionBarClick(const MouseDownMessage &msg) {
 }
 
 bool View1::handleHelpClick(const MouseDownMessage &msg) {
-	Common::Rect screenRect(kScreenWidth, kGameHeight);
+	Common::Rect screenRect(g_engine->screenWidth(), g_engine->gameHeight());
 	if (screenRect.contains(msg._pos)) {
 		uint8 depth = g_engine->_depthMap.getPixel(msg._pos.x, msg._pos.y);
 		if (depth > 0 && depth < 0xFA) {
@@ -2279,12 +2288,15 @@ void View1::draw() {
 	if (hasPersistentActionBar()) {
 		ensureActionBar();
 		if (_actionBar && !_isShowingTextBox && !_isShowingDialoguePanel) {
-			Graphics::ManagedSurface fullScreen(*g_events->getScreen(), Common::Rect(0, 0, kScreenWidth, kScreenHeight));
+			const int sw = g_engine->screenWidth();
+			const int sh = g_engine->screenHeight();
+			Graphics::ManagedSurface fullScreen(*g_events->getScreen(), Common::Rect(0, 0, sw, sh));
 			if (shouldShowActionBar()) {
 				_actionBar->draw(fullScreen);
 			} else {
 				const int top = actionBarTopY();
-				fullScreen.fillRect(Common::Rect(0, top, kScreenWidth, kScreenHeight), 0);
+				if (top >= 0 && top < sh)
+					fullScreen.fillRect(Common::Rect(0, top, sw, sh), 0);
 			}
 		}
 	}
@@ -2802,7 +2814,7 @@ void View1::drawAllCharacters(Graphics::ManagedSurface *surface, bool fullUpdate
 			if (current != nullptr && DebugMan.isDebugChannelEnabled(kDebugGraphics)) {
 				Common::String number = Common::String::format("%u", obj->_orientation);
 				renderString(current->getPosition(), number.c_str());
-				Common::Rect screenRect(0, 0, kScreenWidth, kGameHeight);
+				Common::Rect screenRect(0, 0, g_engine->screenWidth(), g_engine->gameHeight());
 				if (screenRect.contains(current->getPosition()))
 					surface->setPixel(current->getPosition().x, current->getPosition().y, 0xFF);
 			}
@@ -3082,7 +3094,11 @@ static byte applyShadingTable(byte color, int shadingTableOffset) {
 	if (shadingTableOffset == 0)
 		return color;
 	// drawSpriteTransparent @ 1010:0fba: (color - 0xC0) * 0x20 + shadingTableOffset + scene+0x53D3
+	if (color < 0xC0)
+		return color;
 	const uint idx = (uint)(color - 0xC0) * 0x20 + (uint)shadingTableOffset;
+	if (idx >= g_engine->_shadingTable.size())
+		return color;
 	return g_engine->_shadingTable[idx];
 }
 
@@ -3450,7 +3466,7 @@ uint16 View1::getHitObjectID(const Common::Point &pos) const {
 			continue;
 
 		const uint8 characterDepth = currentCharacter->getPosition().y;
-		if (pos.x >= 0 && pos.x < kScreenWidth && pos.y >= 0 && pos.y < kGameHeight) {
+		if (pos.x >= 0 && pos.x < g_engine->screenWidth() && pos.y >= 0 && pos.y < g_engine->gameHeight()) {
 			const uint8 bgDepth = g_engine->_depthMap.getPixel(pos.x, pos.y);
 			if (bgDepth >= characterDepth)
 				continue;
@@ -3540,7 +3556,7 @@ bool Character::isWalkable(const Common::Point &p) const {
 	return Macs2Engine::isWalkabilityWalkable(lookupWalkability(p));
 }
 
-Character::Character() : _pathfindingOverlay(kScreenWidth * kGameHeight, 0) {
+Character::Character() : _pathfindingOverlay(g_engine->screenWidth() * g_engine->gameHeight(), 0) {
 }
 
 bool Character::calculatePath(Common::Point target) {


Commit: 95a613f7475e0bcbc97d4e1cb9e3a707db99eca2
    https://github.com/scummvm/scummvm/commit/95a613f7475e0bcbc97d4e1cb9e3a707db99eca2
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-21T10:43:52+02:00

Commit Message:
MACS2: use engine getters instead of constants

Changed paths:
    engines/macs2/view1.cpp


diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index e994b9c589e..fcb467ef2bf 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -2752,9 +2752,9 @@ void View1::drawAllCharacters(Graphics::ManagedSurface *surface, bool fullUpdate
 
 			int shadingTableOffset = 0;
 			if (g_engine->_shadowMap.w > 0) {
-				const int sx = CLIP<int>(charX, 0, kScreenWidthLast);
-				const int sy = CLIP<int>(charY, 0, kGameHeightLast);
-				shadingTableOffset = MIN<int>(g_engine->_shadowMap.getPixel(sx, sy), 0x20);
+				const int sx = CLIP<int>(charX, 0, g_engine->screenWidth() - 1);
+				const int sy = CLIP<int>(charY, 0, g_engine->gameHeight() - 1);
+				shadingTableOffset = MIN<int>(g_engine->_shadowMap.getPixel(sx, sy), 0x1f);
 			}
 
 			uint16 frameWidth;


Commit: 41e44119d72c1b7204caa0b9429a5a5e4c3bfa57
    https://github.com/scummvm/scummvm/commit/41e44119d72c1b7204caa0b9429a5a5e4c3bfa57
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-21T10:43:52+02:00

Commit Message:
MACS2: some v2 resource loading

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


diff --git a/engines/macs2/gameobjects.cpp b/engines/macs2/gameobjects.cpp
index 41e6a87ad93..e9451a29217 100644
--- a/engines/macs2/gameobjects.cpp
+++ b/engines/macs2/gameobjects.cpp
@@ -38,8 +38,14 @@ Common::MemoryReadStream *Macs2::Scenes::readSceneScript(uint16 sceneIndex, Comm
 	uint32 sceneDataOffset2 = fileStream->readUint32LE();
 	fileStream->seek(sceneDataOffset2, SEEK_SET);
 
-	// DOS: skip 0x80 resource offsets, then script size + bytecode.
-	fileStream->seek(0x80, SEEK_CUR);
+	if (g_engine->isV2()) {
+		fileStream->skip(0x200);
+		fileStream->readUint16LE();
+		fileStream->readUint16LE();
+	} else {
+		// V1: 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);
@@ -433,9 +439,18 @@ 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) {
+	if (g_engine->isV2()) {
+		for (uint i = 0; i < 5; i++) {
+			const uint16 trig = _specialAnimTriggers[i];
+			if ((int16)trig >= 0 && trig == orient) {
+				const uint16 animSlot = Macs2Engine::specialAnimSlotToAnimSlot(i + 1);
+				const Common::Array<uint8> *blob = getAnimSlotBlob(animSlot);
+				return blob != nullptr && !blob->empty();
+			}
+		}
+	} else if (_overloadAnimTriggerDirection != 0x7FFF &&
+			   (int16)_overloadAnimTriggerDirection >= 0 &&
+			   _overloadAnimTriggerDirection == orient) {
 		const Common::Array<uint8> *blob = getAnimSlotBlob(overloadSlot);
 		return blob != nullptr && !blob->empty();
 	}
diff --git a/engines/macs2/gameobjects.h b/engines/macs2/gameobjects.h
index b3f2a38ccff..4064f1dcf34 100644
--- a/engines/macs2/gameobjects.h
+++ b/engines/macs2/gameobjects.h
@@ -96,8 +96,11 @@ public:
 	bool _useOverloadAnimation = false;
 	// Runtime field +0x22D: when the character's orientation matches this value,
 	// the renderer uses animation slot 0x15 (overload) instead of the normal slot.
-	// Initialized to 0x7FFF (never match). Set by opcode 0x27.
+	// Initialized to 0x7FFF (never match). Set by opcode 0x27 (V1).
 	uint16 _overloadAnimTriggerDirection = 0x7FFF;
+	// V2: five trigger words at runtime+0x50e.
+	// 0x7FFF = inactive. When orientation matches entry i, play specialAnimSlotToAnimSlot(i+1).
+	uint16 _specialAnimTriggers[5] = {0x7FFF, 0x7FFF, 0x7FFF, 0x7FFF, 0x7FFF};
 
 	// These are the values read by the code around l0037_082D:
 	Common::Point _position;
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 9f845300802..2f920291468 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -135,18 +135,33 @@ Macs2Engine::McsFileVersion Macs2Engine::detectMcsFileVersion(Common::SeekableRe
 	return McsFileVersion::Unknown;
 }
 
+const char *Macs2Engine::getResourceMcsFilename() const {
+	return "RESOURCE.MCS";
+}
+
 void Macs2Engine::readResourceFile() {
+	const char *mcsName = getResourceMcsFilename();
 	{
-		// Extra scope in order to make sure no code tries to read from the file directly.
-		Common::File file;
-		if (!file.open("RESOURCE.MCS"))
-			error("readResourceFile(): Error reading MCS file");
-
-		int64 size = file.size();
-		byte *fileData = (byte *)malloc(size);
-		file.read(fileData, size);
-
-		_fileStream = new Common::MemoryReadStream(fileData, size, DisposeAfterUse::YES);
+		Common::File *file = new Common::File();
+		if (!file->open(mcsName)) {
+			delete file;
+			error("readResourceFile(): Error reading MCS file %s", mcsName);
+		}
+
+		_mcsFileVersion = detectMcsFileVersion(*file);
+		if (_mcsFileVersion == McsFileVersion::V1) {
+			_mcsDirectoryOffset = kMcsV1DirectoryOffset;
+			debugC(1, kDebugFilePath, "MCS %s: AHFFMACS0100, directory @ 0x%x", mcsName, _mcsDirectoryOffset);
+			const int64 size = file->size();
+			byte *fileData = (byte *)malloc((size_t)size);
+			file->seek(0, SEEK_SET);
+			file->read(fileData, size);
+			delete file;
+			_fileStream = new Common::MemoryReadStream(fileData, (uint32)size, DisposeAfterUse::YES);
+		} else {
+			delete file;
+			error("readResourceFile(): unrecognized MCS magic in %s", mcsName);
+		}
 	}
 
 	_mcsFileVersion = detectMcsFileVersion(*_fileStream);
@@ -208,10 +223,11 @@ void Macs2Engine::loadResourceFileV1() {
 
 	// Load object data (512 entries max, matching original loadResourceFile)
 	// Original allocates all 512 slots, then frees unused ones. We pre-fill with nullptr.
+	const uint32 dir = getMcsDirectoryOffset();
 	GameObjects::instance()._objects.resize(0x200, nullptr);
 	for (int i = 1; i <= 0x200; i++) {
 		// Directory object DATA dword: file+kMcsV1DirectoryOffset+kMcsV1ObjectDataPtrRel+i*12
-		const uint32 addressOffset = kMcsV1DirectoryOffset + kMcsV1ObjectDataPtrRel + (uint32)i * 0xC;
+		const uint32 addressOffset = dir + kMcsV1ObjectDataPtrRel + (uint32)i * 0xC;
 		_fileStream->seek(addressOffset, SEEK_SET);
 		uint32 objectOffset = _fileStream->readUint32LE();
 		if (objectOffset == 0) {
@@ -2432,6 +2448,8 @@ bool Macs2Engine::loadObjectData(GameObject *obj) {
 	};
 
 	const uint16 animSlotCount = maxAnimSlots();
+	if (isV2())
+		_fileStream->readUint16LE(); // ReadyObject lead word before anim slots
 	for (int j = 0; j < (int)animSlotCount; j++) {
 		_fileStream->readUint16LE(); // animID (editor metadata, unused at runtime)
 		uint16 blobSourceKey = _fileStream->readUint16LE();
@@ -2516,7 +2534,7 @@ bool Macs2Engine::loadObjectData(GameObject *obj) {
 	// Binary loadObjectData (1008:08ec): runtime+0x21D = object vertical offset.
 	obj->_storedWalkRuntime.motionTargetVerticalOffset = obj->_verticalOffsetScale;
 
-	const uint32 scriptTableOffset = 0x17F8 + (0xC + 0x04) + obj->_index * 0xC;
+	const uint32 scriptTableOffset = getMcsDirectoryOffset() + kMcsV1ObjectScriptPtrRel + obj->_index * 0xC;
 	_fileStream->seek(scriptTableOffset, SEEK_SET);
 	const uint32 scriptOffset = _fileStream->readUint32LE();
 	if (scriptOffset != 0) {
@@ -2525,6 +2543,11 @@ bool Macs2Engine::loadObjectData(GameObject *obj) {
 		for (uint r = 0; r < maxObjRes; r++) {
 			obj->_resourceOffsets[r] = _fileStream->readUint32LE();
 		}
+		if (isV2()) {
+			_fileStream->skip(0x200 - maxObjRes * 4);
+			_fileStream->readUint16LE();
+			_fileStream->readUint16LE();
+		}
 		const uint16 scriptLength = _fileStream->readUint16LE();
 		obj->_script.resize(scriptLength);
 		if (scriptLength > 0) {
@@ -2604,9 +2627,24 @@ Common::String Macs2Engine::getGameId() const {
 	return _gameDescription->gameId;
 }
 
+uint16 Macs2Engine::specialAnimSlotToAnimSlot(uint16 specialSlot) {
+	static const uint16 kMap[5] = {0x15, 0x11, 0x16, 0x17, 0x18};
+	if (specialSlot < 1 || specialSlot > 5)
+		return 0;
+	return kMap[specialSlot - 1];
+}
+
 uint16 Macs2Engine::resolveAnimSlotIndex(const GameObject *obj) const {
 	if (obj == nullptr)
 		return 0;
+	if (isV2()) {
+		for (uint i = 0; i < 5; i++) {
+			const uint16 trig = obj->_specialAnimTriggers[i];
+			if ((int16)trig >= 0 && trig == obj->_orientation)
+				return specialAnimSlotToAnimSlot(i + 1);
+		}
+		return obj->_orientation;
+	}
 	if ((int16)obj->_overloadAnimTriggerDirection < 0 ||
 		obj->_overloadAnimTriggerDirection != obj->_orientation) {
 		return obj->_orientation;
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index ff9a457e337..f55bcd9f747 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -294,6 +294,7 @@ public:
 	McsFileVersion detectMcsFileVersion(Common::SeekableReadStream &stream) const;
 	/** Load AHFFMACS0100 layout (loadResourceFile @ 1008:2e8d). */
 	void loadResourceFileV1();
+	const char *getResourceMcsFilename() const;
 	/** Amiga: open DataA/Mdir, load OO objects as GameObjects, cursors, and scene stubs. */
 	void readAmigaResources();
 	void applyAmigaUiPalette();
@@ -512,6 +513,8 @@ public:
 
 	Common::MemoryReadStream *_fileStream = nullptr;
 	McsFileVersion _mcsFileVersion = McsFileVersion::Unknown;
+	/** Absolute file offset of the 0x3000-byte scene/object directory. */
+	uint32 _mcsDirectoryOffset = kMcsV1DirectoryOffset;
 
 	/** Amiga MXFF line pitch: measureTextWidth @ 00224420 uses (font[+8] - 1). */
 	uint16 amigaTextLinePitch = 0;
@@ -725,11 +728,11 @@ public:
 
 	bool isDemo() const { return getFeatures() & ADGF_DEMO; }
 
-	/** MCS directory base (v1: file+0x10 after magic + actor/scene words). */
-	uint32 getMcsDirectoryOffset() const { return kMcsV1DirectoryOffset; }
-
 	/** AHFFMACS0200 dialect (directory at 0x212) vs AHFFMACS0100 (0x10). */
 	bool isV2() const { return _mcsFileVersion == McsFileVersion::V2; }
+
+	/** MCS directory base. */
+	uint32 getMcsDirectoryOffset() const { return _mcsDirectoryOffset; }
 	McsFileVersion getMcsFileVersion() const { return _mcsFileVersion; }
 	int screenWidth() const { return isV2() ? kWinScreenWidth : kScreenWidth; }
 	int screenWidthLast() const { return screenWidth() - 1; }
@@ -777,6 +780,7 @@ public:
 	uint16 maxOrientations() const { return isV2() ? 0x25 : 0x14; }
 	/** Overload / special-anim slot index. */
 	uint16 overloadAnimSlot() const { return maxAnimSlots(); }
+	static uint16 specialAnimSlotToAnimSlot(uint16 specialSlot);
 	/** Scene hotspot override table entries (1-based inclusive max). */
 	uint16 maxHotspots() const { return 0x10; }
 	/** Per-object resource offset table entries. */
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index d132bc37310..bfa6c360783 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -1175,8 +1175,12 @@ OpcodeResult Script::ScriptExecutor::scriptChangeScene() {
 	_repeatRunFlag = false;
 	_isSceneInitRun = false;
 	const uint32 newSceneID = scriptReadValue32();
-	const uint16 transitionMode = scriptReadValue16();
-	const uint16 transitionSpeed = scriptReadValue16();
+	uint16 transitionMode = 1;
+	uint16 transitionSpeed = 1;
+	if (!_engine->isV2()) {
+		transitionMode = scriptReadValue16();
+		transitionSpeed = scriptReadValue16();
+	}
 	debugC(kDebugScript, "SCRIPT::changeScene(newSceneID=%u, transitionMode=%u, transitionSpeed=%u)", newSceneID, transitionMode, transitionSpeed);
 
 	if (newSceneID == 0 || newSceneID > 0x200) {
@@ -1186,25 +1190,29 @@ OpcodeResult Script::ScriptExecutor::scriptChangeScene() {
 		endBuffering(_lastOpcodeTriggeredSkip);
 		return OpcodeResult::ReturnFinished;
 	}
-	if (transitionMode == 0 && (transitionSpeed == 0 || transitionSpeed > 0x40)) {
-		setScriptError(0x26);
-		endTimer();
-		endFrameWait();
-		endBuffering(_lastOpcodeTriggeredSkip);
-		return OpcodeResult::ReturnFinished;
-	}
-	if (transitionMode > 1) {
-		setScriptError(4);
-		endTimer();
-		endFrameWait();
-		endBuffering(_lastOpcodeTriggeredSkip);
-		return OpcodeResult::ReturnFinished;
+	if (!_engine->isV2()) {
+		if (transitionMode == 0 && (transitionSpeed == 0 || transitionSpeed > 0x40)) {
+			setScriptError(0x26);
+			endTimer();
+			endFrameWait();
+			endBuffering(_lastOpcodeTriggeredSkip);
+			return OpcodeResult::ReturnFinished;
+		}
+		if (transitionMode > 1) {
+			setScriptError(4);
+			endTimer();
+			endFrameWait();
+			endBuffering(_lastOpcodeTriggeredSkip);
+			return OpcodeResult::ReturnFinished;
+		}
 	}
 
 	// Binary scriptChangeScene (1008:ad6e): beginFrame, hourglass cursor, flipScreen
 	// before loading the new scene. Also drop stale text/dialogue flags from the
 	// previous scene so blocking waits are not misclassified as UI-click waits.
 	clearScriptUiWaitState();
+	if (_cursorMode != MouseMode::Disabled)
+		_cursorModeBeforeWait = _cursorMode;
 	_engine->setCursorMode(MouseMode::Disabled);
 
 	View1 *currentView = (View1 *)_engine->findView("View1");
@@ -2092,9 +2100,17 @@ OpcodeResult Script::ScriptExecutor::scriptSubValues() {
 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();
+	uint16 specialSlot = 1;
+	uint16 shouldMirror = 0;
+	if (_engine->isV2()) {
+		specialSlot = scriptReadValue16();
+		shouldMirror = scriptReadValue16();
+	} else {
+		shouldMirror = scriptReadValue16();
+	}
 	const uint8 animationID = readByte();
-	debugC(kDebugScript, "SCRIPT::loadSpecialAnim(objectID=%u, animationID=%u, shouldMirror=%u)", id, animationID, shouldMirror);
+	debugC(kDebugScript, "SCRIPT::loadSpecialAnim(objectID=%u, specialSlot=%u, animationID=%u, shouldMirror=%u)",
+		   id, specialSlot, animationID, shouldMirror);
 
 	clearScriptError();
 	if (id < 1 || id > 0x200) {
@@ -2110,24 +2126,49 @@ OpcodeResult Script::ScriptExecutor::scriptLoadSpecialAnim() {
 		setScriptError(2);
 		return OpcodeResult::Continue;
 	}
+	if (_engine->isV2() && (specialSlot < 1 || specialSlot > 5)) {
+		setScriptError(0x2e);
+		return OpcodeResult::Continue;
+	}
 
 	const Common::Array<uint8> &blob = Scenes::instance().readSpecialAnimBlob(animationID, g_engine->_fileStream);
-	object->_overloadAnimation = blob;
-	object->_overloadAnimationMirrored = (shouldMirror != 0);
-	if (shouldMirror != 0) {
-		BackgroundAnimationBlob::mirrorAnimBlob(object->_overloadAnimation);
+	Common::Array<uint8> animData = blob;
+	if (shouldMirror != 0)
+		BackgroundAnimationBlob::mirrorAnimBlob(animData);
+
+	const uint16 animSlot = _engine->isV2()
+								? Macs2Engine::specialAnimSlotToAnimSlot(specialSlot)
+								: _engine->overloadAnimSlot();
+	if (animSlot < 1) {
+		setScriptError(0x2e);
+		return OpcodeResult::Continue;
 	}
-	while (object->_blobs.size() <= 20)
+
+	while (object->_blobs.size() < animSlot)
 		object->_blobs.push_back(Common::Array<uint8>());
-	object->_blobs[20] = object->_overloadAnimation;
+	object->_blobs[animSlot - 1] = animData;
+
+	// Keep V1 overload mirror fields in sync when targeting the classic overload slot.
+	if (animSlot == 0x15 || animSlot == _engine->overloadAnimSlot()) {
+		object->_overloadAnimation = animData;
+		object->_overloadAnimationMirrored = (shouldMirror != 0);
+	}
 	return OpcodeResult::Continue;
 }
 
 OpcodeResult Script::ScriptExecutor::scriptSetDirection() {
 	// scriptSetDirection (1008:c858). Writes to runtime field +0x22D.
 	const uint32 characterID = scriptReadValue32() - 0x400;
-	const uint16 value = scriptReadValue16();
-	debugC(kDebugScript, "SCRIPT::setDirection(characterID=%u, value=%u)", characterID, value);
+	uint16 specialSlot = 1;
+	uint16 value = 0;
+	if (_engine->isV2()) {
+		specialSlot = scriptReadValue16();
+		value = scriptReadValue16();
+	} else {
+		value = scriptReadValue16();
+	}
+	debugC(kDebugScript, "SCRIPT::setDirection/setSpecialAnim(characterID=%u, specialSlot=%u, value=%u)",
+		   characterID, specialSlot, value);
 
 	clearScriptError();
 	if (characterID < 1 || characterID > 0x200) {
@@ -2143,6 +2184,19 @@ OpcodeResult Script::ScriptExecutor::scriptSetDirection() {
 		setScriptError(2);
 		return OpcodeResult::Continue;
 	}
+	if (_engine->isV2()) {
+		if (specialSlot < 1 || specialSlot > 5) {
+			setScriptError(0x2e);
+			return OpcodeResult::Continue;
+		}
+		// Binary: clear any other special slot that already uses this direction.
+		for (uint i = 0; i < 5; i++) {
+			if (object->_specialAnimTriggers[i] == value)
+				object->_specialAnimTriggers[i] = 0;
+		}
+		object->_specialAnimTriggers[specialSlot - 1] = value;
+		return OpcodeResult::Continue;
+	}
 	object->_overloadAnimTriggerDirection = value;
 	return OpcodeResult::Continue;
 }
@@ -2150,7 +2204,11 @@ OpcodeResult Script::ScriptExecutor::scriptSetDirection() {
 OpcodeResult Script::ScriptExecutor::scriptStopAnimation() {
 	// scriptStopAnimation (1008:c8e4).
 	const uint32 characterID = scriptReadValue32() - 0x400;
-	debugC(kDebugScript, "SCRIPT::stopAnimation(characterID=%u)", characterID);
+	uint16 specialSlot = 1;
+	if (_engine->isV2())
+		specialSlot = scriptReadValue16();
+	debugC(kDebugScript, "SCRIPT::stopAnimation/clearSpecialAnim(characterID=%u, specialSlot=%u)",
+		   characterID, specialSlot);
 
 	clearScriptError();
 	if (characterID < 1 || characterID > 0x200) {
@@ -2166,6 +2224,21 @@ OpcodeResult Script::ScriptExecutor::scriptStopAnimation() {
 		setScriptError(2);
 		return OpcodeResult::Continue;
 	}
+	if (_engine->isV2()) {
+		if (specialSlot < 1 || specialSlot > 5) {
+			setScriptError(0x2e);
+			return OpcodeResult::Continue;
+		}
+		obj->_specialAnimTriggers[specialSlot - 1] = 0x7FFF;
+		const uint16 animSlot = Macs2Engine::specialAnimSlotToAnimSlot(specialSlot);
+		if (animSlot >= 1 && obj->_blobs.size() >= animSlot)
+			obj->_blobs[animSlot - 1].clear();
+		if (animSlot == 0x15) {
+			obj->_useOverloadAnimation = false;
+			obj->_overloadAnimation.clear();
+		}
+		return OpcodeResult::Continue;
+	}
 	obj->_overloadAnimTriggerDirection = 0x7FFF;
 	obj->_useOverloadAnimation = false;
 	obj->_overloadAnimation.clear();




More information about the Scummvm-git-logs mailing list