[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 ¤t = g_engine->_backgroundAnimations[i];
BackgroundAnimationBlob ¤tBlob = 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(¤tBlob._blob[frameStart]);
- int16 frameOffsetY = (int16)READ_LE_UINT16(¤tBlob._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(¤tBlob._blob[frameStart + 6]);
- currentFrame._height = READ_LE_UINT16(¤tBlob._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(), ¤tBlob._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 ¢er = 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