[Scummvm-git-logs] scummvm master -> 151c31226ba2140a0efc28f4bf1e93e0f27b3d84
mgerhardy
noreply at scummvm.org
Sun Aug 2 19:30:54 UTC 2026
This automated email contains information about 18 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
d869392cd9 MACS2: fixed argument order in isPathWalkable call
d93c8d422c MACS2: renamed adlib to music
0a6d5df7f6 MACS2: cleanup for amiga preparations
944105844c MACS2: extract some constants into functions to prepare for the amiga demo support
f86c012f35 MACS2: minor cleanup in renaming methods and removing loadSongFromSceneData
ab78c9292a MACS2: added amiga demo detection
f33e897885 MACS2: fill some of the platform specific magic values for the amiga
b6556791e7 MACS2: amiga string decoding is different
14601bf994 MACS2: resize of the top of the function
2764981a74 MACS2: don't read data from exe for amiga
7d36e8edeb MACS2: music is (or will be) a facade not just for adlib
0484fcbbf8 MACS2: leave a comment for my future me regarding full version and demo version offsets
1a6023f754 MACS2: opcode table
fe4f8be393 MACS2: fixed opcode outer loop exit
ef2c114ffb MACS2: improved amiga demo support
4330a2ada7 MACS2: improved mcs v1 magic number situation (named a few things)
3dad819e2d MACS2: fixed inventory item transfer in scumm verb ui mode
151c31226b MACS2: replaced opcode functions with script callbacks
Commit: d869392cd9c27d934908c51c907bcaee0686ff03
https://github.com/scummvm/scummvm/commit/d869392cd9c27d934908c51c907bcaee0686ff03
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00
Commit Message:
MACS2: fixed argument order in isPathWalkable call
Changed paths:
engines/macs2/scriptexecutor.cpp
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 0734b2c504b..40cb21fbcc2 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -1974,7 +1974,8 @@ void Script::ScriptExecutor::scriptMoveToPosition() {
}
const Common::Point target(x, y);
- if (!_engine->isPathWalkable(object->_position.y, object->_position.x, y, x) &&
+ // Binary scriptMoveToPosition (1008:bafc): isPathWalkable(targetY, targetX, objY, objX).
+ if (!_engine->isPathWalkable(y, x, object->_position.y, object->_position.x) &&
Macs2Engine::isWalkabilityWalkable(_engine->getWalkabilityAt(target))) {
setScriptError(0x15);
return;
Commit: d93c8d422cd3c4b99b158bf73aaaf9b90d094ecf
https://github.com/scummvm/scummvm/commit/d93c8d422cd3c4b99b158bf73aaaf9b90d094ecf
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00
Commit Message:
MACS2: renamed adlib to music
Changed paths:
engines/macs2/debugtools.cpp
engines/macs2/macs2.cpp
engines/macs2/macs2.h
engines/macs2/scriptexecutor.cpp
engines/macs2/view1.cpp
diff --git a/engines/macs2/debugtools.cpp b/engines/macs2/debugtools.cpp
index baae93e051d..b9ea027eb96 100644
--- a/engines/macs2/debugtools.cpp
+++ b/engines/macs2/debugtools.cpp
@@ -2197,7 +2197,7 @@ static void showSoundWindow() {
return;
}
- Music *adlib = g_engine->getAdlib();
+ Music *adlib = g_engine->getMusic();
const Music::DebugState &ds = adlib->_debug;
ImGui::Text("Master Volume: %u/63", ds.masterVolume);
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 2725c640e3b..970b5f82c73 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -305,7 +305,7 @@ void Macs2Engine::readExecutable() {
exeFileStream.reset(new Common::MemoryReadStream(fileData, size, DisposeAfterUse::YES));
}
- _adlib->readDataFromExecutable(exeFileStream.get());
+ _music->readDataFromExecutable(exeFileStream.get());
exeFileStream->seek(0x0001B610, SEEK_SET);
inventoryIconIndices.resize(6);
@@ -396,7 +396,7 @@ Macs2Engine::Macs2Engine(OSystem *syst, const ADGameDescription *gameDesc) : Eng
g_engine = this;
_scriptExecutor = new Script::ScriptExecutor();
_scriptExecutor->_engine = this;
- _adlib = new Music();
+ _music = new Music();
// We have a fixed 0x10 number of entries
_hotspotOverrides.resize(0x11);
@@ -411,8 +411,8 @@ Macs2Engine::~Macs2Engine() {
#endif
stopInputRecording();
clearCurrentSoundData();
- _adlib->deinit();
- delete _adlib;
+ _music->deinit();
+ delete _music;
delete _fileStream;
delete _scriptExecutor;
for (uint i = 0; i < GameObjects::instance()._objects.size(); i++) {
@@ -437,13 +437,13 @@ void Macs2Engine::sayText(const Common::String &text, Common::TextToSpeechManage
void Macs2Engine::syncSoundSettings() {
Engine::syncSoundSettings();
- if (_adlib && _scriptExecutor) {
+ if (_music && _scriptExecutor) {
int musicVolume = ConfMan.getInt("music_volume");
// OPL emulator is registered as kPlainSoundType; mute it at mixer level
// when user sets music volume to 0 (OPL attenuation 0x3F is not true silence).
_mixer->muteSoundType(Audio::Mixer::kPlainSoundType,
(musicVolume == 0) || (ConfMan.hasKey("mute") && ConfMan.getBool("mute")));
- _adlib->setVolume(scaledMusicVolume(_scriptExecutor->_musicControlVolume));
+ _music->setVolume(scaledMusicVolume(_scriptExecutor->_musicControlVolume));
}
}
@@ -1961,7 +1961,7 @@ void Macs2Engine::loadSongFromSceneData(uint8 dataIndex) {
Common::Array<uint8> data;
data.resize(size);
_fileStream->read(data.data(), size);
- _adlib->playSongData(data);
+ _music->playSongData(data);
}
void Macs2Engine::setCurrentSoundData(const Common::Array<uint8> &data) {
@@ -2021,7 +2021,7 @@ Common::Error Macs2Engine::run() {
CursorMan.showMouse(false);
// Initialize Adlib
- _adlib->init();
+ _music->init();
syncSoundSettings();
// Set the engine's debugger console
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index ea07d195a76..b4f22f649bc 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -236,7 +236,7 @@ class Macs2Engine : public Engine, public Events {
private:
const ADGameDescription *_gameDescription;
- Music *_adlib = nullptr;
+ Music *_music = nullptr;
protected:
// Engine APIs
@@ -413,7 +413,7 @@ public:
void sortObjectsByDepth(uint16 objectIndex);
void loadSongFromSceneData(uint8 dataIndex);
- Music *getAdlib() const { return _adlib; }
+ Music *getMusic() const { return _music; }
// Returns the Music volume (0-63) scaled by the user's music_volume setting
uint16 scaledMusicVolume(uint16 gameAttenuation) const;
void setCurrentSoundData(const Common::Array<uint8> &data);
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 40cb21fbcc2..9b57dd6dd81 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -2707,7 +2707,7 @@ void Script::ScriptExecutor::scriptPlayMusicSlot() {
}
if (_activeMusicSlot != 0) {
- _engine->getAdlib()->stopMusic();
+ _engine->getMusic()->stopMusic();
_activeMusicSlot = 0;
}
@@ -2717,17 +2717,17 @@ void Script::ScriptExecutor::scriptPlayMusicSlot() {
return;
}
- _engine->getAdlib()->playSongData(_musicSlots[slotID - 1]);
+ _engine->getMusic()->playSongData(_musicSlots[slotID - 1]);
if (startMuted == 0) {
_musicControlMode = 1;
_musicControlStep = fadeParam;
_musicControlVolume = 0x3F;
- _engine->getAdlib()->setVolume(_engine->scaledMusicVolume(_musicControlVolume));
+ _engine->getMusic()->setVolume(_engine->scaledMusicVolume(_musicControlVolume));
} else {
_musicControlMode = 0;
_musicControlStep = 0;
_musicControlVolume = 0;
- _engine->getAdlib()->setVolume(_engine->scaledMusicVolume(0));
+ _engine->getMusic()->setVolume(_engine->scaledMusicVolume(0));
}
_activeMusicSlot = slotID;
@@ -2755,7 +2755,7 @@ void Script::ScriptExecutor::scriptStopMusicSlot() {
_musicControlStep = fadeParam;
_musicControlVolume = 0;
} else {
- _engine->getAdlib()->stopMusic();
+ _engine->getMusic()->stopMusic();
_activeMusicSlot = 0;
}
}
@@ -2783,7 +2783,7 @@ void Script::ScriptExecutor::scriptFreeMusicSlot() {
if (_activeMusicSlot == slotID) {
if (_musicEnabled && _soundSystemActive) {
- _engine->getAdlib()->stopMusic();
+ _engine->getMusic()->stopMusic();
}
_activeMusicSlot = 0;
}
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 1c7a503691f..9c2770bf9ed 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -2278,17 +2278,17 @@ bool View1::tick() {
} else {
se->_musicControlVolume = vol;
}
- g_engine->getAdlib()->setVolume(g_engine->scaledMusicVolume(se->_musicControlVolume));
+ g_engine->getMusic()->setVolume(g_engine->scaledMusicVolume(se->_musicControlVolume));
} else {
// Fade in: volume += step. When >= 63: stop music.
int vol = (int)se->_musicControlVolume + (int)musicStep;
if (vol >= 0x3F) {
se->_musicControlMode = 0;
se->_activeMusicSlot = 0;
- g_engine->getAdlib()->stopMusic();
+ g_engine->getMusic()->stopMusic();
} else {
se->_musicControlVolume = vol;
- g_engine->getAdlib()->setVolume(g_engine->scaledMusicVolume(se->_musicControlVolume));
+ g_engine->getMusic()->setVolume(g_engine->scaledMusicVolume(se->_musicControlVolume));
}
}
}
@@ -2452,7 +2452,7 @@ bool View1::tick() {
}
} else if (executor->_waitForAdlibReady) {
drawSceneUpdate();
- if (g_engine->getAdlib()->isPlaybackReady()) {
+ if (g_engine->getMusic()->isPlaybackReady()) {
executor->_waitForAdlibReady = false;
g_engine->runScriptExecutor();
}
@@ -4088,7 +4088,7 @@ void View1::openOriginalSaveLoadPanel() {
// if (g_wMusicEnabled && sceneData[g_wActiveMusicSlot] != 0) adlibStopMusic()
if (g_engine->_scriptExecutor->_musicEnabled &&
g_engine->_scriptExecutor->_activeMusicSlot != 0) {
- g_engine->getAdlib()->stopMusic();
+ g_engine->getMusic()->stopMusic();
}
// First loop: calculate max icon width/height from the 7 button images
@@ -4388,12 +4388,12 @@ void View1::handleOriginalSaveLoadClick(const Common::Point &pos) {
g_engine->_scriptExecutor->_soundSystemActive) {
uint16 slot = g_engine->_scriptExecutor->_activeMusicSlot;
if (slot != 0 && !g_engine->_scriptExecutor->_musicSlots[slot - 1].empty()) {
- g_engine->getAdlib()->playSongData(g_engine->_scriptExecutor->_musicSlots[slot - 1]);
+ g_engine->getMusic()->playSongData(g_engine->_scriptExecutor->_musicSlots[slot - 1]);
// Original's adlibTickHandler resets g_bAdlibMasterVolume=0 (full volume).
// ScummVM layers user volume on top via scaledMusicVolume, so re-apply it.
g_engine->_scriptExecutor->_musicControlMode = 0;
g_engine->_scriptExecutor->_musicControlVolume = 0;
- g_engine->getAdlib()->setVolume(g_engine->scaledMusicVolume(0));
+ g_engine->getMusic()->setVolume(g_engine->scaledMusicVolume(0));
}
}
}
Commit: 0a6d5df7f672c22093678baa3067966156ba7c8b
https://github.com/scummvm/scummvm/commit/0a6d5df7f672c22093678baa3067966156ba7c8b
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00
Commit Message:
MACS2: cleanup for amiga preparations
Changed paths:
engines/macs2/events.cpp
engines/macs2/gameobjects.cpp
engines/macs2/gameobjects.h
engines/macs2/macs2.cpp
engines/macs2/macs2.h
engines/macs2/music.cpp
engines/macs2/music.h
engines/macs2/saveload.cpp
engines/macs2/scriptexecutor.cpp
engines/macs2/view1.cpp
engines/macs2/view1.h
diff --git a/engines/macs2/events.cpp b/engines/macs2/events.cpp
index ca4b0f759da..e4d6b65f28d 100644
--- a/engines/macs2/events.cpp
+++ b/engines/macs2/events.cpp
@@ -49,13 +49,12 @@ void Events::runGame() {
if (saveSlot != -1)
g_engine->loadGameState(saveSlot);
- // DOS timer ISR fires at ~21.6Hz (PIT divisor 0xD7B0 = 55216, 1193182/55216).
- // Main loop processes a game frame when g_wTimerTickCounter > 1 (every ~2 ticks).
- // Effective game frame rate: ~10.8fps (every ~92ms).
- // We use wall-clock timing to match the original rate precisely.
+ // DOS: PIT ISR ~21.6Hz (divisor 0xD7B0). Main loop runs a game frame when the
+ // ISR counter exceeds 1 (every 2 ticks) -> ~10.8fps / ~92.6ms.
+ // Tick period and ticks-per-frame are locals so other platforms can retune later.
uint32 lastTickTime = g_system->getMillis();
- // One timer tick = 1000/21.6 â 46.3ms. Game ticks every 2 timer ticks = ~92.6ms.
- static constexpr uint32 kTimerTickMs = 46;
+ const uint32 kTimerTickMs = 46;
+ const uint16 kNormalTicksPerGameFrame = 2;
uint16 timerTickCounter = 0;
Common::Event e;
@@ -91,8 +90,8 @@ void Events::runGame() {
case 2: // slow mode: tick when counter >= 0x12
doTick = (timerTickCounter >= 0x12); // TODO: this is running at different speed compared to running this in dosbox
break;
- default: // normal: tick when counter > 1
- doTick = (timerTickCounter > 1);
+ default: // normal: every kNormalTicksPerGameFrame timer ticks
+ doTick = (timerTickCounter >= kNormalTicksPerGameFrame);
break;
}
diff --git a/engines/macs2/gameobjects.cpp b/engines/macs2/gameobjects.cpp
index 3a4cb0ad45d..6ad3304d653 100644
--- a/engines/macs2/gameobjects.cpp
+++ b/engines/macs2/gameobjects.cpp
@@ -30,25 +30,19 @@ DECLARE_SINGLETON(Macs2::Scenes);
} // namespace Common
-Common::MemoryReadStream *Macs2::Scenes::readSceneScript(uint16 sceneIndex, Common::MemoryReadStream *fileStream) {
- // Calculate the offset of the script data offset
- // This addressing can be found in the l0037_2856 code block
-
- uint16 sceneDataOffset = sceneIndex * 0xC;
- // Offset of the data in [0752h] global
- constexpr uint16 globalDataOffset = 0xC + 0x4;
- sceneDataOffset += globalDataOffset;
- fileStream->seek(sceneDataOffset - 0x8);
+Common::MemoryReadStream *Macs2::Scenes::readSceneScript(uint16 sceneIndex, Common::SeekableReadStream *fileStream) {
+ // Directory entry for sceneIndex: absolute seek uses
+ // directoryOffset + sceneIndex * 0xC - 8 (second dword of entry sceneIndex-1).
+ const uint32 directoryOffset = g_engine->getMcsDirectoryOffset();
+ fileStream->seek(directoryOffset + sceneIndex * 0xC - 0x8);
uint32 sceneDataOffset2 = fileStream->readUint32LE();
fileStream->seek(sceneDataOffset2, SEEK_SET);
- // Read the script from there
- // We read 80h bytes
+ // DOS: skip 0x80 resource offsets, then script size + bytecode.
fileStream->seek(0x80, SEEK_CUR);
uint16 scriptSize = fileStream->readUint16LE();
if (scriptSize == 0) {
warning("Macs2::Scenes::ReadSceneScript: scene %u has empty script", sceneIndex);
- // Return a valid but empty stream
return new Common::MemoryReadStream(nullptr, 0);
}
byte *scriptData = (byte *)malloc(scriptSize);
@@ -56,56 +50,45 @@ Common::MemoryReadStream *Macs2::Scenes::readSceneScript(uint16 sceneIndex, Comm
return new Common::MemoryReadStream(scriptData, scriptSize, DisposeAfterUse::YES);
}
-Common::Array<uint32> Macs2::Scenes::readSpecialAnimsOffsets(uint16 sceneIndex, Common::MemoryReadStream *fileStream) {
+Common::Array<uint32> Macs2::Scenes::readSpecialAnimsOffsets(uint16 sceneIndex, Common::SeekableReadStream *fileStream) {
Common::Array<uint32> result;
result.resize(0x80 / 4);
- // Calculate the offset of the script data offset
- // This addressing can be found in the l0037_2856 code block
- // TODO: This part is copy-pasted and could be refactored into one proper loading function
-
- uint16 sceneDataOffset = sceneIndex * 0xC;
- // Offset of the data in [0752h] global
- constexpr uint16 globalDataOffset = 0xC + 0x4;
- sceneDataOffset += globalDataOffset;
- fileStream->seek(sceneDataOffset - 0x8);
+ const uint32 directoryOffset = g_engine->getMcsDirectoryOffset();
+ fileStream->seek(directoryOffset + sceneIndex * 0xC - 0x8);
uint32 sceneDataOffset2 = fileStream->readUint32LE();
fileStream->seek(sceneDataOffset2, SEEK_SET);
- // Read the script from there
- // We read 80h bytes for the special animations offsets
fileStream->read(result.data(), 0x80);
return result;
}
-Common::MemoryReadStream *Macs2::Scenes::readSceneStrings(uint16 sceneIndex, Common::MemoryReadStream *fileStream) {
- // Calculate the offset of the script data offset
- // This addressing can be found in the l0037_2856 code block
-
- uint16 sceneDataOffset = sceneIndex * 0xC;
- // Offset of the data in [0752h] global
- constexpr uint16 globalDataOffset = 0xC + 0x4;
- sceneDataOffset += globalDataOffset;
- fileStream->seek(sceneDataOffset - 0x4);
+Common::MemoryReadStream *Macs2::Scenes::readSceneStrings(uint16 sceneIndex, Common::SeekableReadStream *fileStream) {
+ const uint32 directoryOffset = g_engine->getMcsDirectoryOffset();
+ // Third dword of entry (sceneIndex-1) / strings blob â DOS formula directory+scene*0xC-4.
+ fileStream->seek(directoryOffset + sceneIndex * 0xC - 0x4);
uint32 sceneDataOffset2 = fileStream->readUint32LE();
fileStream->seek(sceneDataOffset2, SEEK_SET);
- // This lives in l0037_A4FC:
-
- // Read the script from there
- // Size - lives in global [0F84h]
uint16 size = fileStream->readUint16LE();
- // Stringdata lives in the pointer [0F80h]
byte *stringData = (byte *)malloc(size);
fileStream->read(stringData, size);
return new Common::MemoryReadStream(stringData, size, DisposeAfterUse::YES);
- // Note: We save the current scene number to [0F86h] - maybe "scene we have strings loaded for"?
}
-Common::Array<uint8> Macs2::Scenes::readSpecialAnimBlob(uint16 index, Common::MemoryReadStream *fileStream) {
+Common::Array<uint8> Macs2::Scenes::readSpecialAnimBlob(uint16 index, Common::SeekableReadStream *fileStream) {
+ if (index == 0 || index > _currentSceneSpecialAnimOffsets.size()) {
+ warning("readSpecialAnimBlob: index %u out of range (table size %u)",
+ index, _currentSceneSpecialAnimOffsets.size());
+ return Common::Array<uint8>();
+ }
uint32 offset = _currentSceneSpecialAnimOffsets[index - 1];
+ if (offset == 0 || fileStream == nullptr) {
+ warning("readSpecialAnimBlob: null offset for index %u", index);
+ return Common::Array<uint8>();
+ }
fileStream->seek(offset, SEEK_SET);
uint32 length = fileStream->readUint32LE();
// Skip a string - note the original code adds 0x4 for the previously read size since
@@ -401,33 +384,18 @@ Macs2::GameObject *Macs2::GameObjects::getObjectByIndex(uint16 index) {
return instance()._objects[index - 1];
}
-Common::MemoryReadStream *Macs2::GameObjects::readGameObjectStrings(uint16 index, Common::MemoryReadStream *fileStream) {
- // TODO: The original binary caches the last loaded object's string data in memory
- // (g_wStringDecodeCacheObjectId at DS:0f86 / g_pSavedScriptState). It skips the file
- // read if the same object is requested again. We re-read from file every time.
- // TODO: Copy&Pasted code from ReadSceneStrings
- // Calculate the offset of the script data offset
- // This addressing can be found in the l0037_2856 code block
-
- uint16 gameObjectDataOffset = index * 0xC;
- // Offset of the data in [0752h] global
- constexpr uint16 globalDataOffset = 0xC + 0x4;
- gameObjectDataOffset += globalDataOffset;
- fileStream->seek(gameObjectDataOffset + 0x17FC);
+Common::MemoryReadStream *Macs2::GameObjects::readGameObjectStrings(uint16 index, Common::SeekableReadStream *fileStream) {
+ const uint32 directoryOffset = g_engine->getMcsDirectoryOffset();
+ // Object string table pointer at directory + index*0xC + 0x17FC.
+ fileStream->seek(directoryOffset + index * 0xC + 0x17FC);
uint32 sceneDataOffset2 = fileStream->readUint32LE();
fileStream->seek(sceneDataOffset2, SEEK_SET);
- // This lives in l0037_A4FC:
-
- // Read the script from there
- // Size - lives in global [0F84h]
uint16 size = fileStream->readUint16LE();
- // Stringdata lives in the pointer [0F80h]
byte *stringData = (byte *)malloc(size);
fileStream->read(stringData, size);
return new Common::MemoryReadStream(stringData, size, DisposeAfterUse::YES);
- // Note: We save the current scene number to [0F86h] - maybe "scene we have strings loaded for"?
}
Common::Array<uint8> *Macs2::GameObject::getAnimSlotBlob(uint16 slot) {
diff --git a/engines/macs2/gameobjects.h b/engines/macs2/gameobjects.h
index 1d0482b5602..b158190e0cb 100644
--- a/engines/macs2/gameobjects.h
+++ b/engines/macs2/gameobjects.h
@@ -29,6 +29,7 @@
namespace Common {
class MemoryReadStream;
class MemoryReadStreamEndian;
+class SeekableReadStream;
} // namespace Common
namespace Macs2 {
@@ -60,10 +61,10 @@ public:
class Common::MemoryReadStream *_currentSceneStrings;
Common::Array<uint32> _currentSceneSpecialAnimOffsets;
- class Common::MemoryReadStream *readSceneScript(uint16 sceneIndex, Common::MemoryReadStream *fileStream);
- Common::Array<uint32> readSpecialAnimsOffsets(uint16 sceneIndex, Common::MemoryReadStream *fileStream);
- class Common::MemoryReadStream *readSceneStrings(uint16 sceneIndex, Common::MemoryReadStream *fileStream);
- Common::Array<uint8> readSpecialAnimBlob(uint16 index, Common::MemoryReadStream *fileStream);
+ class Common::MemoryReadStream *readSceneScript(uint16 sceneIndex, Common::SeekableReadStream *fileStream);
+ Common::Array<uint32> readSpecialAnimsOffsets(uint16 sceneIndex, Common::SeekableReadStream *fileStream);
+ class Common::MemoryReadStream *readSceneStrings(uint16 sceneIndex, Common::SeekableReadStream *fileStream);
+ Common::Array<uint8> readSpecialAnimBlob(uint16 index, Common::SeekableReadStream *fileStream);
};
class AnimationReader {
@@ -230,7 +231,7 @@ public:
static GameObject *getObjectByIndex(uint16 index);
- static class Common::MemoryReadStream *readGameObjectStrings(uint16 index, Common::MemoryReadStream *fileStream);
+ static class Common::MemoryReadStream *readGameObjectStrings(uint16 index, Common::SeekableReadStream *fileStream);
};
} // namespace Macs2
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 970b5f82c73..44e434475ec 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -398,8 +398,7 @@ Macs2Engine::Macs2Engine(OSystem *syst, const ADGameDescription *gameDesc) : Eng
_scriptExecutor->_engine = this;
_music = new Music();
- // We have a fixed 0x10 number of entries
- _hotspotOverrides.resize(0x11);
+ _hotspotOverrides.resize(0x21);
for (uint i = 0; i < _hotspotOverrides.size(); i++) {
_hotspotOverrides[i] = 0xFFFF;
}
@@ -1167,7 +1166,7 @@ void Macs2Engine::setCursorMode(Script::MouseMode newMode) {
cursorHalfSize(oldMode, oldHalfW, oldHalfH);
Common::Point mouse = g_system->getEventManager()->getMousePos();
- const bool mouseInUiPanel = scummVerbUI && mouse.y >= kGameHeight;
+ const bool mouseInUiPanel = scummVerbUI && mouse.y >= gameHeight();
_scriptExecutor->_cursorMode = newMode;
@@ -1182,8 +1181,8 @@ void Macs2Engine::setCursorMode(Script::MouseMode newMode) {
mouse.y -= newHalfH;
const int maxY = scummVerbUI ? (kScreenHeightLast - (int)newHalfH)
- : (kGameHeightLast - (int)newHalfH);
- mouse.x = CLIP<int>(mouse.x, (int)newHalfW, kScreenWidthLast - (int)newHalfW);
+ : (gameHeightLast() - (int)newHalfH);
+ mouse.x = CLIP<int>(mouse.x, (int)newHalfW, screenWidthLast() - (int)newHalfW);
mouse.y = CLIP<int>(mouse.y, (int)newHalfH, maxY);
g_system->warpMouse(mouse.x, mouse.y);
}
@@ -1209,8 +1208,7 @@ void Macs2Engine::setCursorMode(Script::MouseMode newMode) {
uint16 Macs2Engine::getHotspotAtPoint(const Common::Point &p) {
uint16 result = 0;
- // TODO: Abstract the screen sizes
- if (p.x < 0 || p.x >= kScreenWidth || p.y < 0 || p.y >= kGameHeight || _hotspotMap.w == 0) {
+ if (p.x < 0 || p.x >= screenWidth() || p.y < 0 || p.y >= gameHeight() || _hotspotMap.w == 0) {
return result;
}
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index b4f22f649bc..3e3f7a23ac4 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -332,6 +332,8 @@ public:
int computeMinCostToReachable(int nodeIndex, int prevNode, uint16 actorIndex, const bool *reachable, int nodeCount, const Common::Point &finalDest);
// This is the override list living at [5BD1]
+ // Savegames sync 16 words into indices 1..16 (array size 0x11 during sync).
+ // Runtime capacity is 0x21 for later growth after load.
Common::Array<uint16> _hotspotOverrides;
Common::Array<Macs2::AnimFrame> _imageResources;
@@ -506,6 +508,13 @@ public:
bool isDemo() const { return getFeatures() & ADGF_DEMO; }
+ /** MCS directory base; DOS resource layout uses 0x10. */
+ uint32 getMcsDirectoryOffset() const { return 0x10; }
+ int screenWidth() const { return kScreenWidth; }
+ int screenWidthLast() const { return screenWidth() - 1; }
+ int gameHeight() const { return kGameHeight; }
+ int gameHeightLast() const { return gameHeight() - 1; }
+
/**
* Returns the game Id
*/
diff --git a/engines/macs2/music.cpp b/engines/macs2/music.cpp
index 16de6a2cab5..df6e575327a 100644
--- a/engines/macs2/music.cpp
+++ b/engines/macs2/music.cpp
@@ -47,6 +47,8 @@ Music::~Music() {
void Music::init() {
_opl = OPL::Config::create();
+ if (_opl == nullptr)
+ return;
_opl->init();
_opl->start(new Common::Functor0Mem<void, Music>(this, &Music::onTimer), CALLBACKS_PER_SECOND);
}
@@ -91,9 +93,12 @@ void Music::silenceAll() {
}
}
-void Music::playSongData(const Common::Array<uint8> &data) {
+bool Music::playSongData(const Common::Array<uint8> &data) {
stopMusic();
+ if (_opl == nullptr)
+ return false;
+
_songData = data;
_parser = new MidiParser_Macs2();
@@ -122,11 +127,12 @@ void Music::playSongData(const Common::Array<uint8> &data) {
if (!_parser->loadMusic(_songData.data(), _songData.size())) {
delete _parser;
_parser = nullptr;
- return;
+ return false;
}
_playing = true;
_adlibPlaybackReady = false;
+ return true;
}
void Music::stopMusic() {
@@ -137,12 +143,15 @@ void Music::stopMusic() {
delete _parser;
_parser = nullptr;
}
- silenceAll();
+ if (_opl != nullptr)
+ silenceAll();
_songData.clear();
_masterVolume = 0;
}
void Music::setVolume(uint16 volume) {
+ if (_opl == nullptr)
+ return;
_masterVolume = volume & 0x3F;
}
diff --git a/engines/macs2/music.h b/engines/macs2/music.h
index 61e827e6199..64c5a21db10 100644
--- a/engines/macs2/music.h
+++ b/engines/macs2/music.h
@@ -39,11 +39,12 @@ class OPL;
namespace Macs2 {
/**
- * Music player for the macs2 engine.
+ * Music facade for the macs2 engine.
*
- * Uses MidiParser_Macs2 for event parsing and drives OPL hardware directly,
- * preserving the original game's register-write behavior for authentic sound.
- * Implements MidiDriver_BASE to receive events from the MidiParser.
+ * Callers always use these methods; backend selection belongs here so additional
+ * drivers can be wired later without scattering checks across the engine.
+ *
+ * Current backend: MidiParser_Macs2 + direct OPL register writes (MidiDriver_BASE).
*/
class Music : public MidiDriver_BASE {
public:
@@ -53,10 +54,12 @@ public:
void init();
void deinit();
- void playSongData(const Common::Array<uint8> &data);
+ /** Start song data on the active backend. Returns false if unavailable or load failed. */
+ bool playSongData(const Common::Array<uint8> &data);
void stopMusic();
void setVolume(uint16 volume);
bool isPlaybackReady() const { return _adlibPlaybackReady; }
+ bool hasAdlibBackend() const { return _opl != nullptr; }
void readDataFromExecutable(Common::MemoryReadStream *fileStream);
diff --git a/engines/macs2/saveload.cpp b/engines/macs2/saveload.cpp
index 53a3a0895e3..cb75087318e 100644
--- a/engines/macs2/saveload.cpp
+++ b/engines/macs2/saveload.cpp
@@ -357,7 +357,8 @@ Common::Error Macs2Engine::syncGame(Common::Serializer &s) {
// --- Scene data: hotspot overrides [+0x5BD3]: 32 bytes (16 x uint16) ---
// Binary table is at scene+i*2+0x5BD1, accessed with 1-based index (1..16).
- // C++ array needs 17 entries (index 0 unused, indices 1-16 used).
+ // Save format is fixed at 16 words (AHFFMSGM0100). Runtime capacity may be
+ // larger; expand only after the DOS fields are restored.
if (s.isLoading()) {
_hotspotOverrides.clear();
_hotspotOverrides.resize(0x11, 0xFFFF);
@@ -371,6 +372,8 @@ Common::Error Macs2Engine::syncGame(Common::Serializer &s) {
_hotspotOverrides[i + 1] = val;
}
}
+ if (s.isLoading() && _hotspotOverrides.size() < 0x21)
+ _hotspotOverrides.resize(0x21, 0xFFFF);
for (int i = 0; i < 4; i++) {
s.syncAsUint32LE(_sceneTimerParams[i]);
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 9b57dd6dd81..679cb038662 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -2717,7 +2717,14 @@ void Script::ScriptExecutor::scriptPlayMusicSlot() {
return;
}
- _engine->getMusic()->playSongData(_musicSlots[slotID - 1]);
+ if (!_engine->getMusic()->playSongData(_musicSlots[slotID - 1])) {
+ _musicControlMode = 0;
+ _musicControlStep = 0;
+ _musicControlVolume = 0;
+ _activeMusicSlot = slotID;
+ return;
+ }
+
if (startMuted == 0) {
_musicControlMode = 1;
_musicControlStep = fadeParam;
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 9c2770bf9ed..f84a74a2409 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -4387,8 +4387,8 @@ void View1::handleOriginalSaveLoadClick(const Common::Point &pos) {
if (g_engine->_scriptExecutor->_musicEnabled &&
g_engine->_scriptExecutor->_soundSystemActive) {
uint16 slot = g_engine->_scriptExecutor->_activeMusicSlot;
- if (slot != 0 && !g_engine->_scriptExecutor->_musicSlots[slot - 1].empty()) {
- g_engine->getMusic()->playSongData(g_engine->_scriptExecutor->_musicSlots[slot - 1]);
+ if (slot != 0 && !g_engine->_scriptExecutor->_musicSlots[slot - 1].empty() &&
+ g_engine->getMusic()->playSongData(g_engine->_scriptExecutor->_musicSlots[slot - 1])) {
// Original's adlibTickHandler resets g_bAdlibMasterVolume=0 (full volume).
// ScummVM layers user volume on top via scaledMusicVolume, so re-apply it.
g_engine->_scriptExecutor->_musicControlMode = 0;
diff --git a/engines/macs2/view1.h b/engines/macs2/view1.h
index 1950f57e986..41592ce2574 100644
--- a/engines/macs2/view1.h
+++ b/engines/macs2/view1.h
@@ -138,7 +138,7 @@ public:
// 2=advance sequence after returning current frame. Hit testing uses 0; drawing uses 2.
bool fillCurrentAnimationFrame(uint16 advanceMode, Macs2::AnimFrame &out);
Macs2::AnimFrame *getCurrentAnimationFrame(uint16 advanceMode);
- Macs2::AnimFrame *getCurrentPortrait(bool onRightSide = false, uint16 frameIndex = 2);
+ Macs2::AnimFrame *getCurrentPortrait(bool onRightSide = false, uint16 frameIndex = 0);
void update();
};
Commit: 944105844c5dbf0607a3684c64a8f69ecf4479fa
https://github.com/scummvm/scummvm/commit/944105844c5dbf0607a3684c64a8f69ecf4479fa
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00
Commit Message:
MACS2: extract some constants into functions to prepare for the amiga demo support
Changed paths:
engines/macs2/events.cpp
engines/macs2/gameobjects.cpp
engines/macs2/gameobjects.h
engines/macs2/macs2.cpp
engines/macs2/macs2.h
engines/macs2/saveload.cpp
engines/macs2/scriptexecutor.cpp
engines/macs2/scriptexecutor.h
engines/macs2/view1.cpp
diff --git a/engines/macs2/events.cpp b/engines/macs2/events.cpp
index e4d6b65f28d..a9acb40fbfb 100644
--- a/engines/macs2/events.cpp
+++ b/engines/macs2/events.cpp
@@ -51,10 +51,10 @@ void Events::runGame() {
// DOS: PIT ISR ~21.6Hz (divisor 0xD7B0). Main loop runs a game frame when the
// ISR counter exceeds 1 (every 2 ticks) -> ~10.8fps / ~92.6ms.
- // Tick period and ticks-per-frame are locals so other platforms can retune later.
+ // Tick period / ticks-per-frame come from engine facades for later platforms.
uint32 lastTickTime = g_system->getMillis();
- const uint32 kTimerTickMs = 46;
- const uint16 kNormalTicksPerGameFrame = 2;
+ const uint32 kTimerTickMs = g_engine->timerTickMs();
+ const uint16 kNormalTicksPerGameFrame = g_engine->ticksPerGameFrame();
uint16 timerTickCounter = 0;
Common::Event e;
diff --git a/engines/macs2/gameobjects.cpp b/engines/macs2/gameobjects.cpp
index 6ad3304d653..28e900f95c8 100644
--- a/engines/macs2/gameobjects.cpp
+++ b/engines/macs2/gameobjects.cpp
@@ -399,11 +399,14 @@ Common::MemoryReadStream *Macs2::GameObjects::readGameObjectStrings(uint16 index
}
Common::Array<uint8> *Macs2::GameObject::getAnimSlotBlob(uint16 slot) {
- if (slot < 1 || slot > 0x15)
+ const uint16 maxSlots = g_engine->maxAnimSlots();
+ const uint16 overloadSlot = g_engine->overloadAnimSlot();
+ if (slot < 1 || slot > maxSlots)
return nullptr;
- if (slot == 0x15) {
- if (_blobs.size() > 20 && !_blobs[20].empty())
- return &_blobs[20];
+ if (slot == overloadSlot) {
+ const uint overloadIndex = overloadSlot - 1;
+ if (_blobs.size() > overloadIndex && !_blobs[overloadIndex].empty())
+ return &_blobs[overloadIndex];
return &_overloadAnimation;
}
const uint index = slot - 1;
@@ -417,17 +420,19 @@ const Common::Array<uint8> *Macs2::GameObject::getAnimSlotBlob(uint16 slot) cons
}
bool Macs2::GameObject::isAnimSlotLoaded(uint16 orient) const {
+ const uint16 overloadSlot = g_engine->overloadAnimSlot();
+ const uint16 maxOrient = g_engine->maxOrientations();
if (_overloadAnimTriggerDirection != 0x7FFF &&
(int16)_overloadAnimTriggerDirection >= 0 &&
_overloadAnimTriggerDirection == orient) {
- const Common::Array<uint8> *blob = getAnimSlotBlob(0x15);
+ const Common::Array<uint8> *blob = getAnimSlotBlob(overloadSlot);
return blob != nullptr && !blob->empty();
}
- if (orient == 0x15) {
- const Common::Array<uint8> *blob = getAnimSlotBlob(0x15);
+ if (orient == overloadSlot) {
+ const Common::Array<uint8> *blob = getAnimSlotBlob(overloadSlot);
return blob != nullptr && !blob->empty();
}
- if (orient < 1 || orient > 0x14)
+ if (orient < 1 || orient > maxOrient)
return false;
const uint slot = orient - 1;
if (slot < _blobs.size() && !_blobs[slot].empty())
diff --git a/engines/macs2/gameobjects.h b/engines/macs2/gameobjects.h
index b158190e0cb..2fc91936bb8 100644
--- a/engines/macs2/gameobjects.h
+++ b/engines/macs2/gameobjects.h
@@ -202,8 +202,8 @@ public:
Common::MemoryReadStream *getScriptStream();
- // Binary pAnimSlots[1..0x15] at runtime+slot*0x10. Slot 0x15 may be in _blobs[20]
- // (loadObjectData) or overloadAnimation (script load / savegame).
+ // Binary pAnimSlots[1..maxAnimSlots] at runtime+slot*0x10. Overload slot may be in
+ // _blobs[overload-1] (loadObjectData) or _overloadAnimation (script load / savegame).
Common::Array<uint8> *getAnimSlotBlob(uint16 slot);
const Common::Array<uint8> *getAnimSlotBlob(uint16 slot) const;
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 44e434475ec..879eaebcc41 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -209,7 +209,8 @@ void Macs2Engine::readResourceFile() {
gameObject->_orientation = _fileStream->readUint16LE();
gameObject->_verticalOffsetScale = _fileStream->readUint16LE();
- for (int j = 1; j <= 0x15; j++) {
+ const uint16 animSlotCount = maxAnimSlots();
+ for (int j = 1; j <= (int)animSlotCount; j++) {
// Per-slot data in file: 2 bytes animID, 2 bytes sourceKey, 4 bytes dataSize, data, 2 bytes speed, 1 byte mirrorFlag, 1 byte (discarded)
_fileStream->readUint16LE(); // runtime+0x24: animation slot ID (not used at runtime, editor metadata)
uint16 blobSourceKey = _fileStream->readUint16LE();
@@ -266,7 +267,8 @@ void Macs2Engine::readResourceFile() {
_fileStream->seek(objectOffset, SEEK_SET);
// Resource offset table at +0x18D equivalent in file (128 bytes = 32 dword offsets).
// Binary loadObjectData reads this into runtime+0x18D.
- for (int r = 0; r < 32; r++) {
+ const uint maxObjRes = maxObjectResources();
+ for (uint r = 0; r < maxObjRes; r++) {
gameObject->_resourceOffsets[r] = _fileStream->readUint32LE();
}
uint16 scriptLength = _fileStream->readUint16LE();
@@ -459,11 +461,8 @@ uint16 Macs2Engine::scaledMusicVolume(uint16 gameAttenuation) const {
return (total > 0x3F) ? 0x3F : total;
}
-void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
- // Release old scene resources
- _backgroundAnimations.clear();
- _backgroundAnimationsBlobs.clear();
- memset(_areaOverrides, 0, sizeof(_areaOverrides));
+bool Macs2Engine::loadSceneGraphics(uint32 sceneIndex) {
+ const uint32 newSceneIndex = sceneIndex;
// Background image
// [0752h] is pointing to 3000h bytes data starting at Ch + 4h in the file
@@ -496,11 +495,11 @@ void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
uint8 data[0x320];
- for (int y = 0; y < kGameHeight; y++) {
+ for (int y = 0; y < gameHeight(); y++) {
// TODO: Use the proper read function, it seems to be available
uint16 length = _fileStream->readUint16LE();
_fileStream->read(data, length);
- int16 remainingPixels = kScreenWidth; // signed: see readRLEImage (matches decodeRLERows 1008:0666)
+ int16 remainingPixels = screenWidth(); // signed: see readRLEImage (matches decodeRLERows 1008:0666)
uint8 *dataPointer = data;
uint16 x = 0;
while (remainingPixels > 0) {
@@ -516,7 +515,7 @@ void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
dataPointer++;
const uint8 &encodedValue = dataPointer[0];
dataPointer++;
- for (int i = 0; i < runlength && x < kScreenWidth; i++) {
+ for (int i = 0; i < runlength && x < screenWidth(); i++) {
_sceneBackground.setPixel(x++, y, encodedValue);
}
remainingPixels -= runlength;
@@ -630,6 +629,18 @@ void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
// Apply palette darkening if this scene has it (binary: sceneData+0x5203 != 1)
applyPaletteDarkening();
+ return true;
+}
+
+void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
+ // Release old scene resources
+ _backgroundAnimations.clear();
+ _backgroundAnimationsBlobs.clear();
+ memset(_areaOverrides, 0, sizeof(_areaOverrides));
+
+ if (!loadSceneGraphics(newSceneIndex))
+ error("changeScene(): Failed to load scene graphics for scene %u", newSceneIndex);
+
// Refresh characters
View1 *currentView = (View1 *)findView("View1");
if (!currentView) {
@@ -1729,7 +1740,7 @@ bool Macs2Engine::loadAnimationFromSceneData(uint16 objectIndex, uint16 slotInde
address = _sceneResourceOffsets[arrayIndex - 1];
} else {
GameObject *execObj = GameObjects::getObjectByIndex(executingScriptObjectId);
- if (execObj == nullptr || arrayIndex == 0 || arrayIndex > 32) {
+ if (execObj == nullptr || arrayIndex == 0 || arrayIndex > maxObjectResources()) {
_scriptExecutor->setScriptError(1);
return false;
}
@@ -1747,18 +1758,20 @@ bool Macs2Engine::loadAnimationFromSceneData(uint16 objectIndex, uint16 slotInde
data.resize(size);
_fileStream->read(data.data(), size);
- while (go->_blobs.size() < 0x15)
+ const uint16 minSlots = maxAnimSlots();
+ const uint16 overloadSlot = overloadAnimSlot();
+ while (go->_blobs.size() < minSlots)
go->_blobs.push_back(Common::Array<uint8>());
- while (go->_blobSourceKeys.size() < 0x15)
+ while (go->_blobSourceKeys.size() < minSlots)
go->_blobSourceKeys.push_back(0);
- while (go->_blobMirrorFlags.size() < 0x15)
+ while (go->_blobMirrorFlags.size() < minSlots)
go->_blobMirrorFlags.push_back(false);
Common::Array<uint8> *targetBlob = nullptr;
- if (slotIndex == 0x15) {
- while (go->_blobs.size() <= 20)
+ if (slotIndex == overloadSlot) {
+ while (go->_blobs.size() <= (uint)(overloadSlot - 1))
go->_blobs.push_back(Common::Array<uint8>());
- targetBlob = &go->_blobs[20];
+ targetBlob = &go->_blobs[overloadSlot - 1];
go->_overloadAnimationSourceKey = static_cast<uint16>(address >> 16);
go->_overloadAnimationMirrored = shouldMirror;
} else {
@@ -1770,7 +1783,7 @@ bool Macs2Engine::loadAnimationFromSceneData(uint16 objectIndex, uint16 slotInde
// Binary: memFree old blob if bSlotLoaded, then alloc + read; sets slot+0x33 = 1.
*targetBlob = data;
- if (slotIndex == 0x15)
+ if (slotIndex == overloadSlot)
go->_overloadAnimation = data;
if (shouldMirror) {
BackgroundAnimationBlob::mirrorAnimBlob(*targetBlob);
@@ -1851,7 +1864,8 @@ bool Macs2Engine::loadObjectData(GameObject *obj) {
obj->_blobMirrorFlags = mirrorsBackup;
};
- for (int j = 0; j < 0x15; j++) {
+ const uint16 animSlotCount = maxAnimSlots();
+ for (int j = 0; j < (int)animSlotCount; j++) {
_fileStream->readUint16LE(); // animID (editor metadata, unused at runtime)
uint16 blobSourceKey = _fileStream->readUint16LE();
uint32 dataSize = _fileStream->readUint32LE();
@@ -1940,7 +1954,8 @@ bool Macs2Engine::loadObjectData(GameObject *obj) {
const uint32 scriptOffset = _fileStream->readUint32LE();
if (scriptOffset != 0) {
_fileStream->seek(scriptOffset, SEEK_SET);
- for (int r = 0; r < 32; r++) {
+ const uint maxObjRes = maxObjectResources();
+ for (uint r = 0; r < maxObjRes; r++) {
obj->_resourceOffsets[r] = _fileStream->readUint32LE();
}
const uint16 scriptLength = _fileStream->readUint16LE();
@@ -1997,6 +2012,16 @@ Common::String Macs2Engine::getGameId() const {
return _gameDescription->gameId;
}
+uint16 Macs2Engine::resolveAnimSlotIndex(const GameObject *obj) const {
+ if (obj == nullptr)
+ return 0;
+ if ((int16)obj->_overloadAnimTriggerDirection < 0 ||
+ obj->_overloadAnimTriggerDirection != obj->_orientation) {
+ return obj->_orientation;
+ }
+ return overloadAnimSlot();
+}
+
void Macs2Engine::setGameSpeedMode(uint16 mode) {
_gameSpeedMode = mode % 3;
ConfMan.setInt(kGameSpeedModeConfigKey, _gameSpeedMode);
@@ -2005,7 +2030,7 @@ void Macs2Engine::setGameSpeedMode(uint16 mode) {
Common::Error Macs2Engine::run() {
GameObjects::instance().init();
setGameSpeedMode(ConfMan.getInt(kGameSpeedModeConfigKey));
- readResourceFile();
+ loadBootstrapResources();
readExecutable();
// Load translation data if available
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index 3e3f7a23ac4..8066c0ff9b4 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -515,6 +515,53 @@ public:
int gameHeight() const { return kGameHeight; }
int gameHeightLast() const { return gameHeight() - 1; }
+ // --- Layout / dialect facades (DOS defaults; other platforms override later) ---
+
+ /** Game-loop timer quantum in milliseconds. */
+ uint32 timerTickMs() const { return 46; }
+ /** Normal-speed: game frames per that many timer ticks. */
+ uint16 ticksPerGameFrame() const { return 2; }
+
+ /** ReadyObject anim slots (1-based inclusive max). */
+ uint16 maxAnimSlots() const { return 0x15; }
+ /** Orientations that map to anim slots 1..N (inclusive). */
+ uint16 maxOrientations() const { return 0x14; }
+ /** Overload / special-anim slot index (DOS ReadyObject slot 0x15). */
+ uint16 overloadAnimSlot() const { return 0x15; }
+ /** Scene hotspot override table entries (1-based inclusive max). */
+ uint16 maxHotspots() const { return 0x10; }
+ /** Per-object resource offset table entries. */
+ uint maxObjectResources() const { return 32; }
+ /** Anim slot used for the current orientation (DOS overload-direction rule). */
+ uint16 resolveAnimSlotIndex(const GameObject *obj) const;
+
+ /** Script stream: literals carry an extra high word after the value word. */
+ bool scriptValuesHaveHighWord() const { return false; }
+ /** Script stream: variable index is followed by a padding word. */
+ bool scriptVarIndexHasPaddingWord() const { return false; }
+ /** Script coordinates â screen/runtime coordinates (identity on DOS). */
+ int16 scaleScriptCoord(int16 coord) const { return coord; }
+
+ /** Dialogue / text-box chrome (DOS l0037_B368 / B462). */
+ int dialogPadW() const { return 0x12; }
+ int dialogPadH() const { return 0x10; }
+ int dialogTextInset() const { return 0x09; }
+ int dialogLineGap() const { return 2; }
+ int portraitBorderPad() const { return 0x0D; }
+ int portraitContentInset() const { return 7; }
+ int portraitTextGap() const { return 0x12; }
+
+ /** Depth-map compare Y for sprite occlusion (full Y on DOS). */
+ uint8 depthThresholdForY(int16 charY) const { return (uint8)charY; }
+
+ /** Resource bootstrap (MCS fonts/cursors/shading + first scene). */
+ void loadBootstrapResources() { readResourceFile(); }
+ /**
+ * Load scene background, maps, pathfinding, and related scene tables.
+ * Called from changeScene; platforms can replace this body later.
+ */
+ bool loadSceneGraphics(uint32 sceneIndex);
+
/**
* Returns the game Id
*/
diff --git a/engines/macs2/saveload.cpp b/engines/macs2/saveload.cpp
index cb75087318e..3dd1ea0ae4c 100644
--- a/engines/macs2/saveload.cpp
+++ b/engines/macs2/saveload.cpp
@@ -704,15 +704,16 @@ Common::Error Macs2Engine::syncGame(Common::Serializer &s) {
// Script resource table [+0x18D]: 0x80 bytes (128 bytes = 32 dword offsets)
// Stored in GameObject::_resourceOffsets, loaded from file during readResourceFile.
- byte scriptResourceTable[128] = {0};
+ const uint maxObjRes = maxObjectResources();
+ byte scriptResourceTable[128] = {0}; // DOS save table is always 0x80 bytes
if (s.isSaving()) {
- for (int r = 0; r < 32; r++) {
+ for (uint r = 0; r < maxObjRes; r++) {
WRITE_LE_UINT32(&scriptResourceTable[r * 4], obj->_resourceOffsets[r]);
}
}
s.syncBytes(scriptResourceTable, 128);
if (s.isLoading()) {
- for (int r = 0; r < 32; r++) {
+ for (uint r = 0; r < maxObjRes; r++) {
obj->_resourceOffsets[r] = READ_LE_UINT32(&scriptResourceTable[r * 4]);
}
}
@@ -729,9 +730,11 @@ Common::Error Macs2Engine::syncGame(Common::Serializer &s) {
// +0x00: 2 bytes (frame cursor X), +0x02: 2 bytes (frame cursor Y),
// +0x0C: 2 bytes (source key), +0x0E: 2 bytes (speed),
// +0x04: 2 bytes (data size), then data_size bytes of pixel data.
- for (int blobIdx = 0; blobIdx < 0x15; blobIdx++) {
- // Slot 0x15 (blobIdx 20) is the overload animation, stored separately
- bool isOverloadSlot = (blobIdx == 20);
+ const int animSlotCount = (int)maxAnimSlots();
+ const int overloadBlobIdx = (int)overloadAnimSlot() - 1;
+ for (int blobIdx = 0; blobIdx < animSlotCount; blobIdx++) {
+ // Overload slot is stored separately from normal orientation blobs.
+ bool isOverloadSlot = (blobIdx == overloadBlobIdx);
// Active flag (2 bytes, but only low byte matters)
uint16 blobActive = 0;
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 679cb038662..0ff40622f5a 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -186,10 +186,13 @@ void ScriptExecutor::scriptReadValuePair(uint16 &out1, uint16 &out2) {
byte type = readByte();
uint16 value = readUint16();
+ uint16 high = 0;
+ if (_engine->scriptValuesHaveHighWord())
+ high = readUint16();
if (type == 0x00) {
out1 = value;
- out2 = 0;
+ out2 = high;
return;
}
@@ -226,6 +229,15 @@ uint16 ScriptExecutor::scriptReadValue16() {
return out1;
}
+int16 ScriptExecutor::scriptReadCoord16() {
+ return _engine->scaleScriptCoord((int16)scriptReadValue16());
+}
+
+void ScriptExecutor::skipOptionalVarIndexPadding() {
+ if (_engine->scriptVarIndexHasPaddingWord())
+ (void)readUint16();
+}
+
void ScriptExecutor::scriptSaveVariableHelper(uint32 value) {
uint8 subOpcode = readByte();
if (subOpcode == 0x00 || subOpcode == 0xFF) {
@@ -234,6 +246,7 @@ void ScriptExecutor::scriptSaveVariableHelper(uint32 value) {
}
uint16 variableID = readUint16();
+ skipOptionalVarIndexPadding();
debugC(kDebugScript, "SCRIPT::saveVariable(subOpcode=0x%02x, variableID=%u, value=%u)", subOpcode, variableID, value);
setVariableValue(variableID, value);
}
@@ -316,8 +329,8 @@ bool ScriptExecutor::loadIndexedResource(Common::Array<uint8> &outData, uint8 re
return false;
}
// Binary reads from runtime+0x18D table (loaded during loadObjectData).
- // Table is 32 dword file offsets, indexed by (resourceIndex - 1).
- if (resourceIndex - 1 >= 32) {
+ // Table is maxObjectResources() dword file offsets, indexed by (resourceIndex - 1).
+ if ((uint)(resourceIndex - 1) >= _engine->maxObjectResources()) {
warning("Ignoring resource load for out-of-range index %u on object %u", resourceIndex, _executingScriptObjectId);
return false;
}
@@ -381,7 +394,7 @@ void ScriptExecutor::scriptPrintString(bool alignRight) {
int stringBoxX = x;
const int stringBoxY = y;
if (alignRight) {
- const int totalWidth = g_engine->measureStrings(strings) + 0x12;
+ const int totalWidth = g_engine->measureStrings(strings) + g_engine->dialogPadW();
stringBoxX -= totalWidth;
}
@@ -732,6 +745,7 @@ void Script::ScriptExecutor::scriptSetVar() {
// This writes to a script variable.
(void)readByte();
uint16 variableIndex = readUint16();
+ skipOptionalVarIndexPadding();
ScriptVariable var;
scriptReadValuePair(var.a, var.b);
debugC(kDebugScript, "SCRIPT::setVar(variableIndex=%u, value1=%u, value2=%u)", variableIndex, var.a, var.b);
@@ -742,6 +756,7 @@ void Script::ScriptExecutor::scriptSetVarOr() {
// Padding/type byte (same as opcode 0x01) - read and discarded
(void)readByte();
uint16 variableIndex = readUint16();
+ skipOptionalVarIndexPadding();
// We skip the left shift and just read the first value directly
uint16 throwaway;
uint16 value1;
@@ -884,8 +899,8 @@ void Script::ScriptExecutor::scriptMoveObject() {
// Handles render list updates for both source and destination scenes.
const uint32 objectID = scriptReadValue32() - 0x400;
const uint16 sceneID = scriptReadValue16();
- const int16 x = (int16)scriptReadValue16();
- const int16 y = (int16)scriptReadValue16();
+ const int16 x = scriptReadCoord16();
+ const int16 y = scriptReadCoord16();
debugC(kDebugScript, "SCRIPT::moveObject(objectID=%u, sceneID=%u, x=%d, y=%d)", objectID, sceneID, x, y);
if (objectID < 1 || objectID > 0x200) {
@@ -1287,8 +1302,8 @@ void Script::ScriptExecutor::scriptWalkToPosition() {
// Sets up runtime movement state. Does NOT block - walkAlongPath handles
// actual movement per-frame from the game tick.
const uint32 objectID = scriptReadValue32() - 0x400;
- const int16 x = (int16)scriptReadValue16();
- const int16 y = (int16)scriptReadValue16();
+ const int16 x = scriptReadCoord16();
+ const int16 y = scriptReadCoord16();
debugC(kDebugScript, "SCRIPT::walkToPosition(objectID=%u, x=%d, y=%d)", objectID, x, y);
clearScriptError();
@@ -1618,12 +1633,10 @@ void Script::ScriptExecutor::scriptSetupObject() {
return;
}
- if (slotID < 1 || slotID > 0x15) {
+ if (slotID < 1 || slotID > _engine->maxAnimSlots()) {
setScriptError(0x10);
return;
}
-
- // Binary: runtime+slot*0x10+0x33 (bSlotLoaded) must be set.
if (!object->isAnimSlotLoaded(slotID)) {
setScriptError(0x10);
return;
@@ -1668,7 +1681,7 @@ void Script::ScriptExecutor::scriptPlayAnimation() {
setScriptError(2);
return;
}
- if (slotID < 1 || slotID > 0x15) {
+ if (slotID < 1 || slotID > _engine->maxAnimSlots()) {
setScriptError(0x10);
return;
}
@@ -1952,9 +1965,9 @@ void Script::ScriptExecutor::scriptMoveToPosition() {
// Opcode 0x23 scriptMoveToPosition (1008:bafc): seeds runtime walk state directly;
// does not pathfind or use time-based lerp.
const int32 objectID = (int32)scriptReadValue32() - 0x400;
- const int16 x = (int16)scriptReadValue16();
- const int16 y = (int16)scriptReadValue16();
- const uint16 targetVerticalOffset = scriptReadValue16();
+ const int16 x = scriptReadCoord16();
+ const int16 y = scriptReadCoord16();
+ const uint16 targetVerticalOffset = (uint16)_engine->scaleScriptCoord((int16)scriptReadValue16());
debugC(kDebugScript, "SCRIPT::moveToPosition(objectID=%d, target=(%d,%d), targetVerticalOffset=%u)", objectID, x, y, targetVerticalOffset);
clearScriptError();
@@ -2148,7 +2161,7 @@ void Script::ScriptExecutor::scriptLoadObjectAnim() {
setScriptError(2);
return;
}
- if (slotID < 1 || slotID > 0x15) {
+ if (slotID < 1 || slotID > _engine->maxAnimSlots()) {
setScriptError(0x13);
return;
}
@@ -2277,7 +2290,7 @@ void Script::ScriptExecutor::scriptTestObjectAnimFrame() {
setScriptError(2);
return;
}
- if (slotID < 1 || slotID > 0x15) {
+ if (slotID < 1 || slotID > _engine->maxAnimSlots()) {
setScriptError(0x10);
return;
}
@@ -2374,7 +2387,8 @@ void Script::ScriptExecutor::scriptSetHotspotOverride() {
debugC(kDebugScript, "SCRIPT::setHotspotOverride(hotspot=%u, override=%u)", v1, v2);
clearScriptError();
- if (v1 < 1 || v1 > 0x10 || v2 < 1 || v2 > 0x10) {
+ const uint16 maxHotspot = _engine->maxHotspots();
+ if (v1 < 1 || v1 > maxHotspot || v2 < 1 || v2 > maxHotspot) {
setScriptError(0x1e);
return;
}
@@ -2629,14 +2643,13 @@ void Script::ScriptExecutor::scriptLoadPcmSound() {
return;
}
- if (_engine->hasCurrentSound() && _soundEnabled) {
- _engine->stopCurrentSound();
- }
+ // Opcode 0x3E only installs the buffer; playback is opcode 0x40.
_engine->setCurrentSoundData(soundData);
}
void Script::ScriptExecutor::scriptPlayPcmSound() {
// Binary (1000:0c7f): no error path; no-op when Sound Blaster disabled.
+ // Buffer was installed by opcode 0x3E; only start playback here.
if (_soundEnabled) {
if (!_engine->hasCurrentSound()) {
warning("Opcode 0x40: playPcmSound with no loaded sound data");
diff --git a/engines/macs2/scriptexecutor.h b/engines/macs2/scriptexecutor.h
index b4a9b0b7aa5..9f37e4ae2b1 100644
--- a/engines/macs2/scriptexecutor.h
+++ b/engines/macs2/scriptexecutor.h
@@ -265,6 +265,12 @@ private:
// Returns only the first of the two 16 bit values
uint16 scriptReadValue16();
+ /** Read a script value and convert to screen/runtime coordinates. */
+ int16 scriptReadCoord16();
+
+ /** Skip optional padding word after a script variable index when required. */
+ void skipOptionalVarIndexPadding();
+
// Saves the given value in a script variable
void scriptSaveVariableHelper(uint32 value);
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index f84a74a2409..f29395847f4 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -44,15 +44,6 @@ constexpr int kNumLoadedCursors = 33;
// Binary: local_4 = 1..7, each iteration reads *(local_4 * 2 + 0x26) as index into cursor array
const uint16 kLookupTable[8] = {9, 15, 14, 27, 29, 16, 17, 9}; // index 0 unused
-// drawAllCharacters (1008:90a2) local_14: animation slot for current orientation.
-uint16 resolveAnimSlotIndex(const GameObject *obj) {
- if ((int16)obj->_overloadAnimTriggerDirection < 0 ||
- obj->_overloadAnimTriggerDirection != obj->_orientation) {
- return obj->_orientation;
- }
- return 0x15;
-}
-
Common::String joinDebugStrings(const Common::StringArray &strings) {
Common::String result;
for (uint i = 0; i < strings.size(); ++i) {
@@ -583,11 +574,13 @@ void View1::drawCurrentSpeaker(Graphics::ManagedSurface &s) {
// Draw the border
const int portraitWidth = MAX<int>(leftPortrait ? leftPortrait->_width : 0, rightPortrait ? rightPortrait->_width : 0);
const int portraitHeight = MAX<int>(leftPortrait ? leftPortrait->_height : 0, rightPortrait ? rightPortrait->_height : 0);
- const Common::Point borderSize(portraitWidth + 0xD, portraitHeight + 0xD);
+ const int borderPad = g_engine->portraitBorderPad();
+ const int contentInset = g_engine->portraitContentInset();
+ const Common::Point borderSize(portraitWidth + borderPad, portraitHeight + borderPad);
drawBorder(currentSpeechActData.position, borderSize, s);
// Draw the portrait over the border
- Common::Point pos = currentSpeechActData.position + Common::Point(7, 7);
+ Common::Point pos = currentSpeechActData.position + Common::Point(contentInset, contentInset);
drawSprite(pos, frame->_width, frame->_height, frame->_data.data(), s, false);
delete frame;
delete leftPortrait;
@@ -739,8 +732,12 @@ void View1::showStringBox(const Common::StringArray &sa) {
// This calculation can be found at l0037_B368:
// int borderWidth = 10;
// int padding = 3;
- int totalWidth = g_engine->measureStrings(sa) + 0x12;
- int totalHeight = g_engine->measureStringsVertically(sa) + 0x10;
+ const int padW = g_engine->dialogPadW();
+ const int padH = g_engine->dialogPadH();
+ const int textInset = g_engine->dialogTextInset();
+ const int lineGap = g_engine->dialogLineGap();
+ int totalWidth = g_engine->measureStrings(sa) + padW;
+ int totalHeight = g_engine->measureStringsVertically(sa) + padH;
g_engine->_textLog.push_back(Common::String::format(
"Render text box: lines=%u pos=(%d,%d) size=(%d,%d) text=\"", sa.size(),
_stringBoxPosition.x, _stringBoxPosition.y, totalWidth, totalHeight) +
@@ -749,11 +746,11 @@ void View1::showStringBox(const Common::StringArray &sa) {
Graphics::ManagedSurface s = getSurface();
drawBorder(_stringBoxPosition, Common::Point(totalWidth, totalHeight), s);
// TODO range based
- int lineOffset = _stringBoxPosition.y + 0x9;
+ int lineOffset = _stringBoxPosition.y + textInset;
for (auto iter = sa.begin(); iter < sa.end(); iter++) {
- logRenderedText("TextBox", _stringBoxPosition.x + 0x9, lineOffset, *iter);
- renderString(_stringBoxPosition.x + 0x9, lineOffset, *iter);
- lineOffset += g_engine->maxGlyphHeight + 2;
+ logRenderedText("TextBox", _stringBoxPosition.x + textInset, lineOffset, *iter);
+ renderString(_stringBoxPosition.x + textInset, lineOffset, *iter);
+ lineOffset += g_engine->maxGlyphHeight + lineGap;
}
}
@@ -940,15 +937,18 @@ bool View1::handleDialogueChoiceClick(int clickY, int clickX) {
// Checks if click is within text box bounds (X+9..X+W-9, Y+9..Y+H-9).
// Iterates choice entries to find which line was clicked.
// Stores script index at scene+0x53B7 and clears scene+0x53B9.
- const int boxW = g_engine->measureStrings(_drawnStringBox) + 0x12;
- const int boxH = g_engine->measureStringsVertically(_drawnStringBox) + 0x10;
- if (clickX < _stringBoxPosition.x + 9 || clickY < _stringBoxPosition.y + 9 ||
- clickX > _stringBoxPosition.x + boxW - 9 || clickY > _stringBoxPosition.y + boxH - 9) {
+ const int padW = g_engine->dialogPadW();
+ const int padH = g_engine->dialogPadH();
+ const int textInset = g_engine->dialogTextInset();
+ const int boxW = g_engine->measureStrings(_drawnStringBox) + padW;
+ const int boxH = g_engine->measureStringsVertically(_drawnStringBox) + padH;
+ if (clickX < _stringBoxPosition.x + textInset || clickY < _stringBoxPosition.y + textInset ||
+ clickX > _stringBoxPosition.x + boxW - textInset || clickY > _stringBoxPosition.y + boxH - textInset) {
return false;
}
- int lineHeight = g_engine->maxGlyphHeight + 2;
- int firstLineY = _stringBoxPosition.y + 9;
+ int lineHeight = g_engine->maxGlyphHeight + g_engine->dialogLineGap();
+ int firstLineY = _stringBoxPosition.y + textInset;
int relY = clickY - firstLineY;
debug("handleDialogueChoiceClick: clickY=%d firstLineY=%d relY=%d lineHeight=%d clickedLine=%d",
clickY, firstLineY, relY, lineHeight, relY >= 0 ? relY / lineHeight : -1);
@@ -2571,7 +2571,7 @@ void View1::drawAllCharacters(Graphics::ManagedSurface *surface, bool fullUpdate
}
}
- const uint16 animSlot = resolveAnimSlotIndex(obj);
+ const uint16 animSlot = g_engine->resolveAnimSlotIndex(obj);
if (!obj->isAnimSlotLoaded(animSlot)) {
executor->setScriptError(10);
@@ -2647,7 +2647,7 @@ void View1::drawAllCharacters(Graphics::ManagedSurface *surface, bool fullUpdate
const int16 drawX = charX - (frameWidth >> 1) + frame._offsetX;
const int16 drawY = (charY - frameHeight) - walkabilityOffset + frame._offsetY;
- const uint8 depthThreshold = (uint8)charY;
+ const uint8 depthThreshold = g_engine->depthThresholdForY(charY);
const byte *pixelData = frame._data.data();
// drawAllCharacters @ 1008:9573-9754: drawAnimFrame / drawAnimFrameShaded / drawAnimFrameDepth
@@ -3087,8 +3087,11 @@ void View1::showSpeechAct(uint16 characterIndex, const Common::Array<Common::Str
currentSpeechActData.position = position;
currentSpeechActData.onRightSide = onRightSide;
- const int totalWidth = g_engine->measureStrings(strings) + 0x12;
- const int totalHeight = g_engine->measureStringsVertically(strings) + 0x10;
+ const int padW = g_engine->dialogPadW();
+ const int padH = g_engine->dialogPadH();
+ const int portraitGap = g_engine->portraitTextGap();
+ const int totalWidth = g_engine->measureStrings(strings) + padW;
+ const int totalHeight = g_engine->measureStringsVertically(strings) + padH;
int stringBoxX = position.x;
int stringBoxY = position.y;
Common::Point portraitBoxPosition = position;
@@ -3099,10 +3102,10 @@ void View1::showSpeechAct(uint16 characterIndex, const Common::Array<Common::Str
const int portraitWidth = MAX<int>(leftPortrait ? leftPortrait->_width : 0, rightPortrait ? rightPortrait->_width : 0);
if (portraitWidth > 0) {
if (onRightSide) {
- stringBoxX = position.x - portraitWidth - 0x12 - totalWidth;
+ stringBoxX = position.x - portraitWidth - portraitGap - totalWidth;
portraitBoxPosition.x = stringBoxX + totalWidth + 4;
} else {
- stringBoxX = position.x + portraitWidth + 0x12;
+ stringBoxX = position.x + portraitWidth + portraitGap;
}
}
delete leftPortrait;
@@ -3653,7 +3656,7 @@ uint8 Character::getMirroredAnimation(uint8 original) const {
}
bool Character::fillCurrentAnimationFrame(uint16 advanceMode, Macs2::AnimFrame &out) {
- const uint16 animSlot = resolveAnimSlotIndex(_gameObject);
+ const uint16 animSlot = g_engine->resolveAnimSlotIndex(_gameObject);
_shouldMirrorCurrentAnimation = false;
@@ -3822,7 +3825,7 @@ void Character::update() {
// = AnimSlot.wAnimSpeed (slot+0x0C). Stored in _blobWalkSpeeds.
uint16 animSpeed = 2; // default fallback
uint8 orient = _gameObject->_orientation;
- if (orient >= 1 && orient <= 0x15 && (uint)(orient - 1) < _gameObject->_blobWalkSpeeds.size()) {
+ if (orient >= 1 && orient <= g_engine->maxAnimSlots() && (uint)(orient - 1) < _gameObject->_blobWalkSpeeds.size()) {
animSpeed = _gameObject->_blobWalkSpeeds[orient - 1];
if (animSpeed == 0)
animSpeed = 2;
Commit: f86c012f352a0078f18b9904e2609a630a4d0fe5
https://github.com/scummvm/scummvm/commit/f86c012f352a0078f18b9904e2609a630a4d0fe5
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00
Commit Message:
MACS2: minor cleanup in renaming methods and removing loadSongFromSceneData
Changed paths:
engines/macs2/macs2.cpp
engines/macs2/macs2.h
engines/macs2/scriptexecutor.cpp
engines/macs2/view1.cpp
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 879eaebcc41..0a70026e2f2 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -1967,44 +1967,34 @@ bool Macs2Engine::loadObjectData(GameObject *obj) {
return true;
}
-void Macs2Engine::loadSongFromSceneData(uint8 dataIndex) {
- uint32 address = _sceneResourceOffsets[dataIndex - 1];
- _fileStream->seek(address);
- uint32 size = _fileStream->readUint32LE();
- Common::Array<uint8> data;
- data.resize(size);
- _fileStream->read(data.data(), size);
- _music->playSongData(data);
-}
-
void Macs2Engine::setCurrentSoundData(const Common::Array<uint8> &data) {
- stopCurrentSound();
+ stopSample();
_currentSoundData = data;
}
void Macs2Engine::clearCurrentSoundData() {
- stopCurrentSound();
+ stopSample();
_currentSoundData.clear();
}
-void Macs2Engine::playCurrentSound() {
+void Macs2Engine::playSample() {
if (_currentSoundData.empty())
return;
- stopCurrentSound();
+ stopSample();
MacsAudioStream *audioStream = new MacsAudioStream();
audioStream->_pos = 2; // Skip 2-byte header (original: size = stored_size - 2)
audioStream->_data = _currentSoundData;
g_system->getMixer()->playStream(Audio::Mixer::kSFXSoundType, &_currentSoundHandle, audioStream);
}
-void Macs2Engine::stopCurrentSound() {
+void Macs2Engine::stopSample() {
Audio::Mixer *mixer = g_system->getMixer();
if (mixer->isSoundHandleActive(_currentSoundHandle))
mixer->stopHandle(_currentSoundHandle);
}
-bool Macs2Engine::isCurrentSoundPlaying() const {
+bool Macs2Engine::isSamplePlaying() const {
return g_system->getMixer()->isSoundHandleActive(_currentSoundHandle);
}
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index 8066c0ff9b4..1f41d9ae5af 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -414,16 +414,15 @@ public:
// sortObjectsByDepth @ 1008:0d79 - inventory cursor reset + free object runtime blobs.
void sortObjectsByDepth(uint16 objectIndex);
- void loadSongFromSceneData(uint8 dataIndex);
Music *getMusic() const { return _music; }
// Returns the Music volume (0-63) scaled by the user's music_volume setting
uint16 scaledMusicVolume(uint16 gameAttenuation) const;
void setCurrentSoundData(const Common::Array<uint8> &data);
void clearCurrentSoundData();
- void playCurrentSound();
- void stopCurrentSound();
- bool hasCurrentSound() const { return !_currentSoundData.empty(); }
- bool isCurrentSoundPlaying() const;
+ bool hasCurrentSoundData() const { return !_currentSoundData.empty(); }
+ void playSample();
+ void stopSample();
+ bool isSamplePlaying() const;
// Offset 50D3h - This is used in 0037:10C4 to terminate the loop
uint16 _numHotspots;
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 0ff40622f5a..98acad5af50 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -2651,11 +2651,11 @@ void Script::ScriptExecutor::scriptPlayPcmSound() {
// Binary (1000:0c7f): no error path; no-op when Sound Blaster disabled.
// Buffer was installed by opcode 0x3E; only start playback here.
if (_soundEnabled) {
- if (!_engine->hasCurrentSound()) {
+ if (!_engine->hasCurrentSoundData()) {
warning("Opcode 0x40: playPcmSound with no loaded sound data");
return;
}
- _engine->playCurrentSound();
+ _engine->playSample();
}
}
@@ -2665,7 +2665,7 @@ bool Script::ScriptExecutor::scriptWaitForSound() {
endTimer();
endBuffering(_lastOpcodeTriggeredSkip);
debugC(kDebugScript, "SCRIPT::waitForSound start (soundPlaying=%d hasSound=%d)",
- g_engine->isCurrentSoundPlaying() ? 1 : 0, g_engine->hasCurrentSound() ? 1 : 0);
+ g_engine->isSamplePlaying() ? 1 : 0, g_engine->hasCurrentSoundData() ? 1 : 0);
debugLogActorWalkState("waitForSound start");
enterBlockingWaitCursor();
return true;
@@ -2677,7 +2677,7 @@ bool Script::ScriptExecutor::scriptWaitForSound() {
void Script::ScriptExecutor::scriptStopPcmSound() {
if (_soundEnabled) {
- _engine->stopCurrentSound();
+ _engine->stopSample();
}
}
@@ -2923,7 +2923,7 @@ bool Script::ScriptExecutor::scriptWaitForAdlib() {
void Script::ScriptExecutor::scriptFreePcmSound() {
debugC(kDebugScript, "SCRIPT::freePcmSound()");
if (_soundEnabled)
- _engine->stopCurrentSound();
+ _engine->stopSample();
_engine->clearCurrentSoundData();
}
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index f29395847f4..9153dcf4fd1 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -2438,7 +2438,7 @@ bool View1::tick() {
}
} else if (executor->_waitForPcmSound) {
drawSceneUpdate();
- if (!g_engine->isCurrentSoundPlaying()) {
+ if (!g_engine->isSamplePlaying()) {
debugC(kDebugScript, "waitForSound complete");
executor->debugLogActorWalkState("waitForSound complete");
executor->_waitForPcmSound = false;
Commit: ab78c9292a61f1ecc545df81f41930f6cbd6f3d7
https://github.com/scummvm/scummvm/commit/ab78c9292a61f1ecc545df81f41930f6cbd6f3d7
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00
Commit Message:
MACS2: added amiga demo detection
Changed paths:
engines/macs2/detection_tables.h
diff --git a/engines/macs2/detection_tables.h b/engines/macs2/detection_tables.h
index 5f35ddefe00..98884576a61 100644
--- a/engines/macs2/detection_tables.h
+++ b/engines/macs2/detection_tables.h
@@ -83,6 +83,16 @@ const ADGameDescription gameDescriptions[] = {
GF_TRANSLATED | ADGF_DEMO | ADGF_UNSTABLE,
GUIO3(GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_TTS, GAMEOPTION_ENHANCEMENTS)},
+ // Amiga demo - DataA (MXMF) + Mdir (MXDR). GMACS II / LINEL.
+ {"sis",
+ "Demo",
+ AD_ENTRY2s("DataA", "30ce5b1b30f7ef60f412c0475a25b0cb", 736670,
+ "Mdir", "269f4c31a50395e3ec6ae69b86e426bd", 598),
+ Common::DE_DEU,
+ Common::kPlatformAmiga,
+ ADGF_DEMO | ADGF_UNSTABLE,
+ GUIO3(GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_TTS, GAMEOPTION_ENHANCEMENTS)},
+
AD_TABLE_END_MARKER};
} // End of namespace Macs2
Commit: f33e897885eed8d3c0ea522dff9725c216c1ae00
https://github.com/scummvm/scummvm/commit/f33e897885eed8d3c0ea522dff9725c216c1ae00
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00
Commit Message:
MACS2: fill some of the platform specific magic values for the amiga
Changed paths:
engines/macs2/macs2.cpp
engines/macs2/macs2.h
engines/macs2/view1.cpp
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 0a70026e2f2..6dff774d495 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -415,6 +415,7 @@ Macs2Engine::~Macs2Engine() {
_music->deinit();
delete _music;
delete _fileStream;
+ _fileStream = nullptr;
delete _scriptExecutor;
for (uint i = 0; i < GameObjects::instance()._objects.size(); i++) {
delete GameObjects::instance()._objects[i];
@@ -1539,8 +1540,8 @@ int Macs2Engine::measureString(const Common::String &s) {
}
int Macs2Engine::measureStringsVertically(const Common::StringArray &sa) {
- // This is implemented around l0037_B318:
- return sa.size() * (maxGlyphHeight + 2);
+ // DOS l0037_B318: maxGlyphHeight + 2. Amiga uses absolute MXFF line pitch.
+ return (int)sa.size() * dialogLineHeight();
}
int Macs2Engine::measureStrings(const Common::StringArray &sa) {
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index 1f41d9ae5af..d6b598ae18f 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -368,7 +368,10 @@ public:
Common::Array<BackgroundAnimation> _backgroundAnimations;
Common::Array<BackgroundAnimationBlob> _backgroundAnimationsBlobs;
- Common::MemoryReadStream *_fileStream;
+ Common::MemoryReadStream *_fileStream = nullptr;
+
+ /** Amiga MXFF line pitch: measureTextWidth @ 00224420 uses (font[+8] - 1). */
+ uint16 amigaTextLinePitch = 0;
void setCursorMode(Script::MouseMode newMode);
void nextCursorMode();
@@ -504,6 +507,8 @@ public:
int computeStringIndex(Common::MemoryReadStream *stream, int targetOffset);
uint32 getFeatures() const;
+ Common::Platform getPlatform() const { return _gameDescription->platform; }
+ bool isAmiga() const { return getPlatform() == Common::kPlatformAmiga; }
bool isDemo() const { return getFeatures() & ADGF_DEMO; }
@@ -542,13 +547,23 @@ public:
int16 scaleScriptCoord(int16 coord) const { return coord; }
/** Dialogue / text-box chrome (DOS l0037_B368 / B462). */
- int dialogPadW() const { return 0x12; }
- int dialogPadH() const { return 0x10; }
- int dialogTextInset() const { return 0x09; }
+ int dialogPadW() const { return isAmiga() ? 0x08 : 0x12; }
+ int dialogPadH() const { return isAmiga() ? 0x08 : 0x10; }
+ int dialogTextInset() const { return isAmiga() ? 0x04 : 0x09; }
int dialogLineGap() const { return 2; }
- int portraitBorderPad() const { return 0x0D; }
- int portraitContentInset() const { return 7; }
- int portraitTextGap() const { return 0x12; }
+ int portraitBorderPad() const { return isAmiga() ? 2 : 0x0D; }
+ int portraitContentInset() const { return isAmiga() ? 1 : 7; }
+ int portraitTextGap() const { return isAmiga() ? 0x0A : 0x12; }
+ /**
+ * Per-line step for dialogue layout.
+ * DOS: maxGlyphHeight + dialogLineGap(). Amiga: absolute MXFF pitch
+ * (amigaTextLinePitch) or maxGlyphHeight when pitch is unset.
+ */
+ int dialogLineHeight() const {
+ if (isAmiga())
+ return amigaTextLinePitch ? (int)amigaTextLinePitch : (int)maxGlyphHeight;
+ return (int)maxGlyphHeight + dialogLineGap();
+ }
/** Depth-map compare Y for sprite occlusion (full Y on DOS). */
uint8 depthThresholdForY(int16 charY) const { return (uint8)charY; }
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 9153dcf4fd1..f928bb3e308 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -735,7 +735,7 @@ void View1::showStringBox(const Common::StringArray &sa) {
const int padW = g_engine->dialogPadW();
const int padH = g_engine->dialogPadH();
const int textInset = g_engine->dialogTextInset();
- const int lineGap = g_engine->dialogLineGap();
+ const int lineHeight = g_engine->dialogLineHeight();
int totalWidth = g_engine->measureStrings(sa) + padW;
int totalHeight = g_engine->measureStringsVertically(sa) + padH;
g_engine->_textLog.push_back(Common::String::format(
@@ -750,7 +750,7 @@ void View1::showStringBox(const Common::StringArray &sa) {
for (auto iter = sa.begin(); iter < sa.end(); iter++) {
logRenderedText("TextBox", _stringBoxPosition.x + textInset, lineOffset, *iter);
renderString(_stringBoxPosition.x + textInset, lineOffset, *iter);
- lineOffset += g_engine->maxGlyphHeight + lineGap;
+ lineOffset += lineHeight;
}
}
@@ -947,7 +947,7 @@ bool View1::handleDialogueChoiceClick(int clickY, int clickX) {
return false;
}
- int lineHeight = g_engine->maxGlyphHeight + g_engine->dialogLineGap();
+ int lineHeight = g_engine->dialogLineHeight();
int firstLineY = _stringBoxPosition.y + textInset;
int relY = clickY - firstLineY;
debug("handleDialogueChoiceClick: clickY=%d firstLineY=%d relY=%d lineHeight=%d clickedLine=%d",
@@ -2146,8 +2146,8 @@ void View1::draw() {
if (_isShowingTextBox || _isShowingDialoguePanel) {
showStringBox(_drawnStringBox);
if (_isDialogueChoiceInputActive && g_engine->enhancementEnabled(kEnhUIUX)) {
- int lineHeight = g_engine->maxGlyphHeight + 2;
- int firstLineY = _stringBoxPosition.y + 9;
+ int lineHeight = g_engine->dialogLineHeight();
+ int firstLineY = _stringBoxPosition.y + g_engine->dialogTextInset();
int relY = mousePos.y - firstLineY;
if (relY >= 0) {
int hoveredLine = relY / lineHeight;
Commit: b6556791e7432e1b2c79f37a940c3023eb9dd78f
https://github.com/scummvm/scummvm/commit/b6556791e7432e1b2c79f37a940c3023eb9dd78f
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00
Commit Message:
MACS2: amiga string decoding is different
Changed paths:
engines/macs2/detection_tables.h
engines/macs2/macs2.cpp
diff --git a/engines/macs2/detection_tables.h b/engines/macs2/detection_tables.h
index 98884576a61..89a02b9d0c8 100644
--- a/engines/macs2/detection_tables.h
+++ b/engines/macs2/detection_tables.h
@@ -93,6 +93,17 @@ const ADGameDescription gameDescriptions[] = {
ADGF_DEMO | ADGF_UNSTABLE,
GUIO3(GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_TTS, GAMEOPTION_ENHANCEMENTS)},
+ // Amiga demo - English fan translation
+ {"sis",
+ "Demo/w/translation",
+ AD_ENTRY3s("DataA", "30ce5b1b30f7ef60f412c0475a25b0cb", 736670,
+ "Mdir", "269f4c31a50395e3ec6ae69b86e426bd", 598,
+ "macs2_translation.dat", nullptr, AD_NO_SIZE),
+ Common::EN_ANY,
+ Common::kPlatformAmiga,
+ GF_TRANSLATED | ADGF_DEMO | ADGF_UNSTABLE,
+ GUIO3(GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_TTS, GAMEOPTION_ENHANCEMENTS)},
+
AD_TABLE_END_MARKER};
} // End of namespace Macs2
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 6dff774d495..bb838051a8c 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -1556,7 +1556,8 @@ int Macs2Engine::computeStringIndex(Common::MemoryReadStream *stream, int target
stream->seek(0);
int index = 0;
while (stream->pos() < targetOffset && !stream->eos()) {
- uint16 len = stream->readUint16LE();
+ // DOS: u16LE + XOR ciphertext. Amiga: u16BE + plaintext.
+ const uint16 len = isAmiga() ? stream->readUint16BE() : stream->readUint16LE();
if (len == 0)
break;
stream->skip(len);
@@ -1681,22 +1682,28 @@ Common::StringArray Macs2Engine::decodeStrings(Common::MemoryReadStream *stream,
Common::StringArray result(numStrings);
stream->seek(offset);
- byte x;
- byte y;
- byte r;
-
- for (int i = 0; i < numStrings; i++) {
- Common::String currentLine;
- uint16 length = stream->readUint16LE();
- byte currentByte;
- for (int index = 1; index < length + 1; index++) {
- currentByte = stream->readByte();
- x = (byte)(index * index * 0x0c);
- y = (byte)(currentByte ^ index);
- r = (byte)(x ^ y);
- currentLine += (char)r;
- }
- result[i] = currentLine;
+ if (isAmiga()) {
+ // Amiga strings: u16BE length + plaintext (Latin-1), no XOR cipher.
+ for (int i = 0; i < numStrings; i++) {
+ Common::String currentLine;
+ const uint16 length = stream->readUint16BE();
+ for (uint16 index = 0; index < length; index++)
+ currentLine += (char)stream->readByte();
+ result[i] = currentLine;
+ }
+ } else {
+ for (int i = 0; i < numStrings; i++) {
+ Common::String currentLine;
+ uint16 length = stream->readUint16LE();
+ for (int index = 1; index < length + 1; index++) {
+ const byte currentByte = stream->readByte();
+ const byte x = (byte)(index * index * 0x0c);
+ const byte y = (byte)(currentByte ^ index);
+ const byte r = (byte)(x ^ y);
+ currentLine += (char)r;
+ }
+ result[i] = currentLine;
+ }
}
// Apply translation if available
Commit: 14601bf9948d8ad0aa018368e0245dddd0c3f1d3
https://github.com/scummvm/scummvm/commit/14601bf9948d8ad0aa018368e0245dddd0c3f1d3
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00
Commit Message:
MACS2: resize of the top of the function
Changed paths:
engines/macs2/macs2.cpp
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index bb838051a8c..54b23acfa68 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -293,6 +293,9 @@ void Macs2Engine::readResourceFile() {
}
void Macs2Engine::readExecutable() {
+ inventoryIconIndices.resize(6);
+ containerInventoryIconIndices.resize(6);
+
Common::ScopedPtr<Common::MemoryReadStream> exeFileStream;
{
// Extra scope in order to make sure no code tries to read from the file directly.
@@ -310,11 +313,9 @@ void Macs2Engine::readExecutable() {
_music->readDataFromExecutable(exeFileStream.get());
exeFileStream->seek(0x0001B610, SEEK_SET);
- inventoryIconIndices.resize(6);
exeFileStream->read(inventoryIconIndices.data(), 12);
exeFileStream->seek(0x0001B61C, SEEK_SET);
- containerInventoryIconIndices.resize(6);
exeFileStream->read(containerInventoryIconIndices.data(), 12);
}
Commit: 2764981a74e1c9f8bc601d5d357331e091a48fdd
https://github.com/scummvm/scummvm/commit/2764981a74e1c9f8bc601d5d357331e091a48fdd
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00
Commit Message:
MACS2: don't read data from exe for amiga
Changed paths:
engines/macs2/macs2.cpp
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 54b23acfa68..4e5630e5790 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -296,6 +296,14 @@ void Macs2Engine::readExecutable() {
inventoryIconIndices.resize(6);
containerInventoryIconIndices.resize(6);
+ if (isAmiga()) {
+ for (uint i = 0; i < 6; i++) {
+ inventoryIconIndices[i] = (uint16)(i + 1);
+ containerInventoryIconIndices[i] = (uint16)(i + 1);
+ }
+ return;
+ }
+
Common::ScopedPtr<Common::MemoryReadStream> exeFileStream;
{
// Extra scope in order to make sure no code tries to read from the file directly.
Commit: 7d36e8edeb3cb6c789e7ea040b6746f21a897765
https://github.com/scummvm/scummvm/commit/7d36e8edeb3cb6c789e7ea040b6746f21a897765
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00
Commit Message:
MACS2: music is (or will be) a facade not just for adlib
Changed paths:
engines/macs2/macs2.cpp
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 4e5630e5790..f0edc2e33a8 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -2050,11 +2050,8 @@ Common::Error Macs2Engine::run() {
CursorMan.showMouse(false);
- // Initialize Adlib
_music->init();
syncSoundSettings();
-
- // Set the engine's debugger console
setDebugger(new GUI::Debugger());
#ifdef USE_IMGUI
Commit: 0484fcbbf8ee0e548d1a5e2ee284a2b4d23da034
https://github.com/scummvm/scummvm/commit/0484fcbbf8ee0e548d1a5e2ee284a2b4d23da034
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00
Commit Message:
MACS2: leave a comment for my future me regarding full version and demo version offsets
Changed paths:
engines/macs2/macs2.cpp
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index f0edc2e33a8..fbbc5feb388 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -318,6 +318,10 @@ void Macs2Engine::readExecutable() {
exeFileStream.reset(new Common::MemoryReadStream(fileData, size, DisposeAfterUse::YES));
}
+ // Full MCSEXEC.EXE and demo MCSEXEC.EXE are different binaries (different MD5, ~12k differing bytes),
+ // but the whole Data5 segment is identical (1020:0000...1020:3787)
+ // TODO: if there are ever other games using different versions of MCSEXEC.EXE, we should check the checksum here
+
_music->readDataFromExecutable(exeFileStream.get());
exeFileStream->seek(0x0001B610, SEEK_SET);
Commit: 1a6023f75481cdd09e2c008a11ed36dd63b58227
https://github.com/scummvm/scummvm/commit/1a6023f75481cdd09e2c008a11ed36dd63b58227
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00
Commit Message:
MACS2: opcode table
to easier install different jump tables for different platforms or games
Changed paths:
engines/macs2/debugtools.cpp
engines/macs2/scriptexecutor.cpp
engines/macs2/scriptexecutor.h
diff --git a/engines/macs2/debugtools.cpp b/engines/macs2/debugtools.cpp
index b9ea027eb96..50019205a44 100644
--- a/engines/macs2/debugtools.cpp
+++ b/engines/macs2/debugtools.cpp
@@ -87,166 +87,9 @@ static const char *getSpecialName(uint16 val) {
}
static const char *getOpcodeName(uint8 opcode) {
- switch (opcode) {
- case 0x01:
- return "setVar";
- case 0x02:
- return "setVarOr";
- case 0x03:
- return "ifFalse";
- case 0x04:
- return "ifTrue";
- case 0x05:
- return "compare";
- case 0x06:
- return "ifInteraction";
- case 0x07:
- return "endIf";
- case 0x08:
- return "else";
- case 0x09:
- return "nop09";
- case 0x0A:
- return "printStringLeft";
- case 0x0B:
- return "moveObject";
- case 0x0C:
- return "changeScene";
- case 0x0D:
- return "showDialogue";
- case 0x0E:
- return "changeAnimation";
- case 0x0F:
- return "frameWait";
- case 0x10:
- return "walkToPosition";
- case 0x11:
- return "waitForWalk";
- case 0x12:
- return "setPathfinding";
- case 0x13:
- return "skipUntil14";
- case 0x14:
- return "skipWord";
- case 0x15:
- return "clearDialogueChoices";
- case 0x16:
- return "addDialogueChoice";
- case 0x17:
- return "showDialogueChoice";
- case 0x18:
- return "dismissPanel";
- case 0x19:
- return "walkToAndPickup";
- case 0x1A:
- return "setPickupFrames";
- case 0x1B:
- return "setupObject";
- case 0x1C:
- return "setSkippable";
- case 0x1D:
- return "clearSkippable";
- case 0x1E:
- return "playAnimation";
- case 0x1F:
- return "pathWalkable = testPathfinding";
- case 0x20:
- return "setYOffset";
- case 0x21:
- return "setMotion";
- case 0x22:
- return "setOrientation";
- case 0x23:
- return "moveToPosition";
- case 0x24:
- return "addValues";
- case 0x25:
- return "subValues";
- case 0x26:
- return "loadSpecialAnim";
- case 0x27:
- return "setDirection";
- case 0x28:
- return "stopAnimation";
- case 0x29:
- return "openInventory";
- case 0x2A:
- return "loadObjectAnim";
- case 0x2B:
- return "checkObjectData";
- case 0x2C:
- return "invCheck = checkInventory";
- case 0x2D:
- return "setSnapToTarget";
- case 0x2E:
- return "animRangeTest = testSceneAnimFrame";
- case 0x2F:
- return "animRangeTest = testObjectAnimFrame";
- case 0x30:
- return "printStringRight";
- case 0x31:
- return "setPaletteDarkness";
- case 0x32:
- return "setObjectShading";
- case 0x33:
- return "setObjectScaling";
- case 0x34:
- return "setHotspotOverride";
- case 0x35:
- return "setObjectBounds";
- case 0x36:
- return "dismissAllPanels";
- case 0x37:
- return "resetToSceneScript";
- case 0x38:
- return "loadOverlayFont";
- case 0x39:
- return "endOverlayText";
- case 0x3A:
- return "addOverlayTextEntry";
- case 0x3B:
- return "clearOverlayText";
- case 0x3C:
- return "fadeToBlack";
- case 0x3D:
- return "fadeFromBlack";
- case 0x3E:
- return "loadPcmSound";
- case 0x3F:
- return "freePcmSound";
- case 0x40:
- return "playPcmSound";
- case 0x41:
- return "waitForSound";
- case 0x42:
- return "stopPcmSound";
- case 0x43:
- return "loadMusicSlot";
- case 0x44:
- return "playMusicSlot";
- case 0x45:
- return "stopMusicSlot";
- case 0x46:
- return "freeMusicSlot";
- case 0x47:
- return "waitForMusic";
- case 0x48:
- return "getObjectX";
- case 0x49:
- return "getObjectY";
- case 0x4A:
- return "getObjectField8";
- case 0x4B:
- return "getObjectOrientation";
- case 0x4C:
- return "clearActorInventory";
- case 0x4D:
- return "setPathfindingRemap";
- case 0x4E:
- return "waitForAdlib";
- default:
- return "???";
- }
+ if (g_engine && g_engine->_scriptExecutor)
+ return g_engine->_scriptExecutor->opcodeName(opcode);
+ return "?";
}
static const char *getCompareOpName(uint8 sub) {
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 98acad5af50..ae2fd77d217 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -58,6 +58,8 @@ ScriptExecutor::ScriptExecutor() {
_variables[i].a = 0;
_variables[i].b = 0;
}
+
+ setOpcodeTable(ScriptExecutor::kDosOpcodeTable, ScriptExecutor::kDosOpcodeTableSize);
}
ScriptExecutor::~ScriptExecutor() {
@@ -67,10 +69,8 @@ ScriptExecutor::~ScriptExecutor() {
}
Common::String ScriptExecutor::identifyScriptOpcode(uint8 opcode, uint8 opcode2) {
- if (opcode == 0x5)
- return Common::String::format("(%.2x)", opcode);
-
- return Common::String::format("(%.2x %.2x)", opcode, opcode2);
+ (void)opcode2;
+ return Common::String::format("%s (%.2x)", opcodeName(opcode), opcode);
}
Common::String ScriptExecutor::identifyHelperOpcode(uint8 opcode, uint16 value) {
@@ -2927,6 +2927,516 @@ void Script::ScriptExecutor::scriptFreePcmSound() {
_engine->clearCurrentSoundData();
}
+void ScriptExecutor::setOpcodeTable(const OpcodeEntry *table, uint size) {
+ _opcodeTable = table;
+ _opcodeTableSize = size;
+}
+
+const char *ScriptExecutor::opcodeName(uint8 opcode) const {
+ if (_opcodeTable == nullptr || opcode >= _opcodeTableSize)
+ return "?";
+ const char *name = _opcodeTable[opcode].name;
+ return name ? name : "?";
+}
+
+OpcodeResult ScriptExecutor::opSetVar() {
+ scriptSetVar();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetVarOr() {
+ scriptSetVarOr();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opIfFalse() {
+ scriptIfFalse();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opIfTrue() {
+ scriptIfTrue();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opCompare() {
+ if (!scriptCompare())
+ return OpcodeResult::FinishScript;
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opIfInteraction() {
+ scriptIfInteraction();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opEndIf() {
+ scriptEndIf();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opElse() {
+ scriptElse();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opNop09() {
+ scriptNop09();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opPrintStringLeft() {
+ scriptPrintStringLeft();
+ return OpcodeResult::WaitForCallback;
+}
+
+OpcodeResult ScriptExecutor::opMoveObject() {
+ scriptMoveObject();
+ if (hasScriptError())
+ return OpcodeResult::FinishScript;
+ // Binary: after scriptMoveObject(), exits if position >= end.
+ if (_stream->pos() >= _stream->size())
+ return OpcodeResult::FinishScript;
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opChangeScene() {
+ const ExecutionResult result = scriptChangeScene();
+ if (result == ExecutionResult::WaitingForCallback)
+ return OpcodeResult::WaitForCallback;
+ return OpcodeResult::ReturnFinished;
+}
+
+OpcodeResult ScriptExecutor::opShowDialogue() {
+ const ExecutionResult dialogueResult = scriptShowDialogue();
+ if (dialogueResult == ExecutionResult::WaitingForCallback)
+ return OpcodeResult::WaitForCallback;
+ return OpcodeResult::FinishScript;
+}
+
+OpcodeResult ScriptExecutor::opChangeAnimation() {
+ scriptChangeAnimation();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opFrameWait() {
+ scriptFrameWait();
+ return OpcodeResult::WaitForCallback;
+}
+
+OpcodeResult ScriptExecutor::opWalkToPosition() {
+ scriptWalkToPosition();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opWaitForWalk() {
+ const ExecutionResult result = scriptWaitForWalk();
+ if (result == ExecutionResult::WaitingForCallback)
+ return OpcodeResult::WaitForCallback;
+ return OpcodeResult::ReturnFinished;
+}
+
+OpcodeResult ScriptExecutor::opSetPathfinding() {
+ scriptSetPathfinding();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSkipUntil14() {
+ scriptSkipUntil14();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSkipWord() {
+ scriptSkipWord();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opClearDialogueChoices() {
+ scriptClearDialogueChoices();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opAddDialogueChoice() {
+ scriptAddDialogueChoice();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opShowDialogueChoice() {
+ const ExecutionResult result = scriptShowDialogueChoice();
+ if (result == ExecutionResult::WaitingForCallback)
+ return OpcodeResult::WaitForCallback;
+ return OpcodeResult::ReturnFinished;
+}
+
+OpcodeResult ScriptExecutor::opDismissPanel() {
+ const ExecutionResult result = scriptDismissPanel();
+ if (result == ExecutionResult::WaitingForCallback)
+ return OpcodeResult::WaitForCallback;
+ return OpcodeResult::ReturnFinished;
+}
+
+OpcodeResult ScriptExecutor::opWalkToAndPickup() {
+ scriptWalkToAndPickup();
+ return OpcodeResult::FinishScript;
+}
+
+OpcodeResult ScriptExecutor::opSetPickupFrames() {
+ scriptSetPickupFrames();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetupObject() {
+ scriptSetupObject();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetSkippable() {
+ scriptSetSkippable();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opClearSkippable() {
+ scriptClearSkippable();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opPlayAnimation() {
+ scriptPlayAnimation();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opTestPathfinding() {
+ scriptTestPathfinding();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetYOffset() {
+ scriptSetYOffset();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetMotion() {
+ scriptSetMotion();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetOrientation() {
+ scriptSetOrientation();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opMoveToPosition() {
+ scriptMoveToPosition();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opAddValues() {
+ scriptAddValues();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSubValues() {
+ scriptSubValues();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opLoadSpecialAnim() {
+ scriptLoadSpecialAnim();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetDirection() {
+ scriptSetDirection();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opStopAnimation() {
+ scriptStopAnimation();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opOpenInventory() {
+ // Binary (1008:e0f9): always terminates script + sets executingObjectId sentinel.
+ scriptOpenInventory();
+ _stream->seek(_stream->size(), SEEK_SET);
+ _executingObjectIndex = 0x201;
+ _executingScriptObjectId = 0x201;
+ return OpcodeResult::FinishScript;
+}
+
+OpcodeResult ScriptExecutor::opLoadObjectAnim() {
+ scriptLoadObjectAnim();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opCheckObjectData() {
+ scriptCheckObjectData();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opCheckInventory() {
+ scriptCheckInventory();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetSnapToTarget() {
+ scriptSetSnapToTarget();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opTestSceneAnimFrame() {
+ scriptTestSceneAnimFrame();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opTestObjectAnimFrame() {
+ scriptTestObjectAnimFrame();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opPrintStringRight() {
+ scriptPrintStringRight();
+ return OpcodeResult::WaitForCallback;
+}
+
+OpcodeResult ScriptExecutor::opSetPaletteDarkness() {
+ scriptSetPaletteDarkness();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetObjectShading() {
+ scriptSetObjectShading();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetObjectScaling() {
+ scriptSetObjectScaling();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetHotspotOverride() {
+ scriptSetHotspotOverride();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetObjectBounds() {
+ scriptSetObjectBounds();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opDismissAllPanels() {
+ scriptDismissAllPanels();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opResetToSceneScript() {
+ scriptResetToSceneScript();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opLoadOverlayFont() {
+ scriptLoadOverlayFont();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opEndOverlayText() {
+ scriptEndOverlayText();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opAddOverlayTextEntry() {
+ scriptAddOverlayTextEntry();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opClearOverlayText() {
+ scriptClearOverlayText();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opFadeToBlack() {
+ scriptFadeToBlack();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opFadeFromBlack() {
+ scriptFadeFromBlack();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opLoadPcmSound() {
+ scriptLoadPcmSound();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opFreePcmSound() {
+ scriptFreePcmSound();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opPlayPcmSound() {
+ scriptPlayPcmSound();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opWaitForSound() {
+ if (scriptWaitForSound())
+ return OpcodeResult::WaitForCallback;
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opStopPcmSound() {
+ scriptStopPcmSound();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opLoadMusicSlot() {
+ scriptLoadMusicSlot();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opPlayMusicSlot() {
+ scriptPlayMusicSlot();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opStopMusicSlot() {
+ scriptStopMusicSlot();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opFreeMusicSlot() {
+ scriptFreeMusicSlot();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opWaitForMusic() {
+ if (scriptWaitForMusic())
+ return OpcodeResult::WaitForCallback;
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opGetObjectX() {
+ scriptGetObjectX();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opGetObjectY() {
+ scriptGetObjectY();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opGetObjectField8() {
+ scriptGetObjectField8();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opGetObjectOrientation() {
+ scriptGetObjectOrientation();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opClearActorInventory() {
+ scriptClearActorInventory();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opSetPathfindingRemap() {
+ scriptSetPathfindingRemap();
+ return OpcodeResult::Continue;
+}
+
+OpcodeResult ScriptExecutor::opWaitForAdlib() {
+ if (scriptWaitForAdlib())
+ return OpcodeResult::WaitForCallback;
+ return OpcodeResult::Continue;
+}
+
+// DOS script opcode table (indexed by primary opcode byte).
+// Platforms can install an alternate table via setOpcodeTable().
+const ScriptExecutor::OpcodeEntry ScriptExecutor::kDosOpcodeTable[] = {
+ {nullptr, nullptr},
+ {"setVar", &ScriptExecutor::opSetVar},
+ {"setVarOr", &ScriptExecutor::opSetVarOr},
+ {"ifFalse", &ScriptExecutor::opIfFalse},
+ {"ifTrue", &ScriptExecutor::opIfTrue},
+ {"compare", &ScriptExecutor::opCompare},
+ {"ifInteraction", &ScriptExecutor::opIfInteraction},
+ {"endIf", &ScriptExecutor::opEndIf},
+ {"else", &ScriptExecutor::opElse},
+ {"nop09", &ScriptExecutor::opNop09},
+ {"printStringLeft", &ScriptExecutor::opPrintStringLeft},
+ {"moveObject", &ScriptExecutor::opMoveObject},
+ {"changeScene", &ScriptExecutor::opChangeScene},
+ {"showDialogue", &ScriptExecutor::opShowDialogue},
+ {"changeAnimation", &ScriptExecutor::opChangeAnimation},
+ {"frameWait", &ScriptExecutor::opFrameWait},
+ {"walkToPosition", &ScriptExecutor::opWalkToPosition},
+ {"waitForWalk", &ScriptExecutor::opWaitForWalk},
+ {"setPathfinding", &ScriptExecutor::opSetPathfinding},
+ {"skipUntil14", &ScriptExecutor::opSkipUntil14},
+ {"skipWord", &ScriptExecutor::opSkipWord},
+ {"clearDialogueChoices", &ScriptExecutor::opClearDialogueChoices},
+ {"addDialogueChoice", &ScriptExecutor::opAddDialogueChoice},
+ {"showDialogueChoice", &ScriptExecutor::opShowDialogueChoice},
+ {"dismissPanel", &ScriptExecutor::opDismissPanel},
+ {"walkToAndPickup", &ScriptExecutor::opWalkToAndPickup},
+ {"setPickupFrames", &ScriptExecutor::opSetPickupFrames},
+ {"setupObject", &ScriptExecutor::opSetupObject},
+ {"setSkippable", &ScriptExecutor::opSetSkippable},
+ {"clearSkippable", &ScriptExecutor::opClearSkippable},
+ {"playAnimation", &ScriptExecutor::opPlayAnimation},
+ {"testPathfinding", &ScriptExecutor::opTestPathfinding},
+ {"setYOffset", &ScriptExecutor::opSetYOffset},
+ {"setMotion", &ScriptExecutor::opSetMotion},
+ {"setOrientation", &ScriptExecutor::opSetOrientation},
+ {"moveToPosition", &ScriptExecutor::opMoveToPosition},
+ {"addValues", &ScriptExecutor::opAddValues},
+ {"subValues", &ScriptExecutor::opSubValues},
+ {"loadSpecialAnim", &ScriptExecutor::opLoadSpecialAnim},
+ {"setDirection", &ScriptExecutor::opSetDirection},
+ {"stopAnimation", &ScriptExecutor::opStopAnimation},
+ {"openInventory", &ScriptExecutor::opOpenInventory},
+ {"loadObjectAnim", &ScriptExecutor::opLoadObjectAnim},
+ {"checkObjectData", &ScriptExecutor::opCheckObjectData},
+ {"checkInventory", &ScriptExecutor::opCheckInventory},
+ {"setSnapToTarget", &ScriptExecutor::opSetSnapToTarget},
+ {"testSceneAnimFrame", &ScriptExecutor::opTestSceneAnimFrame},
+ {"testObjectAnimFrame", &ScriptExecutor::opTestObjectAnimFrame},
+ {"printStringRight", &ScriptExecutor::opPrintStringRight},
+ {"setPaletteDarkness", &ScriptExecutor::opSetPaletteDarkness},
+ {"setObjectShading", &ScriptExecutor::opSetObjectShading},
+ {"setObjectScaling", &ScriptExecutor::opSetObjectScaling},
+ {"setHotspotOverride", &ScriptExecutor::opSetHotspotOverride},
+ {"setObjectBounds", &ScriptExecutor::opSetObjectBounds},
+ {"dismissAllPanels", &ScriptExecutor::opDismissAllPanels},
+ {"resetToSceneScript", &ScriptExecutor::opResetToSceneScript},
+ {"loadOverlayFont", &ScriptExecutor::opLoadOverlayFont},
+ {"endOverlayText", &ScriptExecutor::opEndOverlayText},
+ {"addOverlayTextEntry", &ScriptExecutor::opAddOverlayTextEntry},
+ {"clearOverlayText", &ScriptExecutor::opClearOverlayText},
+ {"fadeToBlack", &ScriptExecutor::opFadeToBlack},
+ {"fadeFromBlack", &ScriptExecutor::opFadeFromBlack},
+ {"loadPcmSound", &ScriptExecutor::opLoadPcmSound},
+ {"freePcmSound", &ScriptExecutor::opFreePcmSound},
+ {"playPcmSound", &ScriptExecutor::opPlayPcmSound},
+ {"waitForSound", &ScriptExecutor::opWaitForSound},
+ {"stopPcmSound", &ScriptExecutor::opStopPcmSound},
+ {"loadMusicSlot", &ScriptExecutor::opLoadMusicSlot},
+ {"playMusicSlot", &ScriptExecutor::opPlayMusicSlot},
+ {"stopMusicSlot", &ScriptExecutor::opStopMusicSlot},
+ {"freeMusicSlot", &ScriptExecutor::opFreeMusicSlot},
+ {"waitForMusic", &ScriptExecutor::opWaitForMusic},
+ {"getObjectX", &ScriptExecutor::opGetObjectX},
+ {"getObjectY", &ScriptExecutor::opGetObjectY},
+ {"getObjectField8", &ScriptExecutor::opGetObjectField8},
+ {"getObjectOrientation", &ScriptExecutor::opGetObjectOrientation},
+ {"clearActorInventory", &ScriptExecutor::opClearActorInventory},
+ {"setPathfindingRemap", &ScriptExecutor::opSetPathfindingRemap},
+ {"waitForAdlib", &ScriptExecutor::opWaitForAdlib}
+};
+const uint ScriptExecutor::kDosOpcodeTableSize = ARRAYSIZE(ScriptExecutor::kDosOpcodeTable);
+
ExecutionResult Script::ScriptExecutor::executeOpcodes() {
debugC(kDebugScript, "----- Scripting function entered - scene: %.2x 1014: %.2x 1012: %.2x", Scenes::instance()._currentSceneIndex, _isSceneInitRun, _repeatRunFlag);
_isRunningScript = true;
@@ -2984,206 +3494,25 @@ ExecutionResult Script::ScriptExecutor::executeOpcodes() {
_lastOpcodeStreamPos = _stream->pos() - 2;
_expectedEndLocation += length + 2;
- // TODO: convert this into a function lookup table and extract all opcode handling into separate functions, this is just for easier reading
- if (opcode1 == 0x01) {
- scriptSetVar();
- } else if (opcode1 == 0x02) {
- scriptSetVarOr();
- } else if (opcode1 == 0x03) {
- scriptIfFalse();
- } else if (opcode1 == 0x04) {
- scriptIfTrue();
- } else if (opcode1 == 0x5) {
- if (!scriptCompare()) {
- endBuffering(_lastOpcodeTriggeredSkip);
- break;
- }
- } else if (opcode1 == 0x06) {
- scriptIfInteraction();
- } else if (opcode1 == 0x07) {
- scriptEndIf();
- } else if (opcode1 == 0x08) {
- scriptElse();
- } else if (opcode1 == 0x09) {
- scriptNop09();
- } else if (opcode1 == 0x10) {
- scriptWalkToPosition();
- } else if (opcode1 == 0x11) {
- return scriptWaitForWalk();
- } else if (opcode1 == 0x13) {
- scriptSkipUntil14();
- } else if (opcode1 == 0x14) {
- scriptSkipWord();
- } else if (opcode1 == 0x0a) {
- scriptPrintStringLeft();
- return ExecutionResult::WaitingForCallback;
- } else if (opcode1 == 0x15) {
- scriptClearDialogueChoices();
- } else if (opcode1 == 0x16) {
- scriptAddDialogueChoice();
- } else if (opcode1 == 0x17) {
- return scriptShowDialogueChoice();
- } else if (opcode1 == 0x18) {
- return scriptDismissPanel();
- } else if (opcode1 == 0x19) {
- scriptWalkToAndPickup();
- endBuffering(_lastOpcodeTriggeredSkip);
- break;
- } else if (opcode1 == 0x1a) {
- scriptSetPickupFrames();
- } else if (opcode1 == 0x1b) {
- scriptSetupObject();
- } else if (opcode1 == 0x1c) {
- scriptSetSkippable();
- } else if (opcode1 == 0x1d) {
- scriptClearSkippable();
- } else if (opcode1 == 0x1e) {
- scriptPlayAnimation();
- } else if (opcode1 == 0x1f) {
- scriptTestPathfinding();
- } else if (opcode1 == 0x20) {
- scriptSetYOffset();
- } else if (opcode1 == 0x21) {
- scriptSetMotion();
- } else if (opcode1 == 0x22) {
- scriptSetOrientation();
- } else if (opcode1 == 0x23) {
- scriptMoveToPosition();
- } else if (opcode1 == 0x24) {
- scriptAddValues();
- } else if (opcode1 == 0x25) {
- scriptSubValues();
- } else if (opcode1 == 0x26) {
- scriptLoadSpecialAnim();
- } else if (opcode1 == 0x27) {
- scriptSetDirection();
- } else if (opcode1 == 0x28) {
- scriptStopAnimation();
- } else if (opcode1 == 0x29) {
- // Binary (1008:e0f9): always terminates script + sets executingObjectId sentinel.
- scriptOpenInventory();
- _stream->seek(_stream->size(), SEEK_SET);
- _executingObjectIndex = 0x201;
- _executingScriptObjectId = 0x201;
- endBuffering(_lastOpcodeTriggeredSkip);
- break;
- } else if (opcode1 == 0x0b) {
- scriptMoveObject();
- if (hasScriptError()) {
- endBuffering(_lastOpcodeTriggeredSkip);
- break;
- }
- // Binary: after scriptMoveObject(), exits if position >= end.
- // scriptMoveObject can advance the stream to end (e.g. if the moved
- // object is the one whose script is executing).
- if (_stream->pos() >= _stream->size()) {
- endBuffering(_lastOpcodeTriggeredSkip);
- break;
- }
- } else if (opcode1 == 0x0c) {
- // Binary: scriptChangeScene (1008:ad6e) runs init+repeat synchronously and
- // returns void; only pause the outer opcode loop when a wait is still active.
- return scriptChangeScene();
- } else if (opcode1 == 0x0d) {
- const ExecutionResult dialogueResult = scriptShowDialogue();
- if (dialogueResult == ExecutionResult::WaitingForCallback)
- return dialogueResult;
- break;
- } else if (opcode1 == 0x0E) {
- scriptChangeAnimation();
- } else if (opcode1 == 0x0F) {
- scriptFrameWait();
- return ExecutionResult::WaitingForCallback;
- } else if (opcode1 == 0x12) {
- scriptSetPathfinding();
- } else if (opcode1 == 0x2A) {
- scriptLoadObjectAnim();
- } else if (opcode1 == 0x2B) {
- scriptCheckObjectData();
- } else if (opcode1 == 0x2C) {
- scriptCheckInventory();
- } else if (opcode1 == 0x2D) {
- scriptSetSnapToTarget();
- } else if (opcode1 == 0x2E) {
- scriptTestSceneAnimFrame();
- } else if (opcode1 == 0x2F) {
- scriptTestObjectAnimFrame();
- } else if (opcode1 == 0x30) {
- scriptPrintStringRight();
- return ExecutionResult::WaitingForCallback;
- } else if (opcode1 == 0x31) {
- scriptSetPaletteDarkness();
- } else if (opcode1 == 0x32) {
- scriptSetObjectShading();
- } else if (opcode1 == 0x33) {
- scriptSetObjectScaling();
- } else if (opcode1 == 0x34) {
- scriptSetHotspotOverride();
- } else if (opcode1 == 0x35) {
- scriptSetObjectBounds();
- } else if (opcode1 == 0x36) {
- scriptDismissAllPanels();
- } else if (opcode1 == 0x37) {
- scriptResetToSceneScript();
- } else if (opcode1 == 0x38) {
- scriptLoadOverlayFont();
- } else if (opcode1 == 0x39) {
- // scriptEndOverlayText (1008:d80f). Clears the overlay text stage.
- scriptEndOverlayText();
- } else if (opcode1 == 0x3A) {
- scriptAddOverlayTextEntry();
- } else if (opcode1 == 0x3B) {
- scriptClearOverlayText();
- } else if (opcode1 == 0x3C) {
- scriptFadeToBlack();
- } else if (opcode1 == 0x3D) {
- scriptFadeFromBlack();
- } else if (opcode1 == 0x3E) {
- scriptLoadPcmSound();
- } else if (opcode1 == 0x3F) {
- scriptFreePcmSound();
- } else if (opcode1 == 0x40) {
- scriptPlayPcmSound();
- } else if (opcode1 == 0x41) {
- if (scriptWaitForSound()) {
- return ExecutionResult::WaitingForCallback;
- }
- } else if (opcode1 == 0x42) {
- scriptStopPcmSound();
- } else if (opcode1 == 0x43) {
- scriptLoadMusicSlot();
- } else if (opcode1 == 0x44) {
- scriptPlayMusicSlot();
- } else if (opcode1 == 0x45) {
- scriptStopMusicSlot();
- } else if (opcode1 == 0x47) {
- if (scriptWaitForMusic()) {
- return ExecutionResult::WaitingForCallback;
- }
- } else if (opcode1 == 0x46) {
- scriptFreeMusicSlot();
- } else if (opcode1 == 0x48) {
- scriptGetObjectX();
- } else if (opcode1 == 0x49) {
- scriptGetObjectY();
- } else if (opcode1 == 0x4A) {
- scriptGetObjectField8();
- } else if (opcode1 == 0x4B) {
- scriptGetObjectOrientation();
- } else if (opcode1 == 0x4C) {
- scriptClearActorInventory();
- } else if (opcode1 == 0x4D) {
- scriptSetPathfindingRemap();
- } else if (opcode1 == 0x4E) {
- if (scriptWaitForAdlib()) {
- return ExecutionResult::WaitingForCallback;
- }
- } else {
+ const OpcodeEntry *entry = nullptr;
+ if (_opcodeTable != nullptr && opcode1 < _opcodeTableSize)
+ entry = &_opcodeTable[opcode1];
+
+ if (entry == nullptr || entry->handler == nullptr) {
setScriptError(7);
endBuffering(_lastOpcodeTriggeredSkip);
break;
}
+
+ const OpcodeResult opcodeResult = (this->*(entry->handler))();
+ if (opcodeResult == OpcodeResult::WaitForCallback)
+ return ExecutionResult::WaitingForCallback;
+ if (opcodeResult == OpcodeResult::ReturnFinished)
+ return ExecutionResult::ScriptFinished;
+
endBuffering(_lastOpcodeTriggeredSkip);
+ if (opcodeResult == OpcodeResult::FinishScript)
+ break;
}
_isRunningScript = false;
if (hasScriptError())
diff --git a/engines/macs2/scriptexecutor.h b/engines/macs2/scriptexecutor.h
index 9f37e4ae2b1..463b952d7d5 100644
--- a/engines/macs2/scriptexecutor.h
+++ b/engines/macs2/scriptexecutor.h
@@ -75,6 +75,21 @@ enum class ExecutionResult {
WaitingForCallback
};
+/** Per-opcode handler result for table-driven dispatch in executeOpcodes(). */
+enum class OpcodeResult {
+ /** Continue with the next opcode in the current script. */
+ Continue,
+ /** Break the opcode loop, then return ScriptFinished (clears running flag). */
+ FinishScript,
+ /** Return WaitingForCallback immediately (leaves running flag set). */
+ WaitForCallback,
+ /**
+ * Return ScriptFinished immediately (leaves running flag set).
+ * Matches legacy `return scriptXxx()` paths such as changeScene / waitForWalk.
+ */
+ ReturnFinished
+};
+
enum class ScriptExecutionState {
// We are executing the scene script
ExecutingSceneScript,
@@ -97,6 +112,23 @@ enum class ScriptExecutionState {
*
*/
class ScriptExecutor {
+public:
+ typedef OpcodeResult (ScriptExecutor::*OpcodeHandler)();
+
+ struct OpcodeEntry {
+ const char *name;
+ OpcodeHandler handler;
+ };
+
+ /** Install the active opcode table (DOS today; other platforms later). */
+ void setOpcodeTable(const OpcodeEntry *table, uint size);
+ /** Name for opcode, or "?" if unset/out of range. */
+ const char *opcodeName(uint8 opcode) const;
+
+ /** Default DOS opcode table (class member so private handlers are accessible). */
+ static const OpcodeEntry kDosOpcodeTable[];
+ static const uint kDosOpcodeTableSize;
+
private:
#ifdef DEMACS2
public:
@@ -180,6 +212,86 @@ public:
void scriptFadeFromBlack();
void scriptFreePcmSound();
+ // Table-driven opcode wrappers (return OpcodeResult for executeOpcodes).
+ OpcodeResult opSetVar();
+ OpcodeResult opSetVarOr();
+ OpcodeResult opIfFalse();
+ OpcodeResult opIfTrue();
+ OpcodeResult opCompare();
+ OpcodeResult opIfInteraction();
+ OpcodeResult opEndIf();
+ OpcodeResult opElse();
+ OpcodeResult opNop09();
+ OpcodeResult opPrintStringLeft();
+ OpcodeResult opMoveObject();
+ OpcodeResult opChangeScene();
+ OpcodeResult opShowDialogue();
+ OpcodeResult opChangeAnimation();
+ OpcodeResult opFrameWait();
+ OpcodeResult opWalkToPosition();
+ OpcodeResult opWaitForWalk();
+ OpcodeResult opSetPathfinding();
+ OpcodeResult opSkipUntil14();
+ OpcodeResult opSkipWord();
+ OpcodeResult opClearDialogueChoices();
+ OpcodeResult opAddDialogueChoice();
+ OpcodeResult opShowDialogueChoice();
+ OpcodeResult opDismissPanel();
+ OpcodeResult opWalkToAndPickup();
+ OpcodeResult opSetPickupFrames();
+ OpcodeResult opSetupObject();
+ OpcodeResult opSetSkippable();
+ OpcodeResult opClearSkippable();
+ OpcodeResult opPlayAnimation();
+ OpcodeResult opTestPathfinding();
+ OpcodeResult opSetYOffset();
+ OpcodeResult opSetMotion();
+ OpcodeResult opSetOrientation();
+ OpcodeResult opMoveToPosition();
+ OpcodeResult opAddValues();
+ OpcodeResult opSubValues();
+ OpcodeResult opLoadSpecialAnim();
+ OpcodeResult opSetDirection();
+ OpcodeResult opStopAnimation();
+ OpcodeResult opOpenInventory();
+ OpcodeResult opLoadObjectAnim();
+ OpcodeResult opCheckObjectData();
+ OpcodeResult opCheckInventory();
+ OpcodeResult opSetSnapToTarget();
+ OpcodeResult opTestSceneAnimFrame();
+ OpcodeResult opTestObjectAnimFrame();
+ OpcodeResult opPrintStringRight();
+ OpcodeResult opSetPaletteDarkness();
+ OpcodeResult opSetObjectShading();
+ OpcodeResult opSetObjectScaling();
+ OpcodeResult opSetHotspotOverride();
+ OpcodeResult opSetObjectBounds();
+ OpcodeResult opDismissAllPanels();
+ OpcodeResult opResetToSceneScript();
+ OpcodeResult opLoadOverlayFont();
+ OpcodeResult opEndOverlayText();
+ OpcodeResult opAddOverlayTextEntry();
+ OpcodeResult opClearOverlayText();
+ OpcodeResult opFadeToBlack();
+ OpcodeResult opFadeFromBlack();
+ OpcodeResult opLoadPcmSound();
+ OpcodeResult opFreePcmSound();
+ OpcodeResult opPlayPcmSound();
+ OpcodeResult opWaitForSound();
+ OpcodeResult opStopPcmSound();
+ OpcodeResult opLoadMusicSlot();
+ OpcodeResult opPlayMusicSlot();
+ OpcodeResult opStopMusicSlot();
+ OpcodeResult opFreeMusicSlot();
+ OpcodeResult opWaitForMusic();
+ OpcodeResult opGetObjectX();
+ OpcodeResult opGetObjectY();
+ OpcodeResult opGetObjectField8();
+ OpcodeResult opGetObjectOrientation();
+ OpcodeResult opClearActorInventory();
+ OpcodeResult opSetPathfindingRemap();
+ OpcodeResult opWaitForAdlib();
+
inline void scriptUnimplementedOpcode(const char *source, uint16 opcode) {
debug("Unimplemented opcode (%s): %.2x.", source, opcode);
}
@@ -187,6 +299,10 @@ public:
private:
#endif
+ // Active opcode table
+ const OpcodeEntry *_opcodeTable = nullptr;
+ uint _opcodeTableSize = 0;
+
// State variables from here
// Overall state
Commit: fe4f8be3935468a83ac03a2b893c103ebf03c88b
https://github.com/scummvm/scummvm/commit/fe4f8be3935468a83ac03a2b893c103ebf03c88b
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00
Commit Message:
MACS2: fixed opcode outer loop exit
Changed paths:
engines/macs2/scriptexecutor.cpp
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index ae2fd77d217..5669eefa209 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -651,6 +651,13 @@ bool ScriptExecutor::loadNextScript() {
// Confirmed from runScriptExecutor (1008:e3e7): after the scene script finishes,
// iterate executingObjectId from 1 to 0x200, skipping objects with no runtime data.
// Load each object's script from runtime+0x187/+0x189/+0x18B.
+ //
+ // Binary: when g_wExecutingScriptObjectId > 0x200 (e.g. opcode 0x29 sentinel),
+ // the outer loop stops without loading further object scripts. Check before the
+ // sceneâobject transition so a scene-script sentinel is not wiped by resetting
+ // _executingObjectIndex to 0.
+ if (_executingScriptObjectId > 0x200)
+ return false;
if (_scriptExecutionState == ScriptExecutionState::ExecutingSceneScript) {
// If we are finished with executing the scene, we need to go over all relevant objects
@@ -695,7 +702,6 @@ bool ScriptExecutor::loadNextScript() {
if (_terminateOuterScriptBeforeRepeat) {
if (_stream)
_stream->seek(_stream->size(), SEEK_SET);
- _executingScriptObjectId = 0x201;
_terminateOuterScriptBeforeRepeat = false;
}
_repeatRunFlag = true;
@@ -708,6 +714,9 @@ bool ScriptExecutor::loadNextScript() {
return false;
}
_stream->seek(0, SEEK_SET);
+ // Fresh scene-script pass (same as runSceneScriptPass): clear any prior
+ // >0x200 sentinel so the early guard above does not skip object scripts.
+ _executingScriptObjectId = 0;
_scriptExecutionState = ScriptExecutionState::ExecutingSceneScript;
debugC(kDebugScript, "----- Deferred repeat pass for scene: %.4x", _executingObjectIndex);
return true;
@@ -3156,7 +3165,13 @@ OpcodeResult ScriptExecutor::opStopAnimation() {
}
OpcodeResult ScriptExecutor::opOpenInventory() {
- // Binary (1008:e0f9): always terminates script + sets executingObjectId sentinel.
+ // Binary executeOpcodes @ 1008:e0f9 (full + demo):
+ // scriptOpenInventory();
+ // g_wScriptPosition = g_wScriptEndPosition;
+ // g_wExecutingScriptObjectId = 0x201;
+ // goto LAB_1008_e3bd;
+ // loadNextScript() honors ScriptObjectId > 0x200 and will not start/continue
+ // the object-script walk (including when 0x29 ran from the scene script).
scriptOpenInventory();
_stream->seek(_stream->size(), SEEK_SET);
_executingObjectIndex = 0x201;
Commit: ef2c114ffb16facf56052e458842bf6454c084f2
https://github.com/scummvm/scummvm/commit/ef2c114ffb16facf56052e458842bf6454c084f2
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00
Commit Message:
MACS2: improved amiga demo support
Changed paths:
A engines/macs2/amiga_archive.cpp
A engines/macs2/amiga_archive.h
A engines/macs2/amiga_decode.cpp
A engines/macs2/amiga_decode.h
A engines/macs2/amiga_resources.cpp
engines/macs2/gameobjects.cpp
engines/macs2/gameobjects.h
engines/macs2/macs2.cpp
engines/macs2/macs2.h
engines/macs2/module.mk
engines/macs2/scriptexecutor.cpp
engines/macs2/scriptexecutor.h
diff --git a/engines/macs2/amiga_archive.cpp b/engines/macs2/amiga_archive.cpp
new file mode 100644
index 00000000000..e2346f69396
--- /dev/null
+++ b/engines/macs2/amiga_archive.cpp
@@ -0,0 +1,603 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "macs2/amiga_archive.h"
+
+#include "common/compression/powerpacker.h"
+#include "common/debug.h"
+#include "common/file.h"
+#include "common/memstream.h"
+#include "common/substream.h"
+#include "common/system.h"
+#include "common/textconsole.h"
+#include "common/array.h"
+
+#include <cstring>
+#include "macs2/detection.h"
+
+namespace Macs2 {
+
+namespace {
+
+static const uint32 kTagMXMF = MKTAG('M', 'X', 'M', 'F');
+static const uint32 kTagMXDR = MKTAG('M', 'X', 'D', 'R');
+static const uint32 kTagMXIN = MKTAG('M', 'X', 'I', 'N');
+static const uint32 kTagMXOO = MKTAG('M', 'X', 'O', 'O');
+static const uint32 kTagMXMM = MKTAG('M', 'X', 'M', 'M');
+static const uint32 kTagPP20 = MKTAG('P', 'P', '2', '0');
+
+static Common::Path volumePathForDisk(uint16 disk) {
+ // Demo uses DataA only (disk 0). Full game may use DataB/DataC/DataD.
+ char letter = (char)('A' + CLIP<int>(disk, 0, 25));
+ return Common::Path(Common::String::format("Data%c", letter));
+}
+
+} // End of anonymous namespace
+
+Macs2AmigaArchive::Macs2AmigaArchive() {
+}
+
+Macs2AmigaArchive::~Macs2AmigaArchive() {
+ close();
+}
+
+AmigaResourceType Macs2AmigaArchive::typeFromTag(uint16 tag) {
+ switch (tag) {
+ case kAmigaResOO:
+ case kAmigaResMM:
+ case kAmigaResOS:
+ case kAmigaResFF:
+ return (AmigaResourceType)tag;
+ default:
+ return kAmigaResUnknown;
+ }
+}
+
+const char *Macs2AmigaArchive::typeToString(AmigaResourceType type) {
+ switch (type) {
+ case kAmigaResOO:
+ return "OO";
+ case kAmigaResMM:
+ return "MM";
+ case kAmigaResOS:
+ return "OS";
+ case kAmigaResFF:
+ return "FF";
+ default:
+ return "??";
+ }
+}
+
+Common::String Macs2AmigaArchive::makeResourceName(AmigaResourceType type, uint16 id) {
+ return Common::String::format("%s_%04u", typeToString(type), id);
+}
+
+bool Macs2AmigaArchive::parseResourceName(const Common::String &name, AmigaResourceType &type, uint16 &id) {
+ if (name.equalsIgnoreCase("scene_table") || name.equalsIgnoreCase("Info"))
+ return false;
+ if (name.size() < 7 || name[2] != '_')
+ return false;
+
+ const uint16 tag = ((uint16)name[0] << 8) | (uint16)name[1];
+ type = typeFromTag(tag);
+ if (type == kAmigaResUnknown)
+ return false;
+
+ id = (uint16)atoi(name.c_str() + 3);
+ return true;
+}
+
+bool Macs2AmigaArchive::open() {
+ close();
+
+ if (!loadMdir()) {
+ close();
+ return false;
+ }
+
+ loadInfo();
+
+ debugC(1, kDebugFilePath, "Macs2 Amiga archive: %u resources, %u scenes, %u volumes indexed",
+ (uint)_entries.size(), _sceneCount, (uint)_volumes.size());
+ return !_entries.empty();
+}
+
+void Macs2AmigaArchive::close() {
+ Common::StackLock lock(_mutex);
+ _entries.clear();
+ _volumes.clear();
+ _sceneCount = 0;
+ _totalResources = 0;
+ _firstBlockSize = 0;
+ _info = AmigaInfoData();
+}
+
+bool Macs2AmigaArchive::openVolume(uint16 disk) {
+ while (_volumes.size() <= disk)
+ _volumes.push_back(Volume());
+
+ Volume &volume = _volumes[disk];
+ if (volume.file)
+ return true;
+
+ volume.path = volumePathForDisk(disk);
+ volume.file.reset(new Common::File());
+ if (!volume.file->open(volume.path)) {
+ warning("Macs2 Amiga: cannot open volume %s", volume.path.toString().c_str());
+ volume.file.reset();
+ return false;
+ }
+ volume.size = (uint32)volume.file->size();
+ return true;
+}
+
+bool Macs2AmigaArchive::loadMdir() {
+ Common::File mdirFile;
+ if (!mdirFile.open("Mdir")) {
+ warning("Macs2 Amiga: Mdir not found");
+ return false;
+ }
+
+ const int64 mdirSize = mdirFile.size();
+ if (mdirSize < 18) {
+ warning("Macs2 Amiga: Mdir too small");
+ return false;
+ }
+
+ if (mdirFile.readUint32BE() != kTagMXDR) {
+ warning("Macs2 Amiga: Mdir is not MXDR");
+ return false;
+ }
+
+ mdirFile.readUint16BE(); // version
+ mdirFile.readUint16BE(); // entry count field (often count+1)
+ mdirFile.readUint16BE(); // type tag, usually "OO"
+ _totalResources = mdirFile.readUint16BE();
+ mdirFile.readUint32BE(); // reserved
+ _sceneCount = mdirFile.readUint16BE();
+
+ const uint16 dirEntryCount = (uint16)((mdirSize - 18) / 10);
+ Common::Array<AmigaDirEntry> dirEntries;
+ dirEntries.resize(dirEntryCount);
+
+ for (uint16 i = 0; i < dirEntryCount; i++) {
+ AmigaDirEntry &e = dirEntries[i];
+ const uint16 typeTag = mdirFile.readUint16BE();
+ e.type = typeFromTag(typeTag);
+ e.id = mdirFile.readUint16BE();
+ e.disk = mdirFile.readUint16BE();
+ e.offset = mdirFile.readUint32BE();
+ }
+
+ // Open volumes referenced by the directory and read each entry header.
+ for (const AmigaDirEntry &dir : dirEntries) {
+ if (dir.type == kAmigaResUnknown)
+ continue;
+ if (!openVolume(dir.disk))
+ return false;
+
+ Volume &volume = _volumes[dir.disk];
+ if (dir.offset + 8 > volume.size) {
+ warning("Macs2 Amiga: entry %s_%04u offset past end of volume", typeToString(dir.type), dir.id);
+ continue;
+ }
+
+ volume.file->seek(dir.offset, SEEK_SET);
+ const uint16 fileTypeTag = volume.file->readUint16BE();
+ const uint16 fileId = volume.file->readUint16BE();
+ const uint32 payloadSize = volume.file->readUint32BE();
+
+ if (typeFromTag(fileTypeTag) != dir.type || fileId != dir.id) {
+ warning("Macs2 Amiga: Mdir/Data mismatch for %s_%04u", typeToString(dir.type), dir.id);
+ continue;
+ }
+ if (payloadSize == 0 || dir.offset + 8 + payloadSize > volume.size) {
+ warning("Macs2 Amiga: bad payload size for %s_%04u", typeToString(dir.type), dir.id);
+ continue;
+ }
+
+ Entry entry;
+ entry.dir = dir;
+ entry.payloadSize = payloadSize;
+ entry.name = makeResourceName(dir.type, dir.id);
+ _entries[Common::Path(entry.name)] = entry;
+ }
+
+ // Index the first PP20 block (scene table) from DataA header.
+ if (!openVolume(0))
+ return false;
+
+ Volume &dataA = _volumes[0];
+ dataA.file->seek(0, SEEK_SET);
+ if (dataA.file->readUint32BE() != kTagMXMF) {
+ warning("Macs2 Amiga: DataA is not MXMF");
+ return false;
+ }
+ dataA.file->readUint16BE(); // version
+ dataA.file->readUint16BE(); // type
+ const uint16 hdrResourceCount = dataA.file->readUint16BE();
+ _firstBlockSize = dataA.file->readUint32BE();
+ _firstBlockOffset = 14;
+ if (hdrResourceCount != 0)
+ _totalResources = hdrResourceCount;
+
+ if (_firstBlockSize == 0 || _firstBlockOffset + _firstBlockSize > dataA.size) {
+ warning("Macs2 Amiga: invalid first PP20 block size");
+ return false;
+ }
+
+ Entry sceneTable;
+ sceneTable.dir.type = kAmigaResOO;
+ sceneTable.dir.id = 0xFFFF;
+ sceneTable.dir.disk = 0;
+ sceneTable.dir.offset = _firstBlockOffset;
+ sceneTable.payloadSize = _firstBlockSize;
+ sceneTable.name = "scene_table";
+ _entries[Common::Path(sceneTable.name)] = sceneTable;
+
+ return true;
+}
+
+bool Macs2AmigaArchive::loadInfo() {
+ Common::File infoFile;
+ if (!infoFile.open("Info"))
+ return false;
+
+ // Layout matches Ghidra load_info_mxin @ 00221494 on demo_unpacked.exe.0:
+ // MXIN(4) + ver(u16)=1 + pad(u16)=0 + startMmId(u32BE)
+ // + 5xu16 panel border color indices
+ // + 8xu16 panel darken color indices
+ // + 16xu16 UI RGB4 colors
+ // + flag bytes (g_bUsePanelPatternFill at +0x50)
+ // + mdirSize / volumeCount
+ if (infoFile.size() < 0x5A || infoFile.readUint32BE() != kTagMXIN)
+ return false;
+
+ const uint16 version = infoFile.readUint16BE();
+ const uint16 pad0 = infoFile.readUint16BE();
+ if (version != 1 || pad0 != 0)
+ return false;
+
+ const uint32 startSceneResourceId = infoFile.readUint32BE();
+
+ AmigaInfoData info;
+ if (startSceneResourceId > 0 && startSceneResourceId <= 0xFFFF)
+ info.startSceneResourceId = (uint16)startSceneResourceId;
+
+ // Ghidra load_info_mxin: bytes 0x0C..0x11 are other MXIN fields; panel tables
+ // start at file +0x12 (demo: six zero bytes after startMmId).
+ byte gap[6] = {0};
+ if (infoFile.read(gap, sizeof(gap)) != sizeof(gap))
+ return false;
+
+ // File offset 0x12: five border color indices (demo 18..22).
+ for (uint i = 0; i < 5; i++)
+ info.panelBorderColorIndices[i] = infoFile.readUint16BE();
+ // Also used historically as cursor mode probes in the demo (OO ids 18..22).
+ for (uint i = 0; i < 5; i++)
+ info.cursorResourceIds[i] = info.panelBorderColorIndices[i];
+
+ for (uint i = 0; i < 8; i++)
+ info.panelDarkenColorIndices[i] = infoFile.readUint16BE();
+
+ for (uint i = 0; i < 16; i++)
+ info.uiPaletteAmiga[i] = infoFile.readUint16BE();
+
+ // Flag block at file +0x4C (Ghidra after UI RGB copy).
+ // g_bUsePanelPatternFill is the byte at file +0x50 (demo: 0 = darken remap).
+ byte flags[10] = {0};
+ if (infoFile.read(flags, sizeof(flags)) != sizeof(flags))
+ return false;
+ info.usePanelPatternFill = flags[4] != 0; // file +0x50 (pre-title only)
+
+ if (infoFile.pos() + 4 <= infoFile.size()) {
+ info.mdirSize = infoFile.readUint16BE();
+ info.volumeCount = infoFile.readUint16BE();
+ }
+ // Demo Info ends with sceneCount-like word earlier in some dumps; keep 0 if absent.
+ info.useInventoryCursorId = 0x17; // Walk/inventory fallback used by gameplay modes
+ info.loaded = true;
+ _info = info;
+
+ loadPrefMenuMode();
+ // Gameplay uses PREF MENUMODE (copied over Info flag after title @ 00234b42).
+ _info.usePanelPatternFill = _info.prefMenuMode != 0;
+
+ if (_info.sceneCount != 0)
+ _sceneCount = _info.sceneCount;
+
+ Entry infoEntry;
+ infoEntry.name = "Info";
+ infoEntry.dir.disk = 0xFFFF; // special: load from loose Info file
+ infoEntry.payloadSize = (uint32)infoFile.size();
+ _entries[Common::Path("Info")] = infoEntry;
+
+ return true;
+}
+
+bool Macs2AmigaArchive::loadPrefMenuMode() {
+ // parsePrefResourceSettings @ 00236e88: * PREF then colon-separated ASCII digits.
+ // Order: SCREENMODE, MENUMODE, MUSIC, INTRO, PICSHADE, ...
+ // initAndRunTitleScreen copies MENUMODE into g_bUsePanelPatternFill.
+ Common::File prefFile;
+ if (!prefFile.open("PREF"))
+ return false;
+
+ const int32 size = prefFile.size();
+ if (size <= 0 || size > 0x10000) {
+ prefFile.close();
+ return false;
+ }
+ Common::Array<char> buf;
+ buf.resize((uint)size + 1);
+ if (prefFile.read(buf.data(), (uint32)size) != (uint32)size) {
+ prefFile.close();
+ return false;
+ }
+ prefFile.close();
+ buf[size] = '\0';
+
+ const char *p = strstr(buf.data(), "* PREF");
+ if (!p)
+ p = buf.data();
+
+ uint values[16] = {0};
+ uint count = 0;
+ while (*p && count < 16) {
+ while (*p && *p != '*')
+ p++;
+ if (!*p)
+ break;
+ p++;
+ while (*p == ' ' || *p == '\t')
+ p++;
+ while (*p && *p != ':' && *p != '\n' && *p != '\r')
+ p++;
+ if (*p != ':')
+ continue;
+ p++;
+ while (*p == ' ' || *p == '\t')
+ p++;
+ if (*p < '0' || *p > '9')
+ continue;
+ uint v = 0;
+ while (*p >= '0' && *p <= '9') {
+ v = v * 10 + (uint)(*p - '0');
+ p++;
+ }
+ values[count++] = v;
+ }
+
+ if (count >= 2) {
+ _info.prefMenuMode = (uint8)(values[1] & 0xFF);
+ debugC(1, kDebugFilePath, "Amiga PREF MENUMODE=%u (0=SHADE/1=GFX/2=SOLID/3=POINTS)",
+ _info.prefMenuMode);
+ return true;
+ }
+ return false;
+}
+
+byte *Macs2AmigaArchive::decompressPP20(const byte *src, uint32 srcLen, uint32 &outLen) {
+ outLen = 0;
+ if (!src || srcLen < 12 || READ_BE_UINT32(src) != kTagPP20)
+ return nullptr;
+
+ // PowerPackerStream::unpackBuffer allocates with new[]; MemoryReadStream frees with free().
+ byte *unpacked = Common::PowerPackerStream::unpackBuffer(src, srcLen, outLen);
+ if (!unpacked || outLen == 0) {
+ delete[] unpacked;
+ outLen = 0;
+ return nullptr;
+ }
+
+ byte *copy = (byte *)malloc(outLen);
+ if (!copy) {
+ delete[] unpacked;
+ outLen = 0;
+ return nullptr;
+ }
+ memcpy(copy, unpacked, outLen);
+ delete[] unpacked;
+ return copy;
+}
+
+byte *Macs2AmigaArchive::decompressPayload(const byte *payload, uint32 payloadLen, uint32 &outLen) {
+ outLen = 0;
+ if (!payload || payloadLen < 4)
+ return nullptr;
+
+ const uint32 magic = READ_BE_UINT32(payload);
+ if (magic == kTagPP20)
+ return decompressPP20(payload, payloadLen, outLen);
+
+ // MXMM is a multi-chunk scene package (chunk0 = planar BG). Return the raw
+ // container so callers can decompress/parse individual chunks.
+ if (magic == kTagMXMM) {
+ byte *copy = (byte *)malloc(payloadLen);
+ if (!copy)
+ return nullptr;
+ memcpy(copy, payload, payloadLen);
+ outLen = payloadLen;
+ return copy;
+ }
+
+ // Uncompressed MXOO / raw payload: return a malloc'd copy for MemoryReadStream.
+ byte *copy = (byte *)malloc(payloadLen);
+ if (!copy)
+ return nullptr;
+ memcpy(copy, payload, payloadLen);
+ outLen = payloadLen;
+ return copy;
+}
+
+Common::SeekableReadStream *Macs2AmigaArchive::readAndDecompressEntry(const Entry &entry) const {
+ // Special case: loose Info file
+ if (entry.name.equalsIgnoreCase("Info")) {
+ Common::File infoFile;
+ if (!infoFile.open("Info"))
+ return nullptr;
+ const uint32 size = (uint32)infoFile.size();
+ byte *data = (byte *)malloc(size);
+ if (!data)
+ return nullptr;
+ if (infoFile.read(data, size) != size) {
+ free(data);
+ return nullptr;
+ }
+ return new Common::MemoryReadStream(data, size, DisposeAfterUse::YES);
+ }
+
+ if (entry.dir.disk == 0xFFFF || entry.dir.disk >= _volumes.size() || !_volumes[entry.dir.disk].file)
+ return nullptr;
+
+ Common::StackLock lock(_mutex);
+ Volume &volume = const_cast<Volume &>(_volumes[entry.dir.disk]);
+
+ uint32 readOffset = entry.dir.offset;
+ uint32 readSize = entry.payloadSize;
+
+ // scene_table points at the raw PP20 block (no 8-byte type header).
+ // Regular resources point at the 8-byte header; payload follows.
+ if (!entry.name.equalsIgnoreCase("scene_table")) {
+ readOffset += 8;
+ }
+
+ if (readOffset + readSize > volume.size)
+ return nullptr;
+
+ Common::Array<byte> payload;
+ payload.resize(readSize);
+ volume.file->seek(readOffset, SEEK_SET);
+ if (volume.file->read(payload.data(), readSize) != readSize)
+ return nullptr;
+
+ uint32 outLen = 0;
+ byte *decoded = decompressPayload(payload.data(), readSize, outLen);
+ if (!decoded)
+ return nullptr;
+
+ return new Common::MemoryReadStream(decoded, outLen, DisposeAfterUse::YES);
+}
+
+bool Macs2AmigaArchive::hasResource(AmigaResourceType type, uint16 id) const {
+ return hasFile(Common::Path(makeResourceName(type, id)));
+}
+
+Common::SeekableReadStream *Macs2AmigaArchive::createReadStreamForResource(AmigaResourceType type, uint16 id) const {
+ return createReadStreamForMember(Common::Path(makeResourceName(type, id)));
+}
+
+Common::SeekableReadStream *Macs2AmigaArchive::createSceneTableStream() const {
+ return createReadStreamForMember(Common::Path("scene_table"));
+}
+
+bool Macs2AmigaArchive::hasFile(const Common::Path &path) const {
+ return _entries.contains(path);
+}
+
+int Macs2AmigaArchive::listMembers(Common::ArchiveMemberList &list) const {
+ for (const auto &entry : _entries)
+ list.push_back(getMember(entry._key));
+ return _entries.size();
+}
+
+const Common::ArchiveMemberPtr Macs2AmigaArchive::getMember(const Common::Path &path) const {
+ if (!hasFile(path))
+ return Common::ArchiveMemberPtr();
+ return Common::ArchiveMemberPtr(new Common::GenericArchiveMember(path, *this));
+}
+
+Common::SeekableReadStream *Macs2AmigaArchive::createReadStreamForMember(const Common::Path &path) const {
+ if (!_entries.contains(path))
+ return nullptr;
+ return readAndDecompressEntry(_entries[path]);
+}
+
+bool Macs2AmigaArchive::decodePlanarSprite(const byte *mxoo, uint32 mxooSize, uint16 &width, uint16 &height, Common::Array<byte> &pixels) {
+ width = height = 0;
+ pixels.clear();
+
+ if (!mxoo || mxooSize < 32 || READ_BE_UINT32(mxoo) != kTagMXOO)
+ return false;
+
+ const uint32 scriptOff = READ_BE_UINT32(mxoo + 4);
+ if (scriptOff <= 12 || scriptOff > mxooSize)
+ return false;
+
+ const byte *body = mxoo + 12;
+ const uint32 bodyLen = scriptOff - 12;
+ if (bodyLen < 20 || body[10] != 1 || body[11] != 1)
+ return false;
+
+ const uint16 frames = READ_BE_UINT16(body + 14);
+ width = READ_BE_UINT16(body + 16);
+ height = READ_BE_UINT16(body + 18);
+ if (frames == 0 || width == 0 || width > 320 || height == 0 || height > 200)
+ return false;
+
+ const uint32 rowBytes = (width + 7) / 8;
+ const uint32 planeSize = rowBytes * height;
+ const uint32 pixelBytes = bodyLen - 20;
+ if (planeSize == 0 || pixelBytes < planeSize * 5)
+ return false;
+
+ const uint32 numPlanes = pixelBytes / planeSize;
+ if (numPlanes < 5 || numPlanes > 6 || pixelBytes != planeSize * numPlanes)
+ return false;
+
+ pixels.resize((uint)width * height);
+ Common::fill(pixels.begin(), pixels.end(), 0);
+
+ const byte *pixData = body + 20;
+ // Planes 0..4 are color; plane 5 (if present) is mask/transparency.
+ for (uint32 plane = 0; plane < 5 && plane < numPlanes; plane++) {
+ const uint32 pOff = plane * planeSize;
+ for (uint16 y = 0; y < height; y++) {
+ for (uint32 bx = 0; bx < rowBytes; bx++) {
+ const byte b = pixData[pOff + y * rowBytes + bx];
+ for (int bit = 0; bit < 8; bit++) {
+ const uint16 x = (uint16)(bx * 8 + bit);
+ if (x < width && (b & (0x80 >> bit)))
+ pixels[y * width + x] |= (byte)(1 << plane);
+ }
+ }
+ }
+ }
+
+ if (numPlanes >= 6) {
+ // Mask plane: clear transparent pixels (bit clear in mask).
+ const uint32 pOff = 5 * planeSize;
+ for (uint16 y = 0; y < height; y++) {
+ for (uint32 bx = 0; bx < rowBytes; bx++) {
+ const byte b = pixData[pOff + y * rowBytes + bx];
+ for (int bit = 0; bit < 8; bit++) {
+ const uint16 x = (uint16)(bx * 8 + bit);
+ if (x < width && (b & (0x80 >> bit)) == 0)
+ pixels[y * width + x] = 0;
+ }
+ }
+ }
+ }
+
+ return true;
+}
+
+} // End of namespace Macs2
diff --git a/engines/macs2/amiga_archive.h b/engines/macs2/amiga_archive.h
new file mode 100644
index 00000000000..07995fcb39a
--- /dev/null
+++ b/engines/macs2/amiga_archive.h
@@ -0,0 +1,179 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MACS2_AMIGA_ARCHIVE_H
+#define MACS2_AMIGA_ARCHIVE_H
+
+#include "common/archive.h"
+#include "common/array.h"
+#include "common/file.h"
+#include "common/hashmap.h"
+#include "common/mutex.h"
+#include "common/ptr.h"
+#include "common/str.h"
+#include "common/stream.h"
+
+namespace Macs2 {
+
+/**
+ * Resource type tags used in Amiga Mdir / DataA entry headers.
+ * Stored as big-endian ASCII pairs: "OO", "MM", "OS", "FF".
+ */
+enum AmigaResourceType : uint16 {
+ kAmigaResOO = 0x4F4F, // object / graphics
+ kAmigaResMM = 0x4D4D, // scene package (MXMM: planar BG + tables; not Protracker)
+ kAmigaResOS = 0x4F53, // sound effect
+ kAmigaResFF = 0x4646, // font
+ kAmigaResUnknown = 0
+};
+
+struct AmigaDirEntry {
+ AmigaResourceType type = kAmigaResUnknown;
+ uint16 id = 0;
+ uint16 disk = 0;
+ uint32 offset = 0; // absolute offset of the 8-byte entry header in Data*
+};
+
+struct AmigaInfoData {
+ uint16 cursorResourceIds[8] = {0}; // Talk..Disabled style slots from Info
+ uint16 useInventoryCursorId = 0;
+ uint16 sceneCount = 0;
+ uint16 volumeCount = 0;
+ uint16 mdirSize = 0;
+ /** 16x 12-bit 0x0RGB UI colors from MXIN (Ghidra DAT_002379dc). */
+ uint16 uiPaletteAmiga[16] = {0};
+ /**
+ * Panel border line/corner color indices from MXIN (Ghidra g_awPanelBorderColorIndices).
+ * Demo: 18,19,20,21,22.
+ */
+ uint16 panelBorderColorIndices[5] = {0};
+ /**
+ * Panel darken luminance->color table (Ghidra g_awPanelDarkenColorIndices).
+ * Demo: 28,29,29,60,61,31,62,23. Indexed by (7 - (R4+G4+B4)/0x18).
+ */
+ uint16 panelDarkenColorIndices[8] = {0};
+ /**
+ * After title screen, g_bUsePanelPatternFill is overwritten from PREF MENUMODE
+ * (initAndRunTitleScreen @ 00234b42): 0=SHADE (darken remap), 1=GFX / 2=SOLID /
+ * 3=POINTS -> fill_ui_panel_pattern_tiles @ 00222332 (opaque 16x16 tiles).
+ * Demo PREF defaults to MENUMODE 2 (SOLID). Info MXIN +0x50 is only the
+ * pre-title default (0).
+ */
+ uint8 prefMenuMode = 2;
+ bool usePanelPatternFill = true;
+ /**
+ * Starting MM resource id from MXIN (u32BE at Info offset 8).
+ * Demo Info stores 40 -> MM_0040 (intro). Script-visible scene id is mmId+1.
+ */
+ uint16 startSceneResourceId = 0;
+ bool loaded = false;
+};
+
+/**
+ * Amiga MACS2 container: Mdir (MXDR) index + DataA/DataB/... (MXMF) volumes.
+ *
+ * Members are exposed as Common::Archive paths:
+ * "scene_table" - first PP20 block (decompressed MXOO)
+ * "OO_0104", "MM_0004".. - typed resources (PP20 decompressed; MXMM raw)
+ * "Info" - raw MXIN game metadata (if present on disk)
+ *
+ * Based on FORMAT.md / extract_macs2 Amiga mode in scummvm-tools.
+ */
+class Macs2AmigaArchive : public Common::Archive {
+public:
+ Macs2AmigaArchive();
+ ~Macs2AmigaArchive() override;
+
+ /**
+ * Open Mdir + DataA (and additional DataB.. volumes referenced by Mdir).
+ * Looks up files via SearchMan / the game directory.
+ */
+ bool open();
+ void close();
+ bool isOpen() const { return !_entries.empty(); }
+
+ uint16 getSceneCount() const { return _sceneCount; }
+ uint16 getResourceCount() const { return _totalResources; }
+ const AmigaInfoData &getInfo() const { return _info; }
+
+ /** Decompressed resource stream, or nullptr if missing. Caller owns the stream. */
+ Common::SeekableReadStream *createReadStreamForResource(AmigaResourceType type, uint16 id) const;
+
+ /** Decompressed first MXMF PP20 block (scene/base table). */
+ Common::SeekableReadStream *createSceneTableStream() const;
+
+ bool hasResource(AmigaResourceType type, uint16 id) const;
+
+ // Common::Archive
+ bool hasFile(const Common::Path &path) const override;
+ int listMembers(Common::ArchiveMemberList &list) const override;
+ const Common::ArchiveMemberPtr getMember(const Common::Path &path) const override;
+ Common::SeekableReadStream *createReadStreamForMember(const Common::Path &path) const override;
+
+ static Common::String makeResourceName(AmigaResourceType type, uint16 id);
+ static bool parseResourceName(const Common::String &name, AmigaResourceType &type, uint16 &id);
+ static AmigaResourceType typeFromTag(uint16 tag);
+ static const char *typeToString(AmigaResourceType type);
+
+ /**
+ * Decode an Amiga planar MXOO sprite body into chunky 8bpp pixels.
+ * Returns false if the resource is not a simple sprite.
+ */
+ static bool decodePlanarSprite(const byte *mxoo, uint32 mxooSize, uint16 &width, uint16 &height, Common::Array<byte> &pixels);
+
+private:
+ struct Volume {
+ Common::Path path;
+ Common::ScopedPtr<Common::File> file;
+ uint32 size = 0;
+ };
+
+ struct Entry {
+ AmigaDirEntry dir;
+ uint32 payloadSize = 0; // size field from the 8-byte Data* header
+ Common::String name;
+ };
+
+ typedef Common::HashMap<Common::Path, Entry, Common::Path::IgnoreCase_Hash, Common::Path::IgnoreCase_EqualTo> EntryMap;
+
+ bool openVolume(uint16 disk);
+ bool loadMdir();
+ bool loadInfo();
+ /** Parse PREF MENUMODE into _info (Ghidra parsePrefResourceSettings -> title exit). */
+ bool loadPrefMenuMode();
+ Common::SeekableReadStream *readAndDecompressEntry(const Entry &entry) const;
+ static byte *decompressPP20(const byte *src, uint32 srcLen, uint32 &outLen);
+ static byte *decompressPayload(const byte *payload, uint32 payloadLen, uint32 &outLen);
+
+ EntryMap _entries;
+ Common::Array<Volume> _volumes;
+ mutable Common::Mutex _mutex;
+
+ uint16 _sceneCount = 0;
+ uint16 _totalResources = 0;
+ uint32 _firstBlockSize = 0;
+ uint32 _firstBlockOffset = 14;
+ AmigaInfoData _info;
+};
+
+} // End of namespace Macs2
+
+#endif // MACS2_AMIGA_ARCHIVE_H
diff --git a/engines/macs2/amiga_decode.cpp b/engines/macs2/amiga_decode.cpp
new file mode 100644
index 00000000000..4418e268c84
--- /dev/null
+++ b/engines/macs2/amiga_decode.cpp
@@ -0,0 +1,1038 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "macs2/amiga_decode.h"
+
+#include "common/compression/powerpacker.h"
+#include "common/endian.h"
+#include "common/hashmap.h"
+#include "common/util.h"
+
+namespace Macs2 {
+
+static const uint16 kPlanes = 6;
+
+bool parseAmigaMxoo(const byte *mxoo, uint32 mxooSize, AmigaMxooInfo &out) {
+ out = AmigaMxooInfo();
+ if (!mxoo || mxooSize < 24)
+ return false;
+ if (READ_BE_UINT32(mxoo) != MKTAG('M', 'X', 'O', 'O'))
+ return false;
+
+ out.scriptOffset = READ_BE_UINT32(mxoo + 4);
+ out.stringOffset = READ_BE_UINT32(mxoo + 8);
+ out.bodyOffset = 12;
+ if (out.scriptOffset > mxooSize || out.stringOffset > mxooSize)
+ return false;
+ out.bodySize = out.scriptOffset > out.bodyOffset ? out.scriptOffset - out.bodyOffset : 0;
+ if (out.bodySize < 0x66)
+ return false;
+
+ const byte *body = mxoo + out.bodyOffset;
+ if (READ_BE_UINT16(body + 0x0C) != 0x0101)
+ return false;
+
+ for (uint i = 0; i < 21; i++)
+ out.slotOffsets[i] = READ_BE_UINT32(body + 0x0E + i * 4);
+ out.extraOffset = READ_BE_UINT32(body + 0x62);
+ return true;
+}
+
+bool inspectAmigaAnimSlot(const byte *mxoo, uint32 mxooSize, uint32 bodyRelativeOffset, AmigaAnimSlotInfo &out) {
+ out = AmigaAnimSlotInfo();
+ if (!mxoo || bodyRelativeOffset == 0 || bodyRelativeOffset == 0xFFFFFFFF)
+ return false;
+
+ const uint32 absOff = 12 + bodyRelativeOffset;
+ if (absOff + 16 > mxooSize)
+ return false;
+
+ const byte *p = mxoo + absOff;
+ out.headerHint = READ_BE_UINT16(p + 0);
+ out.seqPos = READ_BE_UINT16(p + 2);
+ out.repeatCounter = READ_BE_UINT16(p + 4);
+ out.loopStart = READ_BE_UINT16(p + 6);
+
+ // Sequence length follows the same pattern as the DOS blob: headerHint is often
+ // the frame count; sequence payload is max(hint - 2, 0) bytes, padded to even.
+ const uint16 seqBytes = (out.headerHint >= 2) ? (uint16)(out.headerHint - 2) : 0;
+ const uint16 seqPadded = (seqBytes + 1) & ~1;
+ const uint32 metaOff = absOff + 8 + seqPadded;
+ if (metaOff + 6 > mxooSize)
+ return false;
+
+ out.frameCount = READ_BE_UINT16(mxoo + metaOff + 0);
+ out.width = READ_BE_UINT16(mxoo + metaOff + 2);
+ out.height = READ_BE_UINT16(mxoo + metaOff + 4);
+ out.pixelOffset = metaOff + 6;
+ out.headerSize = out.pixelOffset - absOff;
+
+ if (out.frameCount == 0 || out.width == 0 || out.height == 0)
+ return false;
+ if (out.width > 320 || out.height > 200 || out.frameCount > 256)
+ return false;
+
+ const uint32 rowBytes = (out.width + 7) / 8;
+ const uint32 planeBytes = rowBytes * out.height;
+ const uint32 pixelsNeeded = (uint32)out.frameCount * planeBytes * kPlanes;
+ if (out.pixelOffset + pixelsNeeded > mxooSize)
+ return false;
+
+ out.valid = true;
+ return true;
+}
+
+bool decodeAmigaPlanarFrame(const byte *planar, uint16 width, uint16 height, uint16 frameIndex,
+ uint16 frameCount, Common::Array<byte> &outPixels) {
+ if (!planar || width == 0 || height == 0 || frameIndex >= frameCount)
+ return false;
+
+ const uint32 rowBytes = (width + 7) / 8;
+ const uint32 planeBytes = rowBytes * height;
+ const uint32 frameBytes = planeBytes * kPlanes;
+ const byte *frameBase = planar + (uint32)frameIndex * frameBytes;
+
+ outPixels.clear();
+ outPixels.resize((uint32)width * height);
+ Common::fill(outPixels.begin(), outPixels.end(), 0);
+
+ // Anim slots store 6 bitplanes; colors use planes 0..4 (indices 0..31).
+ // Plane 5 is not applied as a sprite mask here - doing so clears almost all
+ // opaque pixels in the demo's character anims. Color 0 remains transparent
+ // for the existing DOS draw path.
+ for (uint16 y = 0; y < height; y++) {
+ for (uint16 x = 0; x < width; x++) {
+ byte color = 0;
+ const uint32 bitIndex = (uint32)x & 7;
+ const uint32 byteInRow = (uint32)x >> 3;
+ for (uint16 plane = 0; plane < 5; plane++) {
+ const byte *planeRow = frameBase + plane * planeBytes + (uint32)y * rowBytes;
+ if (planeRow[byteInRow] & (0x80 >> bitIndex))
+ color |= (byte)(1 << plane);
+ }
+ outPixels[(uint32)y * width + x] = color;
+ }
+ }
+ return true;
+}
+
+bool convertAmigaAnimSlotToDosBlob(const byte *mxoo, uint32 mxooSize, uint32 bodyRelativeOffset,
+ Common::Array<byte> &outBlob) {
+ outBlob.clear();
+ AmigaAnimSlotInfo info;
+ if (!inspectAmigaAnimSlot(mxoo, mxooSize, bodyRelativeOffset, info) || !info.valid)
+ return false;
+
+ const byte *planar = mxoo + info.pixelOffset;
+
+ // DOS AnimBlobView layout (LE):
+ // +0x00 unknown, +0x02 seqPos, +0x04 repeat, +0x06 loopStart, +0x08 delay,
+ // +0x0A seqLenMinusOne; sequence at +0x0C (N bytes);
+ // frames at +0x0B + (N+1) = +0x0C + N: uint16 frameCount, then per-frame records
+ // Frame: ox,oy,unk,w,h (LE) + chunky pixels
+ Common::Array<byte> sequence;
+ for (uint16 i = 0; i < info.frameCount; i++)
+ sequence.push_back((byte)(10 + i));
+
+ const uint32 seqLenMinusOne = sequence.size();
+ const uint32 frameDataOffset = 0x0C + seqLenMinusOne;
+ uint32 total = frameDataOffset + 2; // + frameCount word
+ for (uint16 f = 0; f < info.frameCount; f++)
+ total += 10 + (uint32)info.width * info.height;
+
+ outBlob.resize(total);
+ byte *dst = outBlob.data();
+ WRITE_LE_UINT16(dst + 0x00, info.headerHint);
+ WRITE_LE_UINT16(dst + 0x02, info.seqPos ? info.seqPos : 1);
+ WRITE_LE_UINT16(dst + 0x04, info.repeatCounter);
+ WRITE_LE_UINT16(dst + 0x06, info.loopStart);
+ WRITE_LE_UINT16(dst + 0x08, 0); // delay
+ WRITE_LE_UINT16(dst + 0x0A, (uint16)seqLenMinusOne);
+ if (!sequence.empty())
+ memcpy(dst + 0x0C, sequence.data(), sequence.size());
+
+ WRITE_LE_UINT16(dst + frameDataOffset, info.frameCount);
+ uint32 off = frameDataOffset + 2;
+ for (uint16 f = 0; f < info.frameCount; f++) {
+ Common::Array<byte> pixels;
+ if (!decodeAmigaPlanarFrame(planar, info.width, info.height, f, info.frameCount, pixels))
+ return false;
+ WRITE_LE_UINT16(dst + off + 0, 0); // ox
+ WRITE_LE_UINT16(dst + off + 2, 0); // oy
+ WRITE_LE_UINT16(dst + off + 4, 0); // unk
+ WRITE_LE_UINT16(dst + off + 6, info.width);
+ WRITE_LE_UINT16(dst + off + 8, info.height);
+ memcpy(dst + off + 10, pixels.data(), pixels.size());
+ off += 10 + pixels.size();
+ }
+ return true;
+}
+
+bool extractAmigaScript(const byte *mxoo, uint32 mxooSize, Common::Array<byte> &outScript) {
+ outScript.clear();
+ AmigaMxooInfo info;
+ if (!parseAmigaMxoo(mxoo, mxooSize, info))
+ return false;
+ if (info.scriptOffset + 4 > mxooSize)
+ return false;
+ // Script section: u16BE pad, u16BE size, then bytecode
+ const uint16 size = READ_BE_UINT16(mxoo + info.scriptOffset + 2);
+ const uint32 dataOff = info.scriptOffset + 4;
+ if (dataOff + size > mxooSize)
+ return false;
+ outScript.resize(size);
+ if (size)
+ memcpy(outScript.data(), mxoo + dataOff, size);
+ return true;
+}
+
+bool extractAmigaStringBlock(const byte *mxoo, uint32 mxooSize, Common::Array<byte> &outStrings) {
+ outStrings.clear();
+ AmigaMxooInfo info;
+ if (!parseAmigaMxoo(mxoo, mxooSize, info))
+ return false;
+ // String section: u16BE pad, u16BE size, then length-prefixed plaintext entries.
+ // Script string offsets are relative to the entries (after the 4-byte header).
+ if (info.stringOffset + 4 > mxooSize)
+ return false;
+ const uint16 size = READ_BE_UINT16(mxoo + info.stringOffset + 2);
+ const uint32 dataOff = info.stringOffset + 4;
+ if (dataOff + size > mxooSize)
+ return false;
+ outStrings.resize(size);
+ if (size)
+ memcpy(outStrings.data(), mxoo + dataOff, size);
+ return true;
+}
+
+bool convertAmigaSimpleSpriteToDosBlob(const byte *mxoo, uint32 mxooSize, Common::Array<byte> &outBlob) {
+ outBlob.clear();
+ if (!mxoo || mxooSize < 32 || READ_BE_UINT32(mxoo) != MKTAG('M', 'X', 'O', 'O'))
+ return false;
+
+ const uint32 scriptOff = READ_BE_UINT32(mxoo + 4);
+ if (scriptOff <= 12 || scriptOff > mxooSize)
+ return false;
+
+ const byte *body = mxoo + 12;
+ const uint32 bodyLen = scriptOff - 12;
+ if (bodyLen < 20 || body[10] != 1 || body[11] != 1)
+ return false;
+
+ const uint16 frames = READ_BE_UINT16(body + 14);
+ const uint16 width = READ_BE_UINT16(body + 16);
+ const uint16 height = READ_BE_UINT16(body + 18);
+ if (frames == 0 || width == 0 || width > 320 || height == 0 || height > 200)
+ return false;
+
+ const uint32 rowBytes = (width + 7) / 8;
+ const uint32 planeSize = rowBytes * height;
+ const uint32 pixelBytes = bodyLen - 20;
+ if (planeSize == 0 || pixelBytes < planeSize * 5)
+ return false;
+ const uint32 numPlanes = pixelBytes / planeSize;
+ if (numPlanes < 5 || numPlanes > 6 || pixelBytes != planeSize * numPlanes)
+ return false;
+
+ Common::Array<byte> pixels;
+ pixels.resize((uint32)width * height);
+ Common::fill(pixels.begin(), pixels.end(), 0);
+ const byte *pixData = body + 20;
+ for (uint32 plane = 0; plane < 5 && plane < numPlanes; plane++) {
+ const uint32 pOff = plane * planeSize;
+ for (uint16 y = 0; y < height; y++) {
+ for (uint32 bx = 0; bx < rowBytes; bx++) {
+ const byte b = pixData[pOff + y * rowBytes + bx];
+ for (int bit = 0; bit < 8; bit++) {
+ const uint16 x = (uint16)(bx * 8 + bit);
+ if (x < width && (b & (0x80 >> bit)))
+ pixels[y * width + x] |= (byte)(1 << plane);
+ }
+ }
+ }
+ }
+ if (numPlanes >= 6) {
+ const uint32 pOff = 5 * planeSize;
+ for (uint16 y = 0; y < height; y++) {
+ for (uint32 bx = 0; bx < rowBytes; bx++) {
+ const byte b = pixData[pOff + y * rowBytes + bx];
+ for (int bit = 0; bit < 8; bit++) {
+ const uint16 x = (uint16)(bx * 8 + bit);
+ if (x < width && (b & (0x80 >> bit)) == 0)
+ pixels[y * width + x] = 0;
+ }
+ }
+ }
+ }
+
+ // seqLenMinusOne=0 -> sequenceLength=1 -> frame data at 0x0C:
+ // uint16 frameCount, then ox,oy,unk,w,h + pixels.
+ const uint32 total = 0x0C + 2 + 10 + (uint32)width * height;
+ outBlob.resize(total);
+ byte *dst = outBlob.data();
+ WRITE_LE_UINT16(dst + 0x00, frames);
+ WRITE_LE_UINT16(dst + 0x02, 1);
+ WRITE_LE_UINT16(dst + 0x04, 0);
+ WRITE_LE_UINT16(dst + 0x06, 0);
+ WRITE_LE_UINT16(dst + 0x08, 0);
+ WRITE_LE_UINT16(dst + 0x0A, 0); // seqLenMinusOne
+ WRITE_LE_UINT16(dst + 0x0C, 1); // frameCount
+ WRITE_LE_UINT16(dst + 0x0E + 0, 0);
+ WRITE_LE_UINT16(dst + 0x0E + 2, 0);
+ WRITE_LE_UINT16(dst + 0x0E + 4, 0);
+ WRITE_LE_UINT16(dst + 0x0E + 6, width);
+ WRITE_LE_UINT16(dst + 0x0E + 8, height);
+ memcpy(dst + 0x0E + 10, pixels.data(), pixels.size());
+ return true;
+}
+
+bool convertAmigaPortraitAtlasToDosBlob(const byte *mxoo, uint32 mxooSize, uint32 bodyRelativeExtraOffset,
+ Common::Array<byte> &outBlob) {
+ outBlob.clear();
+ if (!mxoo || bodyRelativeExtraOffset == 0 || bodyRelativeExtraOffset == 0xFFFFFFFF)
+ return false;
+
+ const uint32 absExtra = 12 + bodyRelativeExtraOffset;
+ // animateDialoguePortrait @ 0022f79c: copy 14400 bytes; blit D3=D4=D6=0x50,
+ // D5=0xF0. drawSprite plane loop uses 6 separated planes -> 30*80*6=14400.
+ // Atlas is 240x80 (3x80x80 frames). Color from planes 0..4.
+ const uint16 atlasW = 240;
+ const uint16 atlasH = 80;
+ const uint16 frameW = 80;
+ const uint16 frameH = 80;
+ const uint16 frameCount = 3;
+ const uint32 rowBytes = (atlasW + 7) / 8;
+ const uint32 planeBytes = rowBytes * atlasH;
+ const uint32 portraitBytes = planeBytes * 6; // 14400
+ if (absExtra + portraitBytes > mxooSize)
+ return false;
+
+ const byte *src = mxoo + absExtra;
+ Common::Array<byte> atlas;
+ atlas.resize((uint32)atlasW * atlasH);
+ Common::fill(atlas.begin(), atlas.end(), 0);
+ for (uint16 y = 0; y < atlasH; y++) {
+ for (uint16 x = 0; x < atlasW; x++) {
+ byte color = 0;
+ const uint32 bitIndex = (uint32)x & 7;
+ const uint32 byteInRow = (uint32)x >> 3;
+ for (uint16 plane = 0; plane < 5; plane++) {
+ const byte *planeRow = src + plane * planeBytes + (uint32)y * rowBytes;
+ if (planeRow[byteInRow] & (0x80 >> bitIndex))
+ color |= (byte)(1 << plane);
+ }
+ atlas[(uint32)y * atlasW + x] = color;
+ }
+ }
+
+ // DOS anim blob: seqLenMinusOne = frameCount, sequence at +0x0C, frames follow.
+ const uint32 seqLenMinusOne = frameCount;
+ const uint32 frameDataOffset = 0x0C + seqLenMinusOne;
+ const uint32 frameBytes = 10 + (uint32)frameW * frameH;
+ const uint32 total = frameDataOffset + 2 + (uint32)frameCount * frameBytes;
+ outBlob.resize(total);
+ byte *dst = outBlob.data();
+ WRITE_LE_UINT16(dst + 0x00, frameCount);
+ WRITE_LE_UINT16(dst + 0x02, 1);
+ WRITE_LE_UINT16(dst + 0x04, 0);
+ WRITE_LE_UINT16(dst + 0x06, 0);
+ WRITE_LE_UINT16(dst + 0x08, 0);
+ WRITE_LE_UINT16(dst + 0x0A, (uint16)seqLenMinusOne);
+ for (uint16 i = 0; i < frameCount; i++)
+ dst[0x0C + i] = (byte)(10 + i);
+
+ WRITE_LE_UINT16(dst + frameDataOffset, frameCount);
+ uint32 off = frameDataOffset + 2;
+ for (uint16 f = 0; f < frameCount; f++) {
+ WRITE_LE_UINT16(dst + off + 0, 0);
+ WRITE_LE_UINT16(dst + off + 2, 0);
+ WRITE_LE_UINT16(dst + off + 4, 0);
+ WRITE_LE_UINT16(dst + off + 6, frameW);
+ WRITE_LE_UINT16(dst + off + 8, frameH);
+ byte *pixDst = dst + off + 10;
+ Common::fill(pixDst, pixDst + (uint32)frameW * frameH, 0);
+ for (uint16 y = 0; y < frameH; y++) {
+ memcpy(pixDst + (uint32)y * frameW,
+ atlas.data() + (uint32)y * atlasW + (uint32)f * frameW,
+ frameW);
+ }
+ // Keep Amiga COLOR indices 0..31. Demo portraits (OO_*) use only the
+ // copper high bank COLOR17..31 (chrome / skin / navy cap) - the same
+ // registers animateDialoguePortrait blits into on hardware. Remapping
+ // into a private 0xD0 bank drifted from live copper and caused the
+ // Freunde-dialogue palette mismatch vs FS-UAE.
+ // Color 0 stays transparent (drawSprite skips 0).
+ off += frameBytes;
+ }
+ return true;
+}
+
+static bool decompressPp20ToBuffer(const byte *src, uint32 srcLen, Common::Array<byte> &out) {
+ out.clear();
+ if (!src || srcLen < 12 || READ_BE_UINT32(src) != MKTAG('P', 'P', '2', '0'))
+ return false;
+
+ uint32 outLen = 0;
+ byte *unpacked = Common::PowerPackerStream::unpackBuffer(src, srcLen, outLen);
+ if (!unpacked || outLen == 0) {
+ delete[] unpacked;
+ return false;
+ }
+ out.resize(outLen);
+ memcpy(out.data(), unpacked, outLen);
+ delete[] unpacked;
+ return true;
+}
+
+bool decodeAmigaMxmmSceneBackground(const byte *mxmm, uint32 mxmmSize,
+ Common::Array<byte> &outPixels,
+ byte outPaletteRgb[768],
+ uint &outColorCount) {
+ outPixels.clear();
+ outColorCount = 0;
+ if (outPaletteRgb) {
+ for (uint i = 0; i < 768; i++)
+ outPaletteRgb[i] = 0;
+ }
+ if (!mxmm || mxmmSize < 14 || !outPaletteRgb)
+ return false;
+ if (READ_BE_UINT32(mxmm) != MKTAG('M', 'X', 'M', 'M'))
+ return false;
+
+ const uint32 chunk0Size = READ_BE_UINT32(mxmm + 10);
+ if (chunk0Size == 0 || 14 + chunk0Size > mxmmSize)
+ return false;
+
+ const byte *chunk0 = mxmm + 14;
+ Common::Array<byte> screen;
+ if (READ_BE_UINT32(chunk0) == MKTAG('P', 'P', '2', '0')) {
+ if (!decompressPp20ToBuffer(chunk0, chunk0Size, screen))
+ return false;
+ } else {
+ screen.resize(chunk0Size);
+ memcpy(screen.data(), chunk0, chunk0Size);
+ }
+
+ if (screen.size() < kAmigaSceneScreenSize)
+ return false;
+
+ Common::Array<byte> planarIndex;
+ planarIndex.resize((uint)kAmigaSceneWidth * kAmigaSceneHeight);
+ Common::fill(planarIndex.begin(), planarIndex.end(), 0);
+
+ const byte *planes = screen.data();
+ const uint32 rowBytes = kAmigaSceneWidth / 8; // 40
+ for (uint16 plane = 0; plane < kAmigaScenePlanes; plane++) {
+ const byte *planeBase = planes + (uint32)plane * kAmigaScenePlaneBytes;
+ for (uint16 y = 0; y < kAmigaSceneHeight; y++) {
+ for (uint32 bx = 0; bx < rowBytes; bx++) {
+ const byte b = planeBase[(uint32)y * rowBytes + bx];
+ for (int bit = 0; bit < 8; bit++) {
+ const uint16 x = (uint16)(bx * 8 + bit);
+ if (b & (0x80 >> bit))
+ planarIndex[(uint32)y * kAmigaSceneWidth + x] |= (byte)(1 << plane);
+ }
+ }
+ }
+ }
+
+ const byte *copper = screen.data() + kAmigaSceneCopperOffset;
+ uint16 base16[16];
+ for (uint i = 0; i < 16; i++)
+ base16[i] = READ_BE_UINT16(copper + i * 2);
+ const byte *lineColors = copper + 0x20; // 200 x 16 x u16BE
+
+ auto amiga12ToRgb8 = [](uint16 rgb, byte &r, byte &g, byte &b) {
+ const byte r4 = (rgb >> 8) & 0xF;
+ const byte g4 = (rgb >> 4) & 0xF;
+ const byte b4 = rgb & 0xF;
+ r = (byte)(r4 * 17);
+ g = (byte)(g4 * 17);
+ b = (byte)(b4 * 17);
+ };
+
+ auto buildPal32 = [&](uint16 y, byte pal32[32][3]) {
+ amiga12ToRgb8(base16[0], pal32[0][0], pal32[0][1], pal32[0][2]);
+ for (uint i = 0; i < 16; i++) {
+ const uint16 c = READ_BE_UINT16(lineColors + (uint32)y * 32 + i * 2);
+ amiga12ToRgb8(c, pal32[1 + i][0], pal32[1 + i][1], pal32[1 + i][2]);
+ }
+ for (uint i = 1; i < 16; i++)
+ amiga12ToRgb8(base16[i], pal32[16 + i][0], pal32[16 + i][1], pal32[16 + i][2]);
+ };
+
+ // Reserve 0..31 for Amiga COLOR registers (sprites) and 32..63 for EHB.
+ byte staticPal[32][3];
+ buildPal32(0, staticPal);
+ for (uint i = 0; i < 32; i++) {
+ outPaletteRgb[i * 3 + 0] = staticPal[i][0];
+ outPaletteRgb[i * 3 + 1] = staticPal[i][1];
+ outPaletteRgb[i * 3 + 2] = staticPal[i][2];
+ }
+ for (uint i = 0; i < 32; i++) {
+ outPaletteRgb[(32 + i) * 3 + 0] = (byte)(staticPal[i][0] / 2);
+ outPaletteRgb[(32 + i) * 3 + 1] = (byte)(staticPal[i][1] / 2);
+ outPaletteRgb[(32 + i) * 3 + 2] = (byte)(staticPal[i][2] / 2);
+ }
+ outColorCount = 64;
+
+ Common::HashMap<uint32, byte> colorToIndex;
+ for (uint i = 0; i < 64; i++) {
+ const uint32 key = ((uint32)outPaletteRgb[i * 3 + 0] << 16) |
+ ((uint32)outPaletteRgb[i * 3 + 1] << 8) |
+ outPaletteRgb[i * 3 + 2];
+ if (!colorToIndex.contains(key))
+ colorToIndex[key] = (byte)i;
+ }
+
+ outPixels.resize((uint)kAmigaSceneWidth * kAmigaSceneHeight);
+ for (uint16 y = 0; y < kAmigaSceneHeight; y++) {
+ byte pal32[32][3];
+ buildPal32(y, pal32);
+
+ for (uint16 x = 0; x < kAmigaSceneWidth; x++) {
+ byte idx = planarIndex[(uint32)y * kAmigaSceneWidth + x];
+ byte r, g, b;
+ if (idx >= 32) {
+ const byte base = (byte)(idx - 32);
+ r = (byte)(pal32[base][0] / 2);
+ g = (byte)(pal32[base][1] / 2);
+ b = (byte)(pal32[base][2] / 2);
+ } else {
+ r = pal32[idx][0];
+ g = pal32[idx][1];
+ b = pal32[idx][2];
+ }
+
+ const uint32 key = ((uint32)r << 16) | ((uint32)g << 8) | b;
+ byte outIdx;
+ if (colorToIndex.contains(key)) {
+ outIdx = colorToIndex[key];
+ } else if (outColorCount < 256) {
+ outIdx = (byte)outColorCount;
+ colorToIndex[key] = outIdx;
+ outPaletteRgb[outIdx * 3 + 0] = r;
+ outPaletteRgb[outIdx * 3 + 1] = g;
+ outPaletteRgb[outIdx * 3 + 2] = b;
+ outColorCount++;
+ } else {
+ outIdx = idx < 64 ? idx : (byte)(idx & 31);
+ }
+ outPixels[(uint32)y * kAmigaSceneWidth + x] = outIdx;
+ }
+ }
+
+ return outColorCount > 0;
+}
+
+static bool amigaMxmmIterChunks(const byte *mxmm, uint32 mxmmSize, uint32 &outTrailerOff,
+ Common::Array<const byte *> *outChunkPtrs,
+ Common::Array<uint32> *outChunkSizes) {
+ outTrailerOff = 0;
+ if (!mxmm || mxmmSize < 14 || READ_BE_UINT32(mxmm) != MKTAG('M', 'X', 'M', 'M'))
+ return false;
+
+ uint32 pos = 10;
+ while (pos + 4 <= mxmmSize) {
+ const uint32 chunkSize = READ_BE_UINT32(mxmm + pos);
+ if (chunkSize == 0)
+ break;
+ if (pos + 4 + chunkSize > mxmmSize)
+ break;
+ if (outChunkPtrs && outChunkSizes) {
+ outChunkPtrs->push_back(mxmm + pos + 4);
+ outChunkSizes->push_back(chunkSize);
+ }
+ pos += 4 + chunkSize;
+ }
+ outTrailerOff = pos;
+ return true;
+}
+
+static bool decompressAmigaChunkToBuffer(const byte *chunk, uint32 chunkSize, Common::Array<byte> &out) {
+ out.clear();
+ if (!chunk || chunkSize == 0)
+ return false;
+ if (READ_BE_UINT32(chunk) == MKTAG('P', 'P', '2', '0'))
+ return decompressPp20ToBuffer(chunk, chunkSize, out);
+ out.resize(chunkSize);
+ memcpy(out.data(), chunk, chunkSize);
+ return true;
+}
+
+/**
+ * Advance past MXMM size-prefixed chunks and the trailer script / optional extra
+ * blob so pos sits at the pathfinding table preamble (native load_scene_mxmm
+ * after bindLoadedScriptBlobToSlot @ 00221d90).
+ */
+static bool amigaMxmmSeekSceneTables(const byte *mxmm, uint32 mxmmSize, uint32 &outPos) {
+ outPos = 0;
+ uint32 pos = 0;
+ if (!amigaMxmmIterChunks(mxmm, mxmmSize, pos, nullptr, nullptr))
+ return false;
+
+ // Empty MXAA (and any other leading zero size words) before the script payload.
+ while (pos + 4 <= mxmmSize && READ_BE_UINT32(mxmm + pos) == 0)
+ pos += 4;
+ if (pos + 4 > mxmmSize)
+ return false;
+
+ const uint32 scriptPayloadSize = READ_BE_UINT32(mxmm + pos);
+ pos += 4;
+ if (scriptPayloadSize == 0 || pos + scriptPayloadSize > mxmmSize)
+ return false;
+ pos += scriptPayloadSize;
+
+ if (pos + 4 > mxmmSize)
+ return false;
+ const uint32 extraSize = READ_BE_UINT32(mxmm + pos);
+ pos += 4;
+ if (extraSize > 0) {
+ if (pos + extraSize > mxmmSize)
+ return false;
+ pos += extraSize;
+ }
+
+ outPos = pos;
+ return true;
+}
+
+bool extractAmigaMxmmSceneScript(const byte *mxmm, uint32 mxmmSize,
+ Common::Array<byte> &outScript,
+ Common::Array<byte> &outStrings) {
+ outScript.clear();
+ outStrings.clear();
+
+ uint32 trailerOff = 0;
+ if (!amigaMxmmIterChunks(mxmm, mxmmSize, trailerOff, nullptr, nullptr))
+ return false;
+ if (trailerOff >= mxmmSize)
+ return false;
+
+ const byte *trailer = mxmm + trailerOff;
+ uint32 trailerSize = mxmmSize - trailerOff;
+ uint32 pos = 0;
+
+ // Skip leading empty u32 markers (optional trailing chunk sizes of 0).
+ while (pos + 8 <= trailerSize && READ_BE_UINT32(trailer + pos) == 0)
+ pos += 4;
+
+ if (pos + 8 > trailerSize)
+ return false;
+
+ // Native reads one size-prefixed payload; the first u32 inside is often an
+ // inner bytecode length (e.g. 690) with the remaining bytes as script body.
+ const uint32 scriptPayloadSize = READ_BE_UINT32(trailer + pos);
+ pos += 4;
+ if (scriptPayloadSize == 0 || pos + scriptPayloadSize > trailerSize)
+ return false;
+
+ const byte *payload = trailer + pos;
+ pos += scriptPayloadSize;
+
+ uint32 scriptSize = scriptPayloadSize;
+ const byte *script = payload;
+ if (scriptPayloadSize >= 6) {
+ const uint32 innerLen = READ_BE_UINT32(payload);
+ if (innerLen >= 2 && innerLen + 4 <= scriptPayloadSize &&
+ (innerLen + 4 == scriptPayloadSize || innerLen + 4 + 1 == scriptPayloadSize)) {
+ script = payload + 4;
+ scriptSize = innerLen;
+ }
+ }
+
+ // Accept empty/tiny stubs (e.g. single 0x18) and real scene scripts.
+ if (scriptSize >= 2) {
+ const byte op = script[0];
+ const byte len = script[1];
+ if (len > 32 || (uint32)2 + len > scriptSize)
+ return false;
+ if (op != 0x04 && op != 0x05 && op != 0x01 && op != 0x0F && op != 0x0C && op != 0x18 && op != 0x07)
+ return false;
+ }
+
+ outScript.resize(scriptSize);
+ memcpy(outScript.data(), script, scriptSize);
+
+ // Optional extra blob: outer u32 size, payload often starts with an inner
+ // u32 string-bytes length (Ghidra load_scene_mxmm stage 0x19).
+ if (pos + 4 <= trailerSize) {
+ const uint32 extraSize = READ_BE_UINT32(trailer + pos);
+ if (extraSize > 0 && pos + 4 + extraSize <= trailerSize) {
+ const byte *extra = trailer + pos + 4;
+ uint32 stringBytes = extraSize;
+ const byte *stringData = extra;
+ if (extraSize >= 4) {
+ const uint32 inner = READ_BE_UINT32(extra);
+ if (inner > 0 && inner + 4 <= extraSize) {
+ stringData = extra + 4;
+ stringBytes = inner;
+ }
+ }
+ outStrings.resize(stringBytes);
+ memcpy(outStrings.data(), stringData, stringBytes);
+ }
+ }
+
+ return !outScript.empty();
+}
+
+static bool amigaMxmmGetTrailerTableBase(const byte *mxmm, uint32 mxmmSize, uint32 &outPos) {
+ outPos = 0;
+ if (!amigaMxmmSeekSceneTables(mxmm, mxmmSize, outPos))
+ return false;
+ // Native @ 00221d90: skip u16, then 0xA2 pathfinding, skip u16, 0x22 hotspot, 0xA walk.
+ const uint32 kTableBytes = 2 + 0xA2 + 2 + 0x22 + 10;
+ if (outPos + kTableBytes > mxmmSize)
+ return false;
+ outPos += 2; // preamble u16
+ return true;
+}
+
+bool extractAmigaMxmmScenePathfinding(const byte *mxmm, uint32 mxmmSize,
+ uint16 &outNumPoints,
+ Common::Array<AmigaPathfindingNode> &outNodes) {
+ outNumPoints = 0;
+ outNodes.clear();
+
+ uint32 pos = 0;
+ if (!amigaMxmmGetTrailerTableBase(mxmm, mxmmSize, pos))
+ return false;
+
+ const byte *table = mxmm + pos;
+ outNumPoints = READ_BE_UINT16(table);
+ if (outNumPoints > 16)
+ outNumPoints = 16;
+
+ outNodes.resize(16);
+ for (uint i = 0; i < 16; i++) {
+ const byte *node = table + 2 + i * 10;
+ AmigaPathfindingNode &dst = outNodes[i];
+ dst.x = READ_BE_UINT16(node + 0);
+ dst.y = READ_BE_UINT16(node + 2);
+ dst.adjacent[0] = node[4];
+ dst.adjacent[1] = node[5];
+ dst.adjacent[2] = node[6];
+ dst.adjacent[3] = node[7];
+ // DOS/Amiga: last word is connection count (not padding). BE on Amiga.
+ dst.numConnections = READ_BE_UINT16(node + 8);
+ if (dst.numConnections > 4)
+ dst.numConnections = 4;
+ }
+ return true;
+}
+
+bool extractAmigaMxmmSceneWalkParams(const byte *mxmm, uint32 mxmmSize,
+ uint16 &outWalkDepthThresholdY,
+ uint16 &outWalkDepthScaleFactor,
+ uint16 &outWalkBaseSpeedPct,
+ uint16 &outScenePaletteMode,
+ uint16 &outPaletteDarkenPercent) {
+ outWalkDepthThresholdY = 0;
+ outWalkDepthScaleFactor = 0;
+ outWalkBaseSpeedPct = 0;
+ outScenePaletteMode = 0;
+ outPaletteDarkenPercent = 0;
+
+ uint32 pos = 0;
+ if (!amigaMxmmGetTrailerTableBase(mxmm, mxmmSize, pos))
+ return false;
+
+ // Skip pathfinding (0xA2) + preamble u16 + hotspot (0x22).
+ pos += 0xA2 + 2 + 0x22;
+
+ outWalkDepthThresholdY = READ_BE_UINT16(mxmm + pos + 0);
+ outWalkDepthScaleFactor = READ_BE_UINT16(mxmm + pos + 2);
+ outWalkBaseSpeedPct = READ_BE_UINT16(mxmm + pos + 4);
+ outScenePaletteMode = READ_BE_UINT16(mxmm + pos + 6);
+ outPaletteDarkenPercent = READ_BE_UINT16(mxmm + pos + 8);
+ return true;
+}
+
+bool extractAmigaMxmmSceneMaps(const byte *mxmm, uint32 mxmmSize,
+ Common::Array<byte> &outPathfinding,
+ Common::Array<byte> &outDepth,
+ Common::Array<byte> &outShadow) {
+ outPathfinding.clear();
+ outDepth.clear();
+ outShadow.clear();
+
+ uint32 trailerOff = 0;
+ Common::Array<const byte *> chunkPtrs;
+ Common::Array<uint32> chunkSizes;
+ if (!amigaMxmmIterChunks(mxmm, mxmmSize, trailerOff, &chunkPtrs, &chunkSizes))
+ return false;
+ if (chunkPtrs.size() < 2)
+ return false;
+
+ const uint32 kMapBytes = (uint32)kAmigaSceneWidth * kAmigaSceneHeight; // 64000
+ Common::Array<byte> maps[3];
+ uint mapCount = 0;
+
+ // Chunk0 is the planar screen; subsequent 64000-byte buffers are maps.
+ for (uint i = 1; i < chunkPtrs.size() && mapCount < 3; i++) {
+ Common::Array<byte> decoded;
+ if (!decompressAmigaChunkToBuffer(chunkPtrs[i], chunkSizes[i], decoded))
+ continue;
+ if (decoded.size() != kMapBytes)
+ continue;
+ maps[mapCount++] = Common::move(decoded);
+ }
+
+ if (mapCount == 0)
+ return false;
+
+ // Native load_scene_mxmm order (NOT DOS MCS order):
+ // Map0 -> depth / draw mask (g_pSceneMap0Buffer; buildDepthMaskFromMap0)
+ // Map1 -> walkability (g_pSceneMap1Buffer; getWalkabilityAt)
+ // Map2 -> MXCC hotspot RLE (not a 64000 surface; see extractAmigaMxmmMxccHotspotMap)
+ // DOS MCS is depth, pathfinding, shadow, hotspot. Swapping Map0/Map1 into
+ // ScummVM's pathfinding slot made MM_0004 treat the depth mask as walk
+ // blockers (255 walls), so off-screen walkTo cancelled and waitForWalk
+ // completed early. Map1 on that demo is all zeros (= open, like DOS scene 5).
+ if (mapCount >= 1)
+ outDepth = Common::move(maps[0]);
+ if (mapCount >= 2)
+ outPathfinding = Common::move(maps[1]);
+ if (mapCount >= 3)
+ outShadow = Common::move(maps[2]);
+ return true;
+}
+
+bool extractAmigaMxmmSceneHotspotColors(const byte *mxmm, uint32 mxmmSize,
+ uint16 &outNumHotspots,
+ Common::Array<uint16> &outColorTable) {
+ outNumHotspots = 0;
+ outColorTable.clear();
+
+ uint32 pos = 0;
+ if (!amigaMxmmGetTrailerTableBase(mxmm, mxmmSize, pos))
+ return false;
+
+ // Skip pathfinding (0xA2) + preamble u16 before hotspot table.
+ pos += 0xA2 + 2;
+ if (pos + 0x22 > mxmmSize)
+ return false;
+
+ outNumHotspots = READ_BE_UINT16(mxmm + pos);
+ if (outNumHotspots > 16)
+ outNumHotspots = 16;
+
+ // Same 0x20 byte layout as DOS (f9 00 fe 00 ...). On LE hosts, memcpy into
+ // uint16 yields color in the low byte - matches getHotspotAtPoint.
+ outColorTable.resize(0x20 / sizeof(uint16));
+ memcpy(outColorTable.data(), mxmm + pos + 2, 0x20);
+ return true;
+}
+
+bool extractAmigaMxmmMxccHotspotMap(const byte *mxmm, uint32 mxmmSize,
+ Common::Array<byte> &outHotspotMap) {
+ outHotspotMap.clear();
+
+ uint32 trailerOff = 0;
+ Common::Array<const byte *> chunkPtrs;
+ Common::Array<uint32> chunkSizes;
+ if (!amigaMxmmIterChunks(mxmm, mxmmSize, trailerOff, &chunkPtrs, &chunkSizes))
+ return false;
+
+ const byte *mxcc = nullptr;
+ uint32 mxccSize = 0;
+ for (uint i = 0; i < chunkPtrs.size(); i++) {
+ if (chunkSizes[i] >= 0x19A + 320 &&
+ READ_BE_UINT32(chunkPtrs[i]) == MKTAG('M', 'X', 'C', 'C')) {
+ mxcc = chunkPtrs[i];
+ mxccSize = chunkSizes[i];
+ break;
+ }
+ }
+ if (!mxcc)
+ return false;
+
+ // Header: MXCC + ver u16BE + pad u16BE + payloadSize u16BE (Ghidra Map2).
+ if (READ_BE_UINT16(mxcc + 4) != 1)
+ return false;
+
+ const byte marker = mxcc[5];
+ const uint32 kRowTableOff = 0x0A;
+ const uint32 kRowDataBase = 0x19A;
+ const uint kRows = 200;
+ const uint kWidth = 320;
+ if (kRowTableOff + kRows * 2 > mxccSize || kRowDataBase > mxccSize)
+ return false;
+
+ outHotspotMap.resize(kWidth * kRows);
+ Common::fill(outHotspotMap.begin(), outHotspotMap.end(), 0);
+
+ for (uint y = 0; y < kRows; y++) {
+ // decodeMxccRunLengthAt: y==0 uses offset 0; else BE word at +0x0A+(y-1)*2.
+ uint32 rowOff = 0;
+ if (y > 0)
+ rowOff = READ_BE_UINT16(mxcc + kRowTableOff + (y - 1) * 2);
+
+ uint32 p = kRowDataBase + rowOff;
+ uint x = 0;
+ while (x < kWidth && p < mxccSize) {
+ const byte b = mxcc[p++];
+ if (b == marker) {
+ if (p + 1 >= mxccSize)
+ break;
+ const byte color = mxcc[p++];
+ const byte run = mxcc[p++];
+ for (uint r = 0; r < run && x < kWidth; r++)
+ outHotspotMap[y * kWidth + x++] = color;
+ } else {
+ outHotspotMap[y * kWidth + x++] = b;
+ }
+ }
+ }
+ return true;
+}
+
+bool amigaMxmmHasMxaaOverlay(const byte *mxmm, uint32 mxmmSize) {
+ uint32 pos = 0;
+ if (!amigaMxmmIterChunks(mxmm, mxmmSize, pos, nullptr, nullptr))
+ return false;
+ // Native: immediately after Map2 comes the MXAA size word (0 = absent).
+ if (pos + 4 > mxmmSize)
+ return false;
+ const uint32 mxaaSize = READ_BE_UINT32(mxmm + pos);
+ if (mxaaSize == 0 || pos + 4 + mxaaSize > mxmmSize)
+ return false;
+ const byte *blob = mxmm + pos + 4;
+ if (mxaaSize >= 4 && READ_BE_UINT32(blob) == MKTAG('M', 'X', 'A', 'A'))
+ return true;
+ if (mxaaSize >= 4 && READ_BE_UINT32(blob) == MKTAG('P', 'P', '2', '0')) {
+ Common::Array<byte> decoded;
+ if (decompressAmigaChunkToBuffer(blob, mxaaSize, decoded) &&
+ decoded.size() >= 4 && READ_BE_UINT32(decoded.data()) == MKTAG('M', 'X', 'A', 'A'))
+ return true;
+ }
+ return false;
+}
+
+bool decodeAmigaMxffFont(const byte *mxff, uint32 mxffSize, Common::Array<AmigaMxffGlyph> &outGlyphs) {
+ outGlyphs.clear();
+ if (!mxff || mxffSize < 0x10A + 160)
+ return false;
+ if (READ_BE_UINT32(mxff) != MKTAG('M', 'X', 'F', 'F'))
+ return false;
+ if (READ_BE_UINT16(mxff + 4) != 1)
+ return false;
+
+ const uint16 blitHeight = READ_BE_UINT16(mxff + 6);
+ const uint16 atlasRows = READ_BE_UINT16(mxff + 8);
+ const uint16 atlasWidthPixels = READ_BE_UINT16(mxff + 0xA);
+ const uint16 planes = READ_BE_UINT16(mxff + 0xC);
+ if (blitHeight == 0 || atlasRows == 0 || atlasWidthPixels < 8 || planes != 6)
+ return false;
+
+ const uint32 rowBytes = (uint32)(atlasWidthPixels >> 3);
+ if (rowBytes == 0)
+ return false;
+ const uint32 planeBytes = rowBytes * atlasRows;
+ const uint32 atlasBytes = planeBytes * 6;
+ const uint32 atlasOff = 0x10A;
+ if (atlasOff + atlasBytes > mxffSize)
+ return false;
+
+ const byte *charmap = mxff + 0x0E;
+ const byte *widths = mxff + 0x8C;
+ const byte *atlas = mxff + atlasOff;
+ const uint16 glyphHeight = MIN(blitHeight, atlasRows);
+ const uint16 cellWidth = 16; // drawText: glyphIndex * (height>>3) byte offset -> 16px cells
+
+ // chars 0x20..0x20+0x7D (0x7E entries)
+ for (uint i = 0; i < 0x7E; i++) {
+ const byte ascii = (byte)(0x20 + i);
+ const byte glyphIndex = charmap[i];
+ const byte advance = widths[i];
+ // 0x4E in the map is unused/empty (Ghidra charmap filler).
+ if (glyphIndex == 0x4E || advance == 0)
+ continue;
+
+ const uint32 cellX0 = (uint32)glyphIndex * cellWidth;
+ if (cellX0 + advance > atlasWidthPixels)
+ continue;
+
+ AmigaMxffGlyph glyph;
+ glyph.ascii = (char)ascii;
+ glyph.width = advance;
+ glyph.height = glyphHeight;
+ glyph.pixels.resize((uint32)advance * glyphHeight);
+ Common::fill(glyph.pixels.begin(), glyph.pixels.end(), 0);
+
+ for (uint16 y = 0; y < glyphHeight; y++) {
+ for (uint16 x = 0; x < advance; x++) {
+ const uint32 absX = cellX0 + x;
+ byte color = 0;
+ const uint32 bit = absX & 7;
+ const uint32 byteInRow = absX >> 3;
+ for (uint16 plane = 0; plane < 6; plane++) {
+ const byte *planeRow = atlas + plane * planeBytes + (uint32)y * rowBytes;
+ if (planeRow[byteInRow] & (0x80 >> bit))
+ color |= (byte)(1 << plane);
+ }
+ glyph.pixels[(uint32)y * advance + x] = color;
+ }
+ }
+ outGlyphs.push_back(Common::move(glyph));
+ }
+
+ return !outGlyphs.empty();
+}
+
+bool extractAmigaMxosPcm(const byte *mxos, uint32 mxosSize, Common::Array<byte> &outPcm, uint16 &outRateHz) {
+ outPcm.clear();
+ outRateHz = 8000;
+ if (!mxos || mxosSize < 0x20)
+ return false;
+ if (READ_BE_UINT32(mxos) != MKTAG('M', 'X', 'O', 'S'))
+ return false;
+
+ // Demo MXOS: word at +0x10 is the byte offset of the first sample block.
+ const uint16 sampleOff = READ_BE_UINT16(mxos + 0x10);
+ if (sampleOff < 0x14 || sampleOff >= mxosSize)
+ return false;
+
+ // Optional Paula period near the sample header (word after offset on several demos).
+ // period 0 -> keep 8000 Hz. NTSC color clock / period ~= Paula playback rate.
+ if ((uint32)sampleOff + 4 <= mxosSize) {
+ const uint16 period = READ_BE_UINT16(mxos + 0x12);
+ if (period >= 0x50 && period <= 0x400) {
+ const uint32 rate = 3579545u / (uint32)period;
+ if (rate >= 4000 && rate <= 28867)
+ outRateHz = (uint16)rate;
+ }
+ }
+
+ const uint32 pcmBytes = mxosSize - (uint32)sampleOff;
+ if (pcmBytes < 16)
+ return false;
+
+ outPcm.resize(pcmBytes);
+ // Paula samples are signed 8-bit; MacsAudioStream expects unsigned (value-128)*256.
+ for (uint32 i = 0; i < pcmBytes; i++)
+ outPcm[i] = (byte)((int8)mxos[sampleOff + i] + 128);
+
+ return true;
+}
+
+} // End of namespace Macs2
diff --git a/engines/macs2/amiga_decode.h b/engines/macs2/amiga_decode.h
new file mode 100644
index 00000000000..4dbc633e7a0
--- /dev/null
+++ b/engines/macs2/amiga_decode.h
@@ -0,0 +1,270 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MACS2_AMIGA_DECODE_H
+#define MACS2_AMIGA_DECODE_H
+
+#include "common/array.h"
+#include "common/scummsys.h"
+
+namespace Macs2 {
+
+/**
+ * Amiga MXOO object body layout (after the 12-byte MXOO header):
+ * +0x00..0x0B padding
+ * +0x0C signature 0x0101
+ * +0x0E 21 x uint32BE slot offsets (0 / 0xFFFFFFFF = empty)
+ * +0x62 uint32BE offset of extra/portrait section (often end of anims)
+ *
+ * Each anim slot is planar 6-plane frame data with a short BE header.
+ * Script bytecode is identical to DOS (LE operands). Strings are plaintext
+ * with uint16BE length prefixes (no XOR cipher).
+ *
+ * Game object index = Amiga OO resource id + 1 (OO_0000 -> object 1).
+ */
+struct AmigaMxooInfo {
+ uint32 scriptOffset = 0;
+ uint32 stringOffset = 0;
+ uint32 slotOffsets[21];
+ uint32 extraOffset = 0;
+ uint32 bodyOffset = 12; // start of body within MXOO
+ uint32 bodySize = 0;
+
+ AmigaMxooInfo() {
+ for (uint i = 0; i < 21; i++)
+ slotOffsets[i] = 0xFFFFFFFF;
+ }
+};
+
+struct AmigaAnimSlotInfo {
+ uint16 frameCount = 0;
+ uint16 width = 0;
+ uint16 height = 0;
+ uint16 seqPos = 0;
+ uint16 repeatCounter = 0;
+ uint16 loopStart = 0;
+ uint16 headerHint = 0;
+ uint32 pixelOffset = 0; // absolute offset in MXOO of first planar byte
+ uint32 headerSize = 0; // bytes from slot start to pixel data
+ bool valid = false;
+};
+
+bool parseAmigaMxoo(const byte *mxoo, uint32 mxooSize, AmigaMxooInfo &out);
+bool inspectAmigaAnimSlot(const byte *mxoo, uint32 mxooSize, uint32 bodyRelativeOffset, AmigaAnimSlotInfo &out);
+
+/** Decode one frame of planar Amiga anim data to chunky 8bpp (color planes 0..4). */
+bool decodeAmigaPlanarFrame(const byte *planar, uint16 width, uint16 height, uint16 frameIndex,
+ uint16 frameCount, Common::Array<byte> &outPixels);
+
+/**
+ * Convert an Amiga anim slot into a DOS-compatible animation blob so the
+ * existing AnimBlobView / renderer path can consume it.
+ */
+bool convertAmigaAnimSlotToDosBlob(const byte *mxoo, uint32 mxooSize, uint32 bodyRelativeOffset,
+ Common::Array<byte> &outBlob);
+
+/** Extract script bytecode (without the MXOO script section header). */
+bool extractAmigaScript(const byte *mxoo, uint32 mxooSize, Common::Array<byte> &outScript);
+
+/**
+ * Extract Amiga string entries (u16BE length + plaintext), skipping the
+ * MXOO string-section header. Offsets used by scripts are relative to this block.
+ */
+bool extractAmigaStringBlock(const byte *mxoo, uint32 mxooSize, Common::Array<byte> &outStrings);
+
+/** Convert a simple planar MXOO sprite (cursor/icon) into a 1-frame DOS anim blob. */
+bool convertAmigaSimpleSpriteToDosBlob(const byte *mxoo, uint32 mxooSize, Common::Array<byte> &outBlob);
+
+/**
+ * Convert Amiga dialogue portrait atlas (body+0x62) into a DOS anim blob.
+ * Layout: 240x80x6 separated planar = three 80x80 faces (D5=0xF0, D3/D4/D6=0x50,
+ * 6 plane blit in animateDialoguePortrait @ 0022f79c). Color from planes 0..4.
+ * Pixels keep Amiga COLOR indices 0..31 (playfield copper); demo portraits use
+ * only COLOR17..31 from the copper high bank / MXIN chrome ramp.
+ */
+bool convertAmigaPortraitAtlasToDosBlob(const byte *mxoo, uint32 mxooSize, uint32 bodyRelativeExtraOffset,
+ Common::Array<byte> &outBlob);
+
+/** Decompressed MXMM chunk0 screen buffer size (6x8000 planes + copper block). */
+enum : uint32 {
+ kAmigaSceneScreenSize = 54432, // 0xD4A0
+ kAmigaSceneCopperOffset = 0xBB80,
+ kAmigaSceneCopperSize = 0x1920, // 16 base colors + 200x16 line colors
+ kAmigaScenePlaneBytes = 8000 // 40x200
+};
+enum : uint16 {
+ kAmigaSceneWidth = 320,
+ kAmigaSceneHeight = 200,
+ kAmigaScenePlanes = 6 // BPLCON0 = 0x6200 -> EHB
+};
+
+/**
+ * Decode MXMM scene package chunk0 into chunky 8bpp 320x200 pixels and an RGB8
+ * palette (up to 256 entries).
+ *
+ * Palette layout (matches Amiga copper / sprite drawing):
+ * - indices 0..31: COLOR00..31 from the copper base block + first scanline
+ * - indices 32..63: Extra HalfBrite of 0..31 (BPLCON0 0x6200)
+ * - indices 64..*: extra colors needed for per-scanline copper differences
+ *
+ * Character/OO sprites use planes 0..4 against COLOR00..31, so 0..31 must stay
+ * stable Amiga hardware colors.
+ */
+bool decodeAmigaMxmmSceneBackground(const byte *mxmm, uint32 mxmmSize,
+ Common::Array<byte> &outPixels,
+ byte outPaletteRgb[768],
+ uint &outColorCount);
+
+/**
+ * Extract scene script + string block from an MXMM package trailer.
+ *
+ * After the size-prefixed chunks (BG / maps / MXCC / ...), the trailer is:
+ * [optional u32 zero markers]
+ * u32BE scriptSize
+ * u32BE unknown (ignored)
+ * script[scriptSize] - DOS-identical LE bytecode
+ * [optional] u32BE stringBytes + u16BE-length plaintext entries
+ *
+ * Ghidra: load_scene_mxmm reads this after planar BG / map chunks.
+ * Script-visible scene ids are MM_resource_id + 1 (changeScene subtracts 1
+ * before MM lookup; curScene is set to mmId+1 after load).
+ */
+bool extractAmigaMxmmSceneScript(const byte *mxmm, uint32 mxmmSize,
+ Common::Array<byte> &outScript,
+ Common::Array<byte> &outStrings);
+
+/**
+ * Extract scene walk / palette scalars from the MXMM trailer tables.
+ *
+ * Ghidra load_scene_mxmm @ 00221de8 reads 10 bytes after pathfinding (0xA2) and
+ * hotspot (0x22) tables into DAT_002376da..e2 (five BE u16s), matching DOS
+ * scene+0x51FD..0x5205:
+ * [0] walk depth threshold Y
+ * [1] walk depth scale factor
+ * [2] walk base speed percent
+ * [3] scene palette mode
+ * [4] palette darken percent
+ *
+ * Without these, ScummVM's DOS walkAlongPath formula collapses to 1 px/frame
+ * when the Amiga native path never loads MCS scene metadata.
+ */
+bool extractAmigaMxmmSceneWalkParams(const byte *mxmm, uint32 mxmmSize,
+ uint16 &outWalkDepthThresholdY,
+ uint16 &outWalkDepthScaleFactor,
+ uint16 &outWalkBaseSpeedPct,
+ uint16 &outScenePaletteMode,
+ uint16 &outPaletteDarkenPercent);
+
+/**
+ * One pathfinding graph node from the MXMM trailer 0xA2 table
+ * (Ghidra load_scene_mxmm @ 00221d9a -> 00248794).
+ *
+ * Layout (BE), matching DOS MCS 16x10-byte nodes:
+ * u16 x, u16 y, u8 adj[4], u16 unused
+ * Prefixed by u16BE active node count (DOS scene+0x51F7).
+ */
+struct AmigaPathfindingNode {
+ uint16 x = 0;
+ uint16 y = 0;
+ byte adjacent[4] = {0, 0, 0, 0};
+ uint16 numConnections = 0;
+};
+
+/**
+ * Extract pathfinding node graph from the MXMM trailer (0xA2 bytes after a
+ * skipped u16). Always yields 16 node slots; outNumPoints is the active count.
+ * Without this, Amiga scenes keep _numPathfindingPoints==0, calculatePath fails,
+ * walks cancel early, and waitForWalk completes at the stuck position.
+ */
+bool extractAmigaMxmmScenePathfinding(const byte *mxmm, uint32 mxmmSize,
+ uint16 &outNumPoints,
+ Common::Array<AmigaPathfindingNode> &outNodes);
+
+/**
+ * Decode MXMM map chunks (320x200) after chunk0.
+ * Native order: Map0=depth (g_pSceneMap0Buffer), Map1=walkability
+ * (g_pSceneMap1Buffer). Map2 holds MXCC (hotspot RLE), not a 64000 shadow -
+ * use extractAmigaMxmmMxccHotspotMap for that. A third 64000 PP20 (rare) is
+ * returned as outShadow when present.
+ */
+bool extractAmigaMxmmSceneMaps(const byte *mxmm, uint32 mxmmSize,
+ Common::Array<byte> &outPathfinding,
+ Common::Array<byte> &outDepth,
+ Common::Array<byte> &outShadow);
+
+/**
+ * Extract hotspot color table from the MXMM trailer (0x22 bytes after pathfinding).
+ * Layout matches DOS scene+0x50C3: u16BE numHotspots + 0x20 bytes color words
+ * (color in the first byte of each pair / low byte once stored as uint16 on LE).
+ * MM_0004: num=7, colors 249,254,250,251,253,252,248 - same as DOS scene 5.
+ */
+bool extractAmigaMxmmSceneHotspotColors(const byte *mxmm, uint32 mxmmSize,
+ uint16 &outNumHotspots,
+ Common::Array<uint16> &outColorTable);
+
+/**
+ * Decode MXCC chunk (Map2 / g_pSceneMap2Buffer) to a 320x200 hotspot pixel map.
+ * Ghidra decodeMxccRunLengthAt @ 00233590: per-row RLE with marker at MXCC+5,
+ * row offsets at +0x0A (200xu16BE), row data at +0x19A + offset.
+ * MM_0004 decodes pixel-identical to DOS scene 5 hotspot RLE.
+ * No separate 4th 64000 map chunk exists in the Amiga demo format.
+ */
+bool extractAmigaMxmmMxccHotspotMap(const byte *mxmm, uint32 mxmmSize,
+ Common::Array<byte> &outHotspotMap);
+
+/**
+ * True if MXMM has a non-empty MXAA overlay-anim blob (native tickMxaaOverlayAnims).
+ * Demo MM_0004/MM_0040 have size 0 - no consumer wired yet.
+ */
+bool amigaMxmmHasMxaaOverlay(const byte *mxmm, uint32 mxmmSize);
+
+/**
+ * One decoded MXFF glyph (chunky 8bpp, color 0 = transparent).
+ * Ghidra drawText @ 00224492: charmap at +0x0E, widths at +0x8C, atlas at +0x10A.
+ */
+struct AmigaMxffGlyph {
+ byte ascii = 0;
+ uint16 width = 0;
+ uint16 height = 0;
+ Common::Array<byte> pixels; // width * height
+};
+
+/**
+ * Decode native Amiga MXFF font (FF_0000) into chunky glyphs for GlyphData.
+ *
+ * Header (BE): magic MXFF, ver=1 at +4, blit height at +6, atlas rows at +8,
+ * atlas width-pixels at +0xA (row bytes = value>>3), planes=6 at +0xC.
+ * Glyph index map: +0x0E (0x7E bytes for chars 0x20..).
+ * Advance widths: +0x8C (0x7E bytes).
+ * Planar atlas: +0x10A, rowBytes * atlasRows * 6 planes; cell X = glyphIndex * 16.
+ */
+bool decodeAmigaMxffFont(const byte *mxff, uint32 mxffSize, Common::Array<AmigaMxffGlyph> &outGlyphs);
+
+/**
+ * Extract unsigned 8-bit PCM (+ optional rate) from an MXOS container for MacsAudioStream.
+ * Returns false if magic/version/layout is not recognized.
+ * outRateHz is set from Paula period when present (NTSC clock / period), else 8000.
+ */
+bool extractAmigaMxosPcm(const byte *mxos, uint32 mxosSize, Common::Array<byte> &outPcm, uint16 &outRateHz);
+
+} // End of namespace Macs2
+
+#endif // MACS2_AMIGA_DECODE_H
diff --git a/engines/macs2/amiga_resources.cpp b/engines/macs2/amiga_resources.cpp
new file mode 100644
index 00000000000..04105dcb47b
--- /dev/null
+++ b/engines/macs2/amiga_resources.cpp
@@ -0,0 +1,794 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "macs2/macs2.h"
+
+#include "common/archive.h"
+#include "common/debug.h"
+#include "common/memstream.h"
+#include "common/system.h"
+#include "common/util.h"
+#include "graphics/managed_surface.h"
+
+#include "macs2/amiga_decode.h"
+#include "macs2/detection.h"
+#include "macs2/gameobjects.h"
+#include "macs2/macs2_constants.h"
+#include "macs2/view1.h"
+
+namespace Macs2 {
+
+
+bool Macs2Engine::loadAmigaSceneBackground(uint32 sceneResourceId) {
+ if (!_amigaArchive || sceneResourceId == 0 || sceneResourceId > 0xFFFF)
+ return false;
+ if (!_amigaArchive->hasResource(kAmigaResMM, (uint16)sceneResourceId))
+ return false;
+
+ Common::ScopedPtr<Common::SeekableReadStream> stream(
+ _amigaArchive->createReadStreamForResource(kAmigaResMM, (uint16)sceneResourceId));
+ if (!stream)
+ return false;
+
+ const uint32 size = (uint32)stream->size();
+ Common::Array<byte> mxmm;
+ mxmm.resize(size);
+ if (stream->read(mxmm.data(), size) != size)
+ return false;
+
+ Common::Array<byte> pixels;
+ byte paletteRgb[768];
+ uint colorCount = 0;
+ if (!decodeAmigaMxmmSceneBackground(mxmm.data(), size, pixels, paletteRgb, colorCount))
+ return false;
+ if (pixels.size() != (uint)kScreenWidth * kGameHeight || colorCount == 0)
+ return false;
+
+ for (int y = 0; y < kGameHeight; y++) {
+ for (int x = 0; x < kScreenWidth; x++)
+ _sceneBackground.setPixel(x, y, pixels[(uint)y * kScreenWidth + x]);
+ }
+
+ // _palVanilla holds raw 6-bit VGA values (fade math subtracts from these).
+ // _pal is the 8-bit display palette - must be expanded via applyPaletteDarkening().
+ memset(_pal, 0, sizeof(_pal));
+ memset(_palVanilla, 0, sizeof(_palVanilla));
+ for (uint i = 0; i < colorCount && i < 256; i++) {
+ const byte r8 = paletteRgb[i * 3 + 0];
+ const byte g8 = paletteRgb[i * 3 + 1];
+ const byte b8 = paletteRgb[i * 3 + 2];
+ _palVanilla[i * 3 + 0] = (byte)((r8 * 63) / 255);
+ _palVanilla[i * 3 + 1] = (byte)((g8 * 63) / 255);
+ _palVanilla[i * 3 + 2] = (byte)((b8 * 63) / 255);
+ }
+
+ // Keep Info UI chrome colors in the high VGA indices used by panel drawing.
+ if (_amigaArchive->getInfo().loaded) {
+ const AmigaInfoData &info = _amigaArchive->getInfo();
+ auto amiga12ToVga6 = [](uint16 rgb, byte &r6, byte &g6, byte &b6) {
+ const byte r4 = (rgb >> 8) & 0xF;
+ const byte g4 = (rgb >> 4) & 0xF;
+ const byte b4 = rgb & 0xF;
+ r6 = (byte)((r4 * 63) / 15);
+ g6 = (byte)((g4 * 63) / 15);
+ b6 = (byte)((b4 * 63) / 15);
+ };
+ for (uint i = 0; i < 16; i++) {
+ byte r6, g6, b6;
+ amiga12ToVga6(info.uiPaletteAmiga[i], r6, g6, b6);
+ const uint idx = 0xF0 + i;
+ if (idx >= 256)
+ break;
+ _palVanilla[idx * 3 + 0] = r6;
+ _palVanilla[idx * 3 + 1] = g6;
+ _palVanilla[idx * 3 + 2] = b6;
+ }
+ }
+ _amigaNativePlayfieldPalette = true;
+ // Portraits share playfield COLOR17..31 (copper high bank). Native copper
+ // already filled those slots; installAmigaPortraitPalette is a no-op here.
+ installAmigaPortraitPalette(true);
+ buildAmigaPanelRemapTable();
+ applyPaletteDarkening();
+
+ _depthMap.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
+ _pathfindingMap.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
+ _shadowMap.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
+ _hotspotMap.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
+
+ Common::Array<byte> pathMap, depthMap, shadowMap;
+ if (extractAmigaMxmmSceneMaps(mxmm.data(), size, pathMap, depthMap, shadowMap)) {
+ auto blitMap = [](Graphics::ManagedSurface &dest, const Common::Array<byte> &src) {
+ if (src.size() != (uint)kScreenWidth * kGameHeight)
+ return;
+ for (int y = 0; y < kGameHeight; y++) {
+ for (int x = 0; x < kScreenWidth; x++)
+ dest.setPixel(x, y, src[(uint)y * kScreenWidth + x]);
+ }
+ };
+ blitMap(_pathfindingMap, pathMap);
+ blitMap(_depthMap, depthMap);
+ blitMap(_shadowMap, shadowMap);
+ }
+
+ // Hotspot colors (trailer 0x22) + MXCC RLE map (Map2). No 4th 64000 hotspot
+ // chunk on Amiga - decodeMxccRunLengthAt expands MXCC to the DOS hotspot map.
+ _numHotspots = 0;
+ _hotspotColorTable.clear();
+ uint16 numHotspots = 0;
+ Common::Array<uint16> hotspotColors;
+ if (extractAmigaMxmmSceneHotspotColors(mxmm.data(), size, numHotspots, hotspotColors)) {
+ _numHotspots = numHotspots;
+ _hotspotColorTable = Common::move(hotspotColors);
+ }
+ Common::Array<byte> hotspotMap;
+ if (extractAmigaMxmmMxccHotspotMap(mxmm.data(), size, hotspotMap) &&
+ hotspotMap.size() == (uint)kScreenWidth * kGameHeight) {
+ for (int y = 0; y < kGameHeight; y++) {
+ for (int x = 0; x < kScreenWidth; x++)
+ _hotspotMap.setPixel(x, y, hotspotMap[(uint)y * kScreenWidth + x]);
+ }
+ }
+
+ _backgroundAnimations.clear();
+ _backgroundAnimationsBlobs.clear();
+ _mapImageFileOffset = 0;
+ _mapSubSceneTableFilePos = 0;
+ // Trailer 0x40+0x80 after walk params: native copies to 0024888c/00248954.
+ // Demo scenes are all zeros; no other xrefs in this binary - leave
+ // _sceneResourceOffsets alone (Amiga loads by archive type/id).
+ // MXAA: size word after Map2 is 0 on MM_0004/MM_0040; tickMxaaOverlayAnims
+ // has nothing to run until a scene ships a real MXAA blob.
+ if (amigaMxmmHasMxaaOverlay(mxmm.data(), size)) {
+ debugC(1, kDebugFilePath, "Amiga: MM_%04u has MXAA overlay data (not loaded yet)",
+ (uint)sceneResourceId);
+ }
+
+ // Pathfinding graph + walk depth/speed live in the MXMM trailer (Ghidra
+ // load_scene_mxmm @ 00221d90). Without nodes, calculatePath always fails and
+ // walkAlongPath cancels with finalDest=current - waitForWalk then completes
+ // immediately while the actor is still short of the script target.
+ pathfindingPoints.clear();
+ _numPathfindingPoints = 0;
+ uint16 numPfPoints = 0;
+ Common::Array<AmigaPathfindingNode> pfNodes;
+ if (extractAmigaMxmmScenePathfinding(mxmm.data(), size, numPfPoints, pfNodes)) {
+ _numPathfindingPoints = numPfPoints;
+ for (uint i = 0; i < pfNodes.size(); i++) {
+ PathfindingPoint current;
+ current._index = (uint16)i;
+ current._position.x = (int16)pfNodes[i].x;
+ current._position.y = (int16)pfNodes[i].y;
+ current._adjacentPoints.clear();
+ const uint16 nConn = MIN<uint16>(pfNodes[i].numConnections, 4);
+ for (uint j = 0; j < nConn; j++) {
+ if (pfNodes[i].adjacent[j] != 0)
+ current._adjacentPoints.push_back(pfNodes[i].adjacent[j]);
+ }
+ pathfindingPoints.push_back(current);
+ }
+ }
+
+ // Walk depth/speed percent (Ghidra g_abSceneWalkPaletteParams / DOS 0x51FD..).
+ // Leaving base at 0 makes walkAlongPath clamp to 1 px/frame and cancel early.
+ uint16 walkThreshY = 0, walkScale = 0, walkBasePct = 0, palMode = 0, darken = 0;
+ if (extractAmigaMxmmSceneWalkParams(mxmm.data(), size, walkThreshY, walkScale, walkBasePct, palMode,
+ darken)) {
+ _walkDepthThresholdY = walkThreshY;
+ _walkDepthScaleFactor = walkScale;
+ _walkBaseSpeedPct = walkBasePct;
+ _scenePaletteMode = palMode != 0 ? palMode : 1;
+ _paletteDarkenPercent = darken;
+ } else {
+ // Empty stubs (e.g. MM_0040) and DOS flat rooms use 100/100/100.
+ _walkDepthThresholdY = 100;
+ _walkDepthScaleFactor = 100;
+ _walkBaseSpeedPct = 100;
+ _scenePaletteMode = 1;
+ _paletteDarkenPercent = 0;
+ }
+
+ // Scene script/strings live in the MXMM trailer (not the global scene_table stub).
+ _amigaPendingSceneScript.clear();
+ _amigaPendingSceneStrings.clear();
+ extractAmigaMxmmSceneScript(mxmm.data(), size, _amigaPendingSceneScript, _amigaPendingSceneStrings);
+
+ debugC(1, kDebugFilePath,
+ "Amiga: loaded native MM_%04u (script %u bytes, strings %u bytes, pfNodes %u, "
+ "hotspots %u, walk %u/%u/%u)",
+ (uint)sceneResourceId, (uint)_amigaPendingSceneScript.size(),
+ (uint)_amigaPendingSceneStrings.size(), (uint)_numPathfindingPoints, (uint)_numHotspots,
+ (uint)_walkDepthThresholdY, (uint)_walkDepthScaleFactor, (uint)_walkBaseSpeedPct);
+ return true;
+}
+
+// --- loadAmigaMxffFont (macs2.cpp:2807-2866) ---
+bool Macs2Engine::loadAmigaMxffFont() {
+ if (!_amigaArchive || !_amigaArchive->hasResource(kAmigaResFF, 0))
+ return false;
+
+ Common::ScopedPtr<Common::SeekableReadStream> stream(
+ _amigaArchive->createReadStreamForResource(kAmigaResFF, 0));
+ if (!stream)
+ return false;
+
+ const uint32 size = (uint32)stream->size();
+ Common::Array<byte> mxff;
+ mxff.resize(size);
+ if (stream->read(mxff.data(), size) != size)
+ return false;
+
+ Common::Array<AmigaMxffGlyph> glyphs;
+ if (!decodeAmigaMxffFont(mxff.data(), size, glyphs) || glyphs.empty())
+ return false;
+
+ numGlyphs = 0;
+ maxGlyphHeight = 0;
+ amigaTextLinePitch = 0;
+ if (size >= 0x0A) {
+ const uint16 atlasRows = READ_BE_UINT16(mxff.data() + 8);
+ if (atlasRows > 1)
+ amigaTextLinePitch = (uint16)(atlasRows - 1);
+ }
+ for (uint i = 0; i < glyphs.size() && i < 256; i++) {
+ _glyphs[i]._ascii = glyphs[i].ascii;
+ _glyphs[i]._width = glyphs[i].width;
+ _glyphs[i]._height = glyphs[i].height;
+ _glyphs[i]._data = Common::move(glyphs[i].pixels);
+ // MXFF dialogue glyphs use copper COLOR23 (black) + COLOR27 (near-white
+ // outline). Remap into private UI bank 0xF0.. so outdoor copper cannot
+ // recolor text. COLOR17+i -> 0xF1+i (MXIN ui[1+i]); COLOR23->0xF7=ui[7]=0,
+ // COLOR27->0xFB=ui[11]=EEE. drawText @ 00224492 blits via drawSprite.
+ for (uint p = 0; p < _glyphs[i]._data.size(); p++) {
+ const byte c = _glyphs[i]._data[p];
+ if (c == 0)
+ continue;
+ if (c >= 17 && c <= 31)
+ _glyphs[i]._data[p] = (byte)(0xF0 + (c - 16));
+ else if (c < 16)
+ _glyphs[i]._data[p] = (byte)(0xF0 + c);
+ }
+ maxGlyphHeight = MAX(maxGlyphHeight, _glyphs[i]._height);
+ numGlyphs++;
+ }
+ if (amigaTextLinePitch == 0 && maxGlyphHeight > 1)
+ amigaTextLinePitch = (uint16)(maxGlyphHeight - 1);
+ // Reuse dialogue font for panel/save UI until a second MXFF exists.
+ numPanelGlyphs = numGlyphs;
+ maxPanelGlyphHeight = maxGlyphHeight;
+ for (uint i = 0; i < numGlyphs; i++)
+ _panelGlyphs[i] = _glyphs[i];
+
+ debugC(1, kDebugFilePath, "Amiga: loaded MXFF font FF_0000 (%u glyphs, height %u, linePitch %u)",
+ numGlyphs, maxGlyphHeight, amigaTextLinePitch);
+ return numGlyphs > 0;
+}
+
+bool Macs2Engine::loadAmigaOverlayFontResource(uint16 ffId) {
+ if (_amigaArchive == nullptr || !_amigaArchive->hasResource(kAmigaResFF, ffId))
+ return false;
+
+ Common::ScopedPtr<Common::SeekableReadStream> stream(
+ _amigaArchive->createReadStreamForResource(kAmigaResFF, ffId));
+ if (!stream)
+ return false;
+
+ const uint32 size = (uint32)stream->size();
+ Common::Array<byte> mxff;
+ mxff.resize(size);
+ if (stream->read(mxff.data(), size) != size)
+ return false;
+
+ Common::Array<AmigaMxffGlyph> glyphs;
+ if (!decodeAmigaMxffFont(mxff.data(), size, glyphs) || glyphs.empty())
+ return false;
+
+ numOverlayGlyphs = 0;
+ maxOverlayGlyphHeight = 0;
+ for (uint i = 0; i < glyphs.size() && i < 256; i++) {
+ _overlayGlyphs[i]._ascii = glyphs[i].ascii;
+ _overlayGlyphs[i]._width = glyphs[i].width;
+ _overlayGlyphs[i]._height = glyphs[i].height;
+ _overlayGlyphs[i]._data = Common::move(glyphs[i].pixels);
+ // Same copper->UI-bank remap as loadAmigaMxffFont.
+ for (uint p = 0; p < _overlayGlyphs[i]._data.size(); p++) {
+ const byte c = _overlayGlyphs[i]._data[p];
+ if (c == 0)
+ continue;
+ if (c >= 17 && c <= 31)
+ _overlayGlyphs[i]._data[p] = (byte)(0xF0 + (c - 16));
+ else if (c < 16)
+ _overlayGlyphs[i]._data[p] = (byte)(0xF0 + c);
+ }
+ maxOverlayGlyphHeight = MAX(maxOverlayGlyphHeight, _overlayGlyphs[i]._height);
+ numOverlayGlyphs++;
+ }
+ return numOverlayGlyphs > 0;
+}
+
+bool Macs2Engine::loadAmigaOverlayFont(uint8 resourceIndex) {
+ if (resourceIndex != 0 && _amigaArchive != nullptr) {
+ uint16 ffId = resourceIndex;
+ if (!_amigaArchive->hasResource(kAmigaResFF, ffId) && resourceIndex > 0)
+ ffId = (uint16)(resourceIndex - 1);
+ if (loadAmigaOverlayFontResource(ffId))
+ return true;
+ }
+
+ // Fall back to the already-loaded main MXFF dialogue font.
+ if (numGlyphs == 0)
+ return false;
+
+ numOverlayGlyphs = numGlyphs;
+ maxOverlayGlyphHeight = maxGlyphHeight;
+ for (uint i = 0; i < numGlyphs; i++)
+ _overlayGlyphs[i] = _glyphs[i];
+ return true;
+}
+
+// --- loadAmigaCursorResource (macs2.cpp:2868-2894) ---
+bool Macs2Engine::loadAmigaCursorResource(uint16 resourceId, AnimFrame &out) {
+ out = AnimFrame();
+ if (!_amigaArchive || resourceId == 0)
+ return false;
+
+ Common::ScopedPtr<Common::SeekableReadStream> stream(_amigaArchive->createReadStreamForResource(kAmigaResOO, resourceId));
+ if (!stream)
+ return false;
+
+ const uint32 size = (uint32)stream->size();
+ Common::Array<byte> data;
+ data.resize(size);
+ if (stream->read(data.data(), size) != size)
+ return false;
+
+ uint16 width = 0, height = 0;
+ Common::Array<byte> pixels;
+ if (!Macs2AmigaArchive::decodePlanarSprite(data.data(), size, width, height, pixels))
+ return false;
+
+ out._width = width;
+ out._height = height;
+ out._offsetX = 0;
+ out._offsetY = 0;
+ out._data = Common::move(pixels);
+ return true;
+}
+
+// --- installAmigaPortraitPalette (macs2.cpp:2896-2937) ---
+void Macs2Engine::installAmigaPortraitPalette(bool copyFromPlayfield) {
+ // Portraits now keep Amiga COLOR indices and share the playfield copper
+ // high bank (COLOR17..31), matching animateDialoguePortrait on hardware.
+ // Demo OO_* atlases never touch COLOR01..16.
+ //
+ // When native copper is not resident (pre-scene), seed
+ // COLOR17..31 from MXIN chrome so portraits are not left on the provisional
+ // ramp from applyAmigaUiPalette. copyFromPlayfield is ignored - live copper
+ // already owns those slots after loadAmigaSceneBackground.
+ (void)copyFromPlayfield;
+
+ if (_amigaNativePlayfieldPalette)
+ return;
+
+ auto amiga12ToVga6 = [](uint16 rgb, byte &r6, byte &g6, byte &b6) {
+ const byte r4 = (rgb >> 8) & 0xF;
+ const byte g4 = (rgb >> 4) & 0xF;
+ const byte b4 = rgb & 0xF;
+ r6 = (byte)((r4 * 63) / 15);
+ g6 = (byte)((g4 * 63) / 15);
+ b6 = (byte)((b4 * 63) / 15);
+ };
+
+ static const uint16 kHighBankFallback[15] = {
+ 0x0BBA, 0x0EB8, 0x0C96, 0x0A74, 0x0963, 0x0741, 0x0000, 0x049E,
+ 0x0C00, 0x0DDC, 0x0EEE, 0x0887, 0x0776, 0x0006, 0x0520
+ };
+
+ // Copper base16: COLOR00 + COLOR17..31 == MXIN ui[0..15].
+ const uint16 *highSrc = kHighBankFallback;
+ if (_amigaArchive && _amigaArchive->getInfo().loaded)
+ highSrc = &_amigaArchive->getInfo().uiPaletteAmiga[1];
+
+ for (uint i = 0; i < 15; i++) {
+ byte r6, g6, b6;
+ amiga12ToVga6(highSrc[i], r6, g6, b6);
+ const uint idx = 17 + i;
+ _palVanilla[idx * 3 + 0] = r6;
+ _palVanilla[idx * 3 + 1] = g6;
+ _palVanilla[idx * 3 + 2] = b6;
+ }
+}
+
+// --- applyAmigaUiPalette (macs2.cpp:2939-2990) ---
+void Macs2Engine::applyAmigaUiPalette() {
+ if (!_amigaArchive || !_amigaArchive->getInfo().loaded)
+ return;
+
+ const AmigaInfoData &info = _amigaArchive->getInfo();
+
+ auto amiga12ToVga6 = [](uint16 rgb, byte &r6, byte &g6, byte &b6) {
+ const byte r4 = (rgb >> 8) & 0xF;
+ const byte g4 = (rgb >> 4) & 0xF;
+ const byte b4 = rgb & 0xF;
+ r6 = (byte)((r4 * 63) / 15);
+ g6 = (byte)((g4 * 63) / 15);
+ b6 = (byte)((b4 * 63) / 15);
+ };
+
+ // Provisional playfield until an MM_* copper list is loaded.
+ // Copper base16 layout: COLOR00 + COLOR17..31 = MXIN ui[0..15]. COLOR01..16
+ // stay a visible ramp (overwritten per-line by scene copper).
+ byte r6, g6, b6;
+ amiga12ToVga6(info.uiPaletteAmiga[0], r6, g6, b6);
+ _palVanilla[0] = 0;
+ _palVanilla[1] = 0;
+ _palVanilla[2] = 0;
+ for (uint i = 1; i < 16; i++) {
+ const byte v = (byte)((i * 63) / 15);
+ _palVanilla[i * 3 + 0] = v;
+ _palVanilla[i * 3 + 1] = (byte)((v * 3) / 4);
+ _palVanilla[i * 3 + 2] = (byte)(v / 2);
+ }
+ for (uint i = 0; i < 15; i++) {
+ amiga12ToVga6(info.uiPaletteAmiga[i + 1], r6, g6, b6);
+ const uint idx = 17 + i;
+ _palVanilla[idx * 3 + 0] = r6;
+ _palVanilla[idx * 3 + 1] = g6;
+ _palVanilla[idx * 3 + 2] = b6;
+ }
+
+ // Amiga UI colors also live in the high indices used by panel/chrome drawing.
+ for (uint i = 0; i < 16; i++) {
+ amiga12ToVga6(info.uiPaletteAmiga[i], r6, g6, b6);
+ const uint idx = 0xF0 + i;
+ if (idx >= 256)
+ break;
+ _palVanilla[idx * 3 + 0] = r6;
+ _palVanilla[idx * 3 + 1] = g6;
+ _palVanilla[idx * 3 + 2] = b6;
+ }
+ _amigaNativePlayfieldPalette = false;
+ installAmigaPortraitPalette(false);
+ buildAmigaPanelRemapTable();
+ applyPaletteDarkening();
+}
+
+// --- buildAmigaPanelRemapTable (macs2.cpp:2992-3018) ---
+void Macs2Engine::buildAmigaPanelRemapTable() {
+ // Ghidra fill_ui_panel_darken_remap @ 002221fe:
+ // bucket = 7 - (R4+G4+B4)/0x18, then index g_awPanelDarkenColorIndices[bucket].
+ // Those indices are playfield copper slots on Amiga. ScummVM must not paint wood
+ // RGB into those slots (breaks intro/scene art). Instead map buckets onto the
+ // private MXIN UI bank already installed at 0xF0..0xFF (opaque brown ramp).
+ if (_panelRemapTable.size() != 0x100)
+ _panelRemapTable.resize(0x100);
+
+ // MXIN UI[0..5] = wood ramp (BBA..741). UI[6] is 0x000 - never use it for fill.
+ static const byte kUiWood[8] = {0, 1, 2, 3, 4, 5, 2, 3};
+ for (uint i = 0; i < 0x100; i++) {
+ const byte r6 = _palVanilla[i * 3 + 0];
+ const byte g6 = _palVanilla[i * 3 + 1];
+ const byte b6 = _palVanilla[i * 3 + 2];
+ const uint r4 = (r6 * 15) / 63;
+ const uint g4 = (g6 * 15) / 63;
+ const uint b4 = (b6 * 15) / 63;
+ const uint sum = r4 + g4 + b4;
+ int bucket = 7 - (int)(sum / 0x18);
+ if (bucket < 0)
+ bucket = 0;
+ if (bucket > 7)
+ bucket = 7;
+ _panelRemapTable[i] = (byte)(0xF0 + kUiWood[bucket]);
+ }
+}
+
+// --- readAmigaResources (macs2.cpp:3020-3317) ---
+void Macs2Engine::readAmigaResources() {
+ _amigaArchive = new Macs2AmigaArchive();
+ if (!_amigaArchive->open())
+ error("readAmigaResources(): Failed to open Amiga DataA/Mdir archive");
+
+ SearchMan.add("macs2amiga", _amigaArchive, 0, false);
+
+ Common::ScopedPtr<Common::SeekableReadStream> sceneTable(_amigaArchive->createSceneTableStream());
+ if (!sceneTable)
+ error("readAmigaResources(): Failed to decompress Amiga scene table");
+
+ // Keep the scene table resident for later Amiga scene loading work.
+ {
+ const uint32 size = (uint32)sceneTable->size();
+ byte *data = (byte *)malloc(size);
+ if (!data)
+ error("readAmigaResources(): Out of memory for scene table");
+ if (sceneTable->read(data, size) != size) {
+ free(data);
+ error("readAmigaResources(): Failed reading scene table");
+ }
+ _fileStream = new Common::MemoryReadStream(data, size, DisposeAfterUse::YES);
+ }
+
+ debugC(1, kDebugFilePath, "Amiga scene table loaded (%d bytes), %u scenes, %u archive members",
+ (int)_fileStream->size(), _amigaArchive->getSceneCount(), (uint)_amigaArchive->getResourceCount());
+
+ // Cursor / UI icon slots: DOS uses 33 entries; modes 0x13.. map to index mode-1.
+ _imageResources.clear();
+ _imageResources.resize(33);
+
+ const AmigaInfoData &info = _amigaArchive->getInfo();
+ auto tryLoadCursor = [this](uint16 resourceId) {
+ if (resourceId == 0 || resourceId > _imageResources.size())
+ return;
+ AnimFrame frame;
+ if (loadAmigaCursorResource(resourceId, frame))
+ _imageResources[resourceId - 1] = Common::move(frame);
+ };
+
+ if (info.loaded) {
+ for (uint i = 0; i < 5; i++)
+ tryLoadCursor(info.cursorResourceIds[i]);
+ tryLoadCursor(info.useInventoryCursorId);
+ // Panel / map cursor modes used by the engine (0x18, 0x19).
+ tryLoadCursor(0x18);
+ tryLoadCursor(0x19);
+ tryLoadCursor(0x1A);
+ } else {
+ // Fallback: load every small OO sprite whose id fits the cursor table.
+ for (uint16 id = 1; id <= 33; id++) {
+ if (_amigaArchive->hasResource(kAmigaResOO, id))
+ tryLoadCursor(id);
+ }
+ }
+
+ // Some cursor mode ids in Info are not standalone OO sprites in the demo.
+ // Duplicate the first loaded cursor into empty gameplay slots so the UI works.
+ int fallbackIndex = -1;
+ for (uint i = 0; i < _imageResources.size(); i++) {
+ if (!_imageResources[i]._data.empty() && _imageResources[i]._width > 0 &&
+ _imageResources[i]._height > 0) {
+ fallbackIndex = (int)i;
+ break;
+ }
+ }
+ if (fallbackIndex >= 0) {
+ static const uint16 kRequiredModes[] = {0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A};
+ for (uint16 mode : kRequiredModes) {
+ const uint idx = mode - 1;
+ if (idx < _imageResources.size() && _imageResources[idx]._data.empty())
+ _imageResources[idx] = _imageResources[fallbackIndex];
+ }
+ }
+
+ applyAmigaUiPalette();
+
+ // Native MXFF dialogue font (Ghidra drawText @ 00224492). Prefer over DOS MCS glyphs.
+ const bool loadedMxff = loadAmigaMxffFont();
+
+ // Load every OO resource as a GameObject. Object index = resource id + 1.
+ GameObjects::instance()._objects.resize(0x200, nullptr);
+ uint loadedObjects = 0;
+ uint loadedAnims = 0;
+
+ Common::ArchiveMemberList members;
+ _amigaArchive->listMembers(members);
+ for (const Common::ArchiveMemberPtr &member : members) {
+ AmigaResourceType type = kAmigaResUnknown;
+ uint16 resId = 0;
+ if (!Macs2AmigaArchive::parseResourceName(member->getName(), type, resId) || type != kAmigaResOO)
+ continue;
+
+ Common::ScopedPtr<Common::SeekableReadStream> stream(_amigaArchive->createReadStreamForResource(type, resId));
+ if (!stream)
+ continue;
+
+ const uint32 size = (uint32)stream->size();
+ Common::Array<byte> mxoo;
+ mxoo.resize(size);
+ if (stream->read(mxoo.data(), size) != size)
+ continue;
+
+ const uint16 objectIndex = (uint16)(resId + 1);
+ if (objectIndex == 0 || objectIndex > 0x200)
+ continue;
+
+ GameObject *gameObject = GameObjects::instance()._objects[objectIndex - 1];
+ if (gameObject == nullptr) {
+ gameObject = new GameObject();
+ gameObject->_index = objectIndex;
+ GameObjects::instance()._objects[objectIndex - 1] = gameObject;
+ }
+ // Non-zero marks the object as having resident Amiga data (skip DOS file reload).
+ gameObject->_dataOffset = 1;
+ gameObject->_position = Common::Point(0, 0);
+ gameObject->_sceneIndex = 0;
+ gameObject->_orientation = 11;
+ gameObject->_verticalOffsetScale = 0;
+ // MXOO has no DOS +0x185/+0x186 flag bytes. Defaults stay false until we
+ // infer character-style rendering below (or a script opcode sets them).
+
+ while (gameObject->_blobs.size() < 0x15)
+ gameObject->_blobs.push_back(Common::Array<uint8>());
+ while (gameObject->_blobSourceKeys.size() < 0x15)
+ gameObject->_blobSourceKeys.push_back(0);
+ while (gameObject->_blobWalkSpeeds.size() < 0x15)
+ gameObject->_blobWalkSpeeds.push_back(0);
+ while (gameObject->_blobMirrorFlags.size() < 0x15)
+ gameObject->_blobMirrorFlags.push_back(false);
+
+ AmigaMxooInfo mxooInfo;
+ if (parseAmigaMxoo(mxoo.data(), size, mxooInfo)) {
+ for (uint slot = 0; slot < 21; slot++) {
+ Common::Array<byte> blob;
+ if (!convertAmigaAnimSlotToDosBlob(mxoo.data(), size, mxooInfo.slotOffsets[slot], blob) || blob.empty())
+ continue;
+ if (slot < gameObject->_blobs.size())
+ gameObject->_blobs[slot] = Common::move(blob);
+ else
+ gameObject->_blobs.push_back(Common::move(blob));
+ loadedAnims++;
+ }
+
+ // Amiga MXOO omits mirrored walk/stand slots (Ghidra/demo OO_0000: slots
+ // 6-8 and 14-16 empty). DOS stores those slots with bMirrorFlag=1 and
+ // mirrorAnimBlob at load (MCSEXEC loadObjectData). Synthesize the same
+ // post-load state so walkAlongPath DirAvail and drawAllCharacters work.
+ // Walk speeds match DOS protagonist RESOURCE.MCS object 1:
+ // orients 1..8 -> 2,4,6,4,2,4,6,4
+ static const uint16 kAmigaWalkSpeeds[8] = {2, 4, 6, 4, 2, 4, 6, 4};
+ static const uint8 kMirrorOrientToSource[6][2] = {
+ {6, 4}, {7, 3}, {8, 2}, {14, 12}, {15, 11}, {16, 10}};
+ for (uint i = 0; i < 8; i++) {
+ if (i < gameObject->_blobWalkSpeeds.size())
+ gameObject->_blobWalkSpeeds[i] = kAmigaWalkSpeeds[i];
+ }
+ for (uint m = 0; m < 6; m++) {
+ const uint16 dstOrient = kMirrorOrientToSource[m][0];
+ const uint16 srcOrient = kMirrorOrientToSource[m][1];
+ const uint dstIdx = dstOrient - 1;
+ const uint srcIdx = srcOrient - 1;
+ if (dstIdx >= gameObject->_blobs.size() || srcIdx >= gameObject->_blobs.size())
+ continue;
+ if (!gameObject->_blobs[dstIdx].empty() || gameObject->_blobs[srcIdx].empty())
+ continue;
+ gameObject->_blobs[dstIdx] = gameObject->_blobs[srcIdx];
+ BackgroundAnimationBlob::mirrorAnimBlob(gameObject->_blobs[dstIdx]);
+ gameObject->_blobMirrorFlags[dstIdx] = true;
+ if (dstOrient <= 8 && srcOrient <= 8)
+ gameObject->_blobWalkSpeeds[dstIdx] = kAmigaWalkSpeeds[dstOrient - 1];
+ loadedAnims++;
+ }
+
+ extractAmigaScript(mxoo.data(), size, gameObject->_script);
+ extractAmigaStringBlock(mxoo.data(), size, gameObject->_stringData);
+
+ // Dialogue portraits: 240x80x6 atlas (3x80x80) -> blobs[17]/[18].
+ while (gameObject->_blobs.size() < 19)
+ gameObject->_blobs.push_back(Common::Array<uint8>());
+ Common::Array<byte> portraitBlob;
+ if (convertAmigaPortraitAtlasToDosBlob(mxoo.data(), size, mxooInfo.extraOffset, portraitBlob) &&
+ !portraitBlob.empty()) {
+ gameObject->_blobs[17] = portraitBlob;
+ gameObject->_blobs[18] = portraitBlob;
+ loadedAnims++;
+ }
+
+ // DOS MCS stores hasShading/hasScaling after the 21 anim slots (obj1/6/12/...
+ // = 1). Amiga MXOO omits those bytes; without them drawAllCharacters used
+ // the non-depth blit and actors drew over foreground depth (brown rocks).
+ // Match DOS character objects: walk anims and/or dialogue portrait.
+ uint walkSlots = 0;
+ for (uint s = 0; s < 8 && s < gameObject->_blobs.size(); s++) {
+ if (!gameObject->_blobs[s].empty())
+ walkSlots++;
+ }
+ const bool hasPortrait = !gameObject->_blobs[17].empty();
+ if (walkSlots >= 2 || hasPortrait) {
+ gameObject->_hasScaling = true;
+ gameObject->_hasShading = true;
+ }
+ } else {
+ // Simple planar sprite (cursor / inventory icon): expose as walk + inventory slots.
+ Common::Array<byte> blob;
+ if (convertAmigaSimpleSpriteToDosBlob(mxoo.data(), size, blob) && !blob.empty()) {
+ gameObject->_blobs[0] = blob;
+ gameObject->_blobs[0x13] = blob;
+ loadedAnims++;
+ }
+ }
+
+ loadedObjects++;
+ }
+
+ // Protagonist object slot (OO_0000 -> object 1). Do NOT place him in the room here -
+ // the intro/scene MXMM scripts call moveObject on scene init (same as DOS).
+ GameObject *protagonist = GameObjects::instance()._objects[0];
+ if (protagonist == nullptr) {
+ protagonist = new GameObject();
+ protagonist->_index = 1;
+ protagonist->_dataOffset = 1;
+ GameObjects::instance()._objects[0] = protagonist;
+ }
+ protagonist->_sceneIndex = 0;
+ protagonist->_position = Common::Point(0, 0);
+ protagonist->_orientation = 11;
+
+ Scenes::instance()._currentActorIndex = 1;
+
+ // Info MXIN u32 @ offset 8 = starting MM resource id (demo: 40 -> intro MM_0040).
+ // Script-visible scene id = MM id + 1 (Ghidra load_scene_mxmm / FUN_002215fa).
+ uint16 startResourceId = _amigaArchive->getInfo().startSceneResourceId;
+ if (startResourceId == 0 || !_amigaArchive->hasResource(kAmigaResMM, startResourceId)) {
+ if (_amigaArchive->hasResource(kAmigaResMM, 40))
+ startResourceId = 40;
+ else if (_amigaArchive->hasResource(kAmigaResMM, 4))
+ startResourceId = 4;
+ else
+ startResourceId = 1;
+ }
+ const uint16 startScriptScene = (uint16)(startResourceId + 1);
+ Scenes::instance()._currentSceneIndex = startScriptScene;
+
+ _sceneBackground.create(kScreenWidth, kGameHeight, Graphics::PixelFormat::createFormatCLUT8());
+ _depthMap.create(kScreenWidth, kGameHeight, Graphics::PixelFormat::createFormatCLUT8());
+ _pathfindingMap.create(kScreenWidth, kGameHeight, Graphics::PixelFormat::createFormatCLUT8());
+ _shadowMap.create(kScreenWidth, kGameHeight, Graphics::PixelFormat::createFormatCLUT8());
+ _hotspotMap.create(kScreenWidth, kGameHeight, Graphics::PixelFormat::createFormatCLUT8());
+ _shadingTable.resize(0x800);
+ Common::fill(_shadingTable.begin(), _shadingTable.end(), 0);
+ _panelRemapTable.resize(0x100);
+ buildAmigaPanelRemapTable();
+
+ _numHotspots = 0;
+ _numPathfindingPoints = 0;
+ _scenePaletteMode = 1;
+ _paletteDarkenPercent = 0;
+
+ // Fonts: MXFF dialogue font from DataA (standalone Amiga demo - no DOS MCS).
+ if (!loadedMxff)
+ warning("Amiga: no MXFF font FF_0000 in DataA - text may be missing");
+
+ // Border chrome uses fixed slots 30/31/32. Empty/zero-size tiles make
+ // drawBorderSide spin forever (no events, frameWait appears stuck).
+ auto ensureBorderTile = [this](uint index, byte color, uint16 w, uint16 h) {
+ if (index >= _imageResources.size())
+ return;
+ if (!_imageResources[index]._data.empty() && _imageResources[index]._width > 0 &&
+ _imageResources[index]._height > 0)
+ return;
+ AnimFrame tile;
+ tile._width = w;
+ tile._height = h;
+ tile._data.resize((uint)w * h);
+ Common::fill(tile._data.begin(), tile._data.end(), color);
+ _imageResources[index] = Common::move(tile);
+ };
+ if (_imageResources.size() < 33)
+ _imageResources.resize(33);
+ ensureBorderTile(30, 0xF2, 8, 1);
+ ensureBorderTile(31, 0xF1, 8, 8);
+ ensureBorderTile(32, 0xF3, 8, 1);
+
+ debugC(1, kDebugFilePath,
+ "Amiga objects loaded: %u resources, %u anim slots; start MM_%04u (script scene %u)",
+ loadedObjects, loadedAnims, startResourceId, startScriptScene);
+ changeScene(Scenes::instance()._currentSceneIndex);
+}
+
+} // namespace Macs2
diff --git a/engines/macs2/gameobjects.cpp b/engines/macs2/gameobjects.cpp
index 28e900f95c8..80eacbd8d48 100644
--- a/engines/macs2/gameobjects.cpp
+++ b/engines/macs2/gameobjects.cpp
@@ -385,6 +385,17 @@ Macs2::GameObject *Macs2::GameObjects::getObjectByIndex(uint16 index) {
}
Common::MemoryReadStream *Macs2::GameObjects::readGameObjectStrings(uint16 index, Common::SeekableReadStream *fileStream) {
+ // Amiga: strings live on the GameObject itself (plaintext, u16BE lengths).
+ if (g_engine->isAmiga()) {
+ GameObject *obj = getObjectByIndex(index);
+ if (obj == nullptr)
+ return new Common::MemoryReadStream(nullptr, 0);
+ byte *copy = (byte *)malloc(obj->_stringData.size());
+ if (!obj->_stringData.empty())
+ memcpy(copy, obj->_stringData.data(), obj->_stringData.size());
+ return new Common::MemoryReadStream(copy, obj->_stringData.size(), DisposeAfterUse::YES);
+ }
+
const uint32 directoryOffset = g_engine->getMcsDirectoryOffset();
// Object string table pointer at directory + index*0xC + 0x17FC.
fileStream->seek(directoryOffset + index * 0xC + 0x17FC);
diff --git a/engines/macs2/gameobjects.h b/engines/macs2/gameobjects.h
index 2fc91936bb8..9864c6d7392 100644
--- a/engines/macs2/gameobjects.h
+++ b/engines/macs2/gameobjects.h
@@ -176,6 +176,10 @@ public:
// The object-specific script
Common::Array<uint8> _script;
+ // Amiga: plaintext string entries for this object (script offsets are relative to this).
+ // Empty on DOS; DOS strings are read from RESOURCE.MCS via the scene/object tables.
+ Common::Array<uint8> _stringData;
+
// Per-object resource offset table (runtime +0x18D, 128 bytes = 32 dword file offsets).
// Loaded from file during loadObjectData. Used by scriptLoadObjectAnim/scriptLoadSpecialAnim
// to look up animation resource file addresses for this object.
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index fbbc5feb388..f3144d62429 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -41,6 +41,7 @@
#include "graphics/pixelformat.h"
#include "graphics/surface.h"
#include "gui/debugger.h"
+#include "macs2/amiga_decode.h"
#include "macs2/debugtools.h"
#include "macs2/detection.h"
#include "macs2/hotspot_names.h"
@@ -331,6 +332,13 @@ void Macs2Engine::readExecutable() {
exeFileStream->read(containerInventoryIconIndices.data(), 12);
}
+void Macs2Engine::loadBootstrapResources() {
+ if (isAmiga())
+ readAmigaResources();
+ else
+ readResourceFile();
+}
+
void Macs2Engine::readBackgroundAnimations(Common::MemoryReadStream *stream) {
// changeScene (1008:2574): background animation loading at scene+0x50F5.
// Per-entry runtime struct (0x10 bytes stride):
@@ -427,6 +435,11 @@ Macs2Engine::~Macs2Engine() {
clearCurrentSoundData();
_music->deinit();
delete _music;
+ if (_amigaArchive) {
+ SearchMan.remove("macs2amiga");
+ delete _amigaArchive;
+ _amigaArchive = nullptr;
+ }
delete _fileStream;
_fileStream = nullptr;
delete _scriptExecutor;
@@ -476,6 +489,32 @@ uint16 Macs2Engine::scaledMusicVolume(uint16 gameAttenuation) const {
}
bool Macs2Engine::loadSceneGraphics(uint32 sceneIndex) {
+ if (isAmiga()) {
+ uint32 sceneResourceId = 0;
+ if (sceneIndex > 0)
+ sceneResourceId = sceneIndex - 1;
+
+ if (sceneResourceId != 0 && loadAmigaSceneBackground(sceneResourceId))
+ return true;
+
+ _sceneBackground.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
+ _depthMap.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
+ _pathfindingMap.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
+ _shadowMap.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
+ _hotspotMap.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
+ _numHotspots = 0;
+ _numPathfindingPoints = 0;
+ _walkDepthThresholdY = 100;
+ _walkDepthScaleFactor = 100;
+ _walkBaseSpeedPct = 100;
+ _scenePaletteMode = 1;
+ _paletteDarkenPercent = 0;
+ applyAmigaUiPalette();
+ warning("Amiga: no MM_%04u in DataA (script scene %u)",
+ (uint)sceneResourceId, (uint)sceneIndex);
+ return true;
+ }
+
const uint32 newSceneIndex = sceneIndex;
// Background image
@@ -652,6 +691,134 @@ void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
_backgroundAnimationsBlobs.clear();
memset(_areaOverrides, 0, sizeof(_areaOverrides));
+ if (isAmiga()) {
+ // Amiga scripts use scene ids = MM_resource_id + 1 (Ghidra FUN_002215fa
+ // subtracts 1 before load_scene_mxmm; load_scene sets curScene = mmId+1).
+ _amigaPendingSceneScript.clear();
+ _amigaPendingSceneStrings.clear();
+
+ if (!loadSceneGraphics(newSceneIndex))
+ error("changeScene(): Failed to load scene graphics for scene %u", newSceneIndex);
+
+ View1 *currentView = (View1 *)findView("View1");
+ if (currentView != nullptr) {
+ // Do not push _pal here: scriptChangeScene fades the previous
+ // palette to black, then fades the new scene in.
+ for (auto currentCharacter : currentView->_characters) {
+ if (currentCharacter->_gameObject != nullptr)
+ _scriptExecutor->saveWalkRuntime(currentCharacter, currentCharacter->_gameObject);
+ delete currentCharacter;
+ }
+ currentView->_characters.clear();
+ currentView->flushPendingCharacterDeletes();
+
+ GameObject *actorObject = GameObjects::getObjectByIndex(Scenes::instance()._currentActorIndex);
+ if (actorObject != nullptr && actorObject->_sceneIndex == newSceneIndex) {
+ Character *actorChar = new Character();
+ actorChar->_gameObject = actorObject;
+ currentView->_characters.push_back(actorChar);
+ _scriptExecutor->restoreWalkRuntime(actorChar, actorObject);
+ resetCharacterWalkPath(actorChar);
+ _scriptExecutor->saveWalkRuntime(actorChar, actorObject);
+ }
+ for (auto currentObject : GameObjects::instance()._objects) {
+ if (currentObject == nullptr)
+ continue;
+ if (currentObject->_sceneIndex == newSceneIndex &&
+ currentObject->_index != Scenes::instance()._currentActorIndex &&
+ currentObject->_dataOffset != 0) {
+ Character *c = new Character();
+ c->_gameObject = currentObject;
+ currentView->_characters.push_back(c);
+ _scriptExecutor->restoreWalkRuntime(c, currentObject);
+ resetCharacterWalkPath(c);
+ _scriptExecutor->saveWalkRuntime(c, currentObject);
+ }
+ }
+ currentView->rebuildCharacterLookupTable();
+ currentView->_backgroundSurface.copyFrom(_sceneBackground);
+ if (executeScript)
+ currentView->_paletteDirty = true;
+ }
+
+ Scenes::instance()._lastSceneIndex = Scenes::instance()._currentSceneIndex;
+ Scenes::instance()._currentSceneIndex = newSceneIndex;
+ _scriptExecutor->releaseObjectStream();
+ delete Scenes::instance()._currentSceneScript;
+ delete Scenes::instance()._currentSceneStrings;
+
+ byte *scriptCopy = nullptr;
+ uint32 scriptSize = 0;
+ byte *stringCopy = nullptr;
+ uint32 stringSize = 0;
+
+ if (!_amigaPendingSceneScript.empty()) {
+ scriptSize = _amigaPendingSceneScript.size();
+ scriptCopy = (byte *)malloc(scriptSize);
+ if (scriptCopy)
+ memcpy(scriptCopy, _amigaPendingSceneScript.data(), scriptSize);
+ else
+ scriptSize = 0;
+ }
+ if (!_amigaPendingSceneStrings.empty()) {
+ stringSize = _amigaPendingSceneStrings.size();
+ stringCopy = (byte *)malloc(stringSize);
+ if (stringCopy)
+ memcpy(stringCopy, _amigaPendingSceneStrings.data(), stringSize);
+ else
+ stringSize = 0;
+ }
+ _amigaPendingSceneScript.clear();
+ _amigaPendingSceneStrings.clear();
+
+ // Fallback: global scene_table MXOO stub (usually just opcode 0x18 in the demo).
+ if (scriptSize == 0 && _fileStream != nullptr) {
+ AmigaMxooInfo sceneInfo;
+ const uint32 tableSize = (uint32)_fileStream->size();
+ Common::Array<byte> table;
+ table.resize(tableSize);
+ _fileStream->seek(0);
+ if (_fileStream->read(table.data(), tableSize) == tableSize &&
+ parseAmigaMxoo(table.data(), tableSize, sceneInfo)) {
+ Common::Array<byte> script;
+ Common::Array<byte> strings;
+ if (extractAmigaScript(table.data(), tableSize, script) && !script.empty()) {
+ scriptSize = script.size();
+ scriptCopy = (byte *)malloc(scriptSize);
+ if (scriptCopy)
+ memcpy(scriptCopy, script.data(), scriptSize);
+ else
+ scriptSize = 0;
+ }
+ if (stringSize == 0 && extractAmigaStringBlock(table.data(), tableSize, strings)) {
+ stringSize = strings.size();
+ stringCopy = (byte *)malloc(stringSize);
+ if (stringCopy && stringSize)
+ memcpy(stringCopy, strings.data(), stringSize);
+ else
+ stringSize = 0;
+ }
+ }
+ }
+
+ Scenes::instance()._currentSceneScript = new Common::MemoryReadStream(scriptCopy, scriptSize, DisposeAfterUse::YES);
+ Scenes::instance()._currentSceneStrings = new Common::MemoryReadStream(stringCopy, stringSize, DisposeAfterUse::YES);
+ Scenes::instance()._currentSceneSpecialAnimOffsets.clear();
+ _scriptExecutor->setScript(Scenes::instance()._currentSceneScript);
+
+ _pathfindingOverrides.clear();
+ for (uint i = 0; i < _hotspotOverrides.size(); i++)
+ _hotspotOverrides[i] = 0xFFFF;
+
+ // Match DOS changeScene: when View1 is not up yet (first call from
+ // readAmigaResources), only load scene data. Entry init/repeat â including
+ // intro frameWait/changeScene â must run from View1::tick so waits can
+ // complete in the game loop.
+ if (executeScript && currentView != nullptr)
+ _scriptExecutor->runSceneEntryScriptPasses();
+ return;
+ }
+
if (!loadSceneGraphics(newSceneIndex))
error("changeScene(): Failed to load scene graphics for scene %u", newSceneIndex);
@@ -744,6 +911,9 @@ void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
}
bool Macs2Engine::loadOverlayFont(uint8 resourceIndex, uint16 executingObjectID) {
+ if (isAmiga())
+ return loadAmigaOverlayFont(resourceIndex);
+
// Original (1008:d749): looks up file offset from scene/object resource table
// at scene+0x5209+index*4 (same table as loadIndexedResource/_sceneResourceOffsets),
// seeks to offset+0x10, then calls loadFontData.
@@ -776,7 +946,7 @@ bool Macs2Engine::loadOverlayFont(uint8 resourceIndex, uint16 executingObjectID)
// Seek to address + 0x10 (original skips 16-byte resource header)
_fileStream->seek(address + 0x10, SEEK_SET);
- uint16 glyphCount = _fileStream->readUint16LE();
+ const uint16 glyphCount = _fileStream->readUint16LE();
if (glyphCount == 0 || glyphCount > 256) {
_fileStream->seek(oldPos, SEEK_SET);
return false;
@@ -1861,6 +2031,17 @@ bool Macs2Engine::loadObjectData(GameObject *obj) {
_scriptExecutor->setScriptError(0x19);
return false;
}
+ // Amiga: OO_* animation/script payloads stay resident after readAmigaResources().
+ // _dataOffset is a non-zero sentinel (not an MCS file offset). Opcode 0x0b
+ // (moveObject) and checkObjectData call this when an object enters the scene;
+ // seeking into RESOURCE.MCS would set script error 0x01.
+ if (isAmiga()) {
+ if (obj->_dataOffset == 0) {
+ _scriptExecutor->setScriptError(0x19);
+ return false;
+ }
+ return true;
+ }
if (obj->_dataOffset == 0) {
_scriptExecutor->setScriptError(0x19);
return false;
@@ -1988,14 +2169,18 @@ bool Macs2Engine::loadObjectData(GameObject *obj) {
return true;
}
-void Macs2Engine::setCurrentSoundData(const Common::Array<uint8> &data) {
+void Macs2Engine::setCurrentSoundData(const Common::Array<uint8> &data, int rateHz, int headerSkip) {
stopSample();
_currentSoundData = data;
+ _currentSoundRate = rateHz > 0 ? rateHz : 0x1F40;
+ _currentSoundHeaderSkip = headerSkip >= 0 ? headerSkip : 0;
}
void Macs2Engine::clearCurrentSoundData() {
stopSample();
_currentSoundData.clear();
+ _currentSoundRate = 0x1F40;
+ _currentSoundHeaderSkip = 2;
}
void Macs2Engine::playSample() {
@@ -2004,7 +2189,11 @@ void Macs2Engine::playSample() {
stopSample();
MacsAudioStream *audioStream = new MacsAudioStream();
- audioStream->_pos = 2; // Skip 2-byte header (original: size = stored_size - 2)
+ audioStream->_rate = _currentSoundRate;
+ // DOS: skip 2-byte size header. Amiga MXOS extract is raw PCM (skip 0).
+ audioStream->_pos = _currentSoundHeaderSkip;
+ if (audioStream->_pos > (int64)_currentSoundData.size())
+ audioStream->_pos = (int64)_currentSoundData.size();
audioStream->_data = _currentSoundData;
g_system->getMixer()->playStream(Audio::Mixer::kSFXSoundType, &_currentSoundHandle, audioStream);
}
@@ -2357,7 +2546,7 @@ bool MacsAudioStream::isStereo() const {
}
int MacsAudioStream::getRate() const {
- return 0x1F40;
+ return _rate > 0 ? _rate : 0x1F40;
}
bool MacsAudioStream::endOfData() const {
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index d6b598ae18f..39d8d2e352d 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -38,6 +38,7 @@
#include "common/text-to-speech.h"
#include "common/util.h"
#include "engines/engine.h"
+#include "macs2/amiga_archive.h"
#include "macs2/events.h"
#include "macs2/macs2_constants.h"
#include "macs2/scriptexecutor.h"
@@ -47,7 +48,9 @@ namespace Macs2 {
class MacsAudioStream : public Audio::SeekableAudioStream {
public:
Common::Array<byte> _data;
- int64 _pos;
+ int64 _pos = 0;
+ /** Playback rate in Hz (DOS SB samples are 8000; Amiga MXOS uses Paula period). */
+ int _rate = 0x1F40;
virtual ~MacsAudioStream() {}
@@ -253,6 +256,35 @@ public:
Graphics::ManagedSurface readRLEImage(int64 offs, Common::MemoryReadStream *stream);
void readResourceFile();
+ /** Amiga: open DataA/Mdir, load OO objects as GameObjects, cursors, and scene stubs. */
+ void readAmigaResources();
+ void applyAmigaUiPalette();
+ /**
+ * Ensure Amiga dialogue portraits can use playfield COLOR17..31.
+ * After native MM copper load those slots are already correct; otherwise
+ * seed them from MXIN chrome (copper base16 layout).
+ */
+ void installAmigaPortraitPalette(bool copyFromPlayfield);
+ /**
+ * Build _panelRemapTable from luminance buckets (Ghidra fill_ui_panel_darken_remap
+ * @ 002221fe). Outputs into private UI bank 0xF0.. so playfield/intro colors
+ * at MXIN darken indices are never overwritten.
+ */
+ void buildAmigaPanelRemapTable();
+ bool loadAmigaCursorResource(uint16 resourceId, AnimFrame &out);
+ /** Load FF_0000 MXFF into `_glyphs` (Ghidra drawText / g_pFont1Data). */
+ bool loadAmigaMxffFont();
+ /** Opcode 0x38 overlay font: FF_* from DataA, else copy the main MXFF glyphs. */
+ bool loadAmigaOverlayFont(uint8 resourceIndex);
+ /** Load one FF_* MXFF into `_overlayGlyphs`. Returns false if missing/undecodable. */
+ bool loadAmigaOverlayFontResource(uint16 ffId);
+ /**
+ * Amiga: load native MM_* MXMM package by resource id (not script scene id).
+ * Script-visible scene ids are resourceId+1 (Ghidra FUN_002215fa / load_scene_mxmm).
+ * Also extracts trailer script/strings into _amigaPendingScene* for changeScene.
+ * Palette indices 0..31 stay Amiga COLOR registers for OO sprite compatibility.
+ */
+ bool loadAmigaSceneBackground(uint32 sceneResourceId);
// We also need some data from the executable, specifically embedded
// Adlib data
@@ -372,6 +404,13 @@ public:
/** Amiga MXFF line pitch: measureTextWidth @ 00224420 uses (font[+8] - 1). */
uint16 amigaTextLinePitch = 0;
+ /** True after loadAmigaSceneBackground installed copper colors in 0..31. */
+ bool _amigaNativePlayfieldPalette = false;
+ /** Amiga DataA/Mdir archive (owned). Null on DOS. */
+ Macs2AmigaArchive *_amigaArchive = nullptr;
+ /** Filled by loadAmigaSceneBackground; consumed by Amiga changeScene. */
+ Common::Array<byte> _amigaPendingSceneScript;
+ Common::Array<byte> _amigaPendingSceneStrings;
void setCursorMode(Script::MouseMode newMode);
void nextCursorMode();
@@ -420,7 +459,13 @@ public:
Music *getMusic() const { return _music; }
// Returns the Music volume (0-63) scaled by the user's music_volume setting
uint16 scaledMusicVolume(uint16 gameAttenuation) const;
- void setCurrentSoundData(const Common::Array<uint8> &data);
+ /**
+ * Install PCM for opcode 0x3E / playSample.
+ * @param rateHz sample rate (DOS Sound Blaster path uses 8000)
+ * @param headerSkip bytes to skip at start of buffer (DOS resources have a 2-byte size header;
+ * Amiga MXOS extract is raw PCM and uses 0)
+ */
+ void setCurrentSoundData(const Common::Array<uint8> &data, int rateHz = 0x1F40, int headerSkip = 2);
void clearCurrentSoundData();
bool hasCurrentSoundData() const { return !_currentSoundData.empty(); }
void playSample();
@@ -480,6 +525,8 @@ public:
bool readInputFrame(uint16 &mouseX, uint16 &mouseY, uint16 &buttons);
Common::Array<uint8> _currentSoundData;
+ int _currentSoundRate = 0x1F40;
+ int _currentSoundHeaderSkip = 2;
Audio::SoundHandle _currentSoundHandle;
// Schedules a run of the script the next time the executor is ticked
@@ -509,6 +556,7 @@ public:
uint32 getFeatures() const;
Common::Platform getPlatform() const { return _gameDescription->platform; }
bool isAmiga() const { return getPlatform() == Common::kPlatformAmiga; }
+ Macs2AmigaArchive *getAmigaArchive() const { return _amigaArchive; }
bool isDemo() const { return getFeatures() & ADGF_DEMO; }
@@ -568,11 +616,11 @@ public:
/** Depth-map compare Y for sprite occlusion (full Y on DOS). */
uint8 depthThresholdForY(int16 charY) const { return (uint8)charY; }
- /** Resource bootstrap (MCS fonts/cursors/shading + first scene). */
- void loadBootstrapResources() { readResourceFile(); }
+ /** Resource bootstrap (DOS MCS or Amiga DataA/Mdir). */
+ void loadBootstrapResources();
/**
* Load scene background, maps, pathfinding, and related scene tables.
- * Called from changeScene; platforms can replace this body later.
+ * Called from changeScene; Amiga uses native MXMM from DataA.
*/
bool loadSceneGraphics(uint32 sceneIndex);
diff --git a/engines/macs2/module.mk b/engines/macs2/module.mk
index 7611e9f2071..84a299ad07a 100644
--- a/engines/macs2/module.mk
+++ b/engines/macs2/module.mk
@@ -1,6 +1,9 @@
MODULE := engines/macs2
MODULE_OBJS = \
+ amiga_archive.o \
+ amiga_decode.o \
+ amiga_resources.o \
midiparser_macs2.o \
music.o \
dialogs.o \
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 5669eefa209..24404491fb3 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -22,6 +22,8 @@
#include "macs2/scriptexecutor.h"
#include "common/debug.h"
#include "common/memstream.h"
+#include "macs2/amiga_archive.h"
+#include "macs2/amiga_decode.h"
#include "macs2/debugtools.h"
#include "macs2/detection.h"
#include "macs2/gameobjects.h"
@@ -59,7 +61,7 @@ ScriptExecutor::ScriptExecutor() {
_variables[i].b = 0;
}
- setOpcodeTable(ScriptExecutor::kDosOpcodeTable, ScriptExecutor::kDosOpcodeTableSize);
+ setOpcodeTable(ScriptExecutor::kV1OpcodeTable, ScriptExecutor::kV1OpcodeTableSize);
}
ScriptExecutor::~ScriptExecutor() {
@@ -356,11 +358,53 @@ bool ScriptExecutor::loadIndexedResource(Common::Array<uint8> &outData, uint8 re
return !outData.empty();
}
-bool ScriptExecutor::loadSoundResource(Common::Array<uint8> &outData, uint8 resourceIndex) {
+bool ScriptExecutor::loadSoundResource(Common::Array<uint8> &outData, uint8 resourceIndex,
+ int &rateHz, int &headerSkip) {
+ rateHz = 0x1F40;
+ headerSkip = 2;
+
+ if (_engine != nullptr && _engine->isAmiga()) {
+ outData.clear();
+ headerSkip = 0;
+ if (resourceIndex == 0 || _engine->getAmigaArchive() == nullptr)
+ return false;
+
+ Macs2AmigaArchive *archive = _engine->getAmigaArchive();
+ // Scripts index OS_* by the same 1-based resource id used in Mdir.
+ uint16 osId = resourceIndex;
+ if (!archive->hasResource(kAmigaResOS, osId)) {
+ if (resourceIndex > 0 && archive->hasResource(kAmigaResOS, (uint16)(resourceIndex - 1)))
+ osId = (uint16)(resourceIndex - 1);
+ else
+ return false;
+ }
+
+ Common::ScopedPtr<Common::SeekableReadStream> stream(
+ archive->createReadStreamForResource(kAmigaResOS, osId));
+ if (!stream)
+ return false;
+ const uint32 size = (uint32)stream->size();
+ Common::Array<byte> mxos;
+ mxos.resize(size);
+ if (stream->read(mxos.data(), size) != size)
+ return false;
+
+ uint16 rate = 8000;
+ if (!extractAmigaMxosPcm(mxos.data(), size, outData, rate) || outData.empty())
+ return false;
+ rateHz = (int)rate;
+ return true;
+ }
+
return loadIndexedResource(outData, resourceIndex);
}
bool ScriptExecutor::loadMusicResource(Common::Array<uint8> &outData, uint8 resourceIndex) {
+ // Amiga DataA has no AdLib/Protracker song blobs (MM_* are scene packages).
+ if (_engine != nullptr && _engine->isAmiga()) {
+ outData.clear();
+ return true;
+ }
return loadIndexedResource(outData, resourceIndex);
}
@@ -2647,13 +2691,15 @@ void Script::ScriptExecutor::scriptFadeFromBlack() {
void Script::ScriptExecutor::scriptLoadPcmSound() {
const uint8 resourceIndex = readByte();
Common::Array<uint8> soundData;
- if (!loadSoundResource(soundData, resourceIndex)) {
+ int rateHz = 0x1F40;
+ int headerSkip = 2;
+ if (!loadSoundResource(soundData, resourceIndex, rateHz, headerSkip)) {
warning("Opcode 0x3E: failed to load PCM sound resource %u", resourceIndex);
return;
}
// Opcode 0x3E only installs the buffer; playback is opcode 0x40.
- _engine->setCurrentSoundData(soundData);
+ _engine->setCurrentSoundData(soundData, rateHz, headerSkip);
}
void Script::ScriptExecutor::scriptPlayPcmSound() {
@@ -3367,9 +3413,9 @@ OpcodeResult ScriptExecutor::opWaitForAdlib() {
return OpcodeResult::Continue;
}
-// DOS script opcode table (indexed by primary opcode byte).
-// Platforms can install an alternate table via setOpcodeTable().
-const ScriptExecutor::OpcodeEntry ScriptExecutor::kDosOpcodeTable[] = {
+// Script dialect v1 opcode table (indexed by primary opcode byte).
+// Later dialects can install an alternate table via setOpcodeTable().
+const ScriptExecutor::OpcodeEntry ScriptExecutor::kV1OpcodeTable[] = {
{nullptr, nullptr},
{"setVar", &ScriptExecutor::opSetVar},
{"setVarOr", &ScriptExecutor::opSetVarOr},
@@ -3450,7 +3496,7 @@ const ScriptExecutor::OpcodeEntry ScriptExecutor::kDosOpcodeTable[] = {
{"setPathfindingRemap", &ScriptExecutor::opSetPathfindingRemap},
{"waitForAdlib", &ScriptExecutor::opWaitForAdlib}
};
-const uint ScriptExecutor::kDosOpcodeTableSize = ARRAYSIZE(ScriptExecutor::kDosOpcodeTable);
+const uint ScriptExecutor::kV1OpcodeTableSize = ARRAYSIZE(ScriptExecutor::kV1OpcodeTable);
ExecutionResult Script::ScriptExecutor::executeOpcodes() {
debugC(kDebugScript, "----- Scripting function entered - scene: %.2x 1014: %.2x 1012: %.2x", Scenes::instance()._currentSceneIndex, _isSceneInitRun, _repeatRunFlag);
diff --git a/engines/macs2/scriptexecutor.h b/engines/macs2/scriptexecutor.h
index 463b952d7d5..59af9b705bf 100644
--- a/engines/macs2/scriptexecutor.h
+++ b/engines/macs2/scriptexecutor.h
@@ -120,14 +120,14 @@ public:
OpcodeHandler handler;
};
- /** Install the active opcode table (DOS today; other platforms later). */
+ /** Install the active opcode table (use when a platform/version remaps opcodes). */
void setOpcodeTable(const OpcodeEntry *table, uint size);
/** Name for opcode, or "?" if unset/out of range. */
const char *opcodeName(uint8 opcode) const;
- /** Default DOS opcode table (class member so private handlers are accessible). */
- static const OpcodeEntry kDosOpcodeTable[];
- static const uint kDosOpcodeTableSize;
+ /** Script dialect v1 opcode table (MCS / Amiga demo bytecode). */
+ static const OpcodeEntry kV1OpcodeTable[];
+ static const uint kV1OpcodeTableSize;
private:
#ifdef DEMACS2
@@ -369,7 +369,13 @@ private:
void scriptSkipAlternate();
bool loadIndexedResource(Common::Array<uint8> &outData, uint8 resourceIndex, uint16 objectTableOffset = 0x189);
- bool loadSoundResource(Common::Array<uint8> &outData, uint8 resourceIndex);
+ /**
+ * Load PCM for opcode 0x3E.
+ * DOS: indexed MCS resource (2-byte header). Amiga: OS_/MXOS from DataA (raw PCM).
+ * On success, rateHz/headerSkip describe how playSample should consume the buffer.
+ */
+ bool loadSoundResource(Common::Array<uint8> &outData, uint8 resourceIndex,
+ int &rateHz, int &headerSkip);
bool loadMusicResource(Common::Array<uint8> &outData, uint8 resourceIndex);
// scriptReadValue: reads a typed value from the script stream
Commit: 4330a2ada73d3c6e93f70588bbbf6d05dcfee72b
https://github.com/scummvm/scummvm/commit/4330a2ada73d3c6e93f70588bbbf6d05dcfee72b
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00
Commit Message:
MACS2: improved mcs v1 magic number situation (named a few things)
Changed paths:
engines/macs2/gameobjects.cpp
engines/macs2/macs2.cpp
engines/macs2/macs2.h
engines/macs2/macs2_constants.h
engines/macs2/midiparser_macs2.cpp
engines/macs2/scriptexecutor.cpp
engines/macs2/view1.cpp
diff --git a/engines/macs2/gameobjects.cpp b/engines/macs2/gameobjects.cpp
index 80eacbd8d48..41e6a87ad93 100644
--- a/engines/macs2/gameobjects.cpp
+++ b/engines/macs2/gameobjects.cpp
@@ -66,7 +66,7 @@ Common::Array<uint32> Macs2::Scenes::readSpecialAnimsOffsets(uint16 sceneIndex,
Common::MemoryReadStream *Macs2::Scenes::readSceneStrings(uint16 sceneIndex, Common::SeekableReadStream *fileStream) {
const uint32 directoryOffset = g_engine->getMcsDirectoryOffset();
- // Third dword of entry (sceneIndex-1) / strings blob â DOS formula directory+scene*0xC-4.
+ // Third dword of entry (sceneIndex-1) / strings blob - DOS formula directory+scene*0xC-4.
fileStream->seek(directoryOffset + sceneIndex * 0xC - 0x4);
uint32 sceneDataOffset2 = fileStream->readUint32LE();
fileStream->seek(sceneDataOffset2, SEEK_SET);
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index f3144d62429..645eba875aa 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -113,6 +113,24 @@ Graphics::ManagedSurface Macs2Engine::readRLEImage(int64 offs, Common::MemoryRea
return result;
}
+Macs2Engine::McsFileVersion Macs2Engine::detectMcsFileVersion(Common::SeekableReadStream &stream) const {
+ if (stream.size() < (int64)kMcsMagicSize)
+ return McsFileVersion::Unknown;
+
+ const int64 pos = stream.pos();
+ byte magic[kMcsMagicSize];
+ stream.seek(0, SEEK_SET);
+ if (stream.read(magic, kMcsMagicSize) != kMcsMagicSize) {
+ stream.seek(pos, SEEK_SET);
+ return McsFileVersion::Unknown;
+ }
+ stream.seek(pos, SEEK_SET);
+
+ if (memcmp(magic, kMcsMagicV1, kMcsMagicSize) == 0)
+ return McsFileVersion::V1;
+ return McsFileVersion::Unknown;
+}
+
void Macs2Engine::readResourceFile() {
{
// Extra scope in order to make sure no code tries to read from the file directly.
@@ -127,23 +145,25 @@ void Macs2Engine::readResourceFile() {
_fileStream = new Common::MemoryReadStream(fileData, size, DisposeAfterUse::YES);
}
- // Full implementation here
-
- // File layout (from loadResourceFile at 1008:2e8d):
- // 0x00: 12-byte magic header "AHFFMCSR0100"
- // 0x0C: 2 bytes actor index + 2 bytes initial scene index (loaded below)
- // 0x10: 0x3000 bytes scene table (512 entries * 12 bytes, accessed via seek per scene)
- // 0x3010: 0x300 bytes vanilla palette (global default, overwritten per-scene by changeScene)
- // 0x3310: 0x800 bytes shading table -> sceneData+0x53D3 (64 palette entries x 32 levels)
- // 0x3B10: 33 cursor/icon image entries (4-byte size + blob data each)
- // Then: Font 1 (4-byte size + glyph data)
- // Then: Font 2 (4-byte size + glyph data)
- // Then: 0x400 bytes map scene offsets -> sceneData+0x5DDB
- // We skip to the cursor images; palette/shading are loaded per-scene in changeScene.
- _fileStream->seek(0xC + 0x4 + 0x3000 + 0x300);
- _shadingTable.resize(0x800);
- _fileStream->read(_shadingTable.data(), 0x800);
- _fileStream->seek(0xC + 0x4 + 0x3000 + 0x300 + 0x800);
+ _mcsFileVersion = detectMcsFileVersion(*_fileStream);
+ if (_mcsFileVersion != McsFileVersion::V1)
+ error("readResourceFile(): unrecognized MCS magic (expected %s)", kMcsMagicV1);
+
+ loadResourceFileV1();
+}
+
+void Macs2Engine::loadResourceFileV1() {
+ // File layout (loadResourceFile @ 1008:2e8d, magic AHFFMACS0100):
+ // +0x00 12-byte magic
+ // +0x0C actor index, +0x0E initial scene index
+ // +0x10 directory 0x3000 (512 x 12); object DATA/SCRIPT ptrs at +0x17F4/+0x17F8
+ // +0x3010 vanilla palette 0x300
+ // +0x3310 shading table 0x800 -> scene+0x53D3
+ // then 0x21 cursor/icon images, Font1, Font2, 0x400 map scene offsets -> scene+0x5DDB
+ // Global vanilla palette is overwritten per-scene in changeScene; skip to shading.
+ _fileStream->seek(kMcsV1ShadingTableOffset, SEEK_SET);
+ _shadingTable.resize(kMcsV1ShadingTableSize);
+ _fileStream->read(_shadingTable.data(), kMcsV1ShadingTableSize);
readImageResources(_fileStream);
// Font 1 follows immediately after the 33 image resource entries.
// Original: 4-byte size field (skipped) + 2-byte glyph count + glyph data.
@@ -168,13 +188,12 @@ void Macs2Engine::readResourceFile() {
}
numPanelGlyphs = font2GlyphCount;
- // Map scene offsets: 0x400 bytes (256 entries x 4 bytes) -> scene+0x5DDB
- // First entry is the help screen image offset.
- for (int i = 0; i < 256; i++) {
+ // Map scene offsets -> scene+0x5DDB. First entry is the help screen image offset.
+ for (uint i = 0; i < kMcsV1MapSceneOffsetCount; i++) {
_mapSceneOffsets[i] = _fileStream->readUint32LE();
}
- _fileStream->seek(0xC, SEEK_SET);
+ _fileStream->seek(kMcsV1ActorIndexOffset, SEEK_SET);
Scenes::instance()._currentActorIndex = _fileStream->readUint16LE();
uint16 firstSceneIndex = _fileStream->readUint16LE();
Scenes::instance()._currentSceneIndex = firstSceneIndex;
@@ -187,10 +206,8 @@ void Macs2Engine::readResourceFile() {
// Original allocates all 512 slots, then frees unused ones. We pre-fill with nullptr.
GameObjects::instance()._objects.resize(0x200, nullptr);
for (int i = 1; i <= 0x200; i++) {
- // The formula for the seek lives at l0037_0936
- // The global [0752h] is loaded with 3000h bytes read from offset Ch + 4h in the file
- // Regarding the 4h offset: Before the 3000h bytes, we have the values of the two globals 0776 and 077C
- uint32 addressOffset = 0x17F4 + (0xC + 0x04) + i * 0xC;
+ // Directory object DATA dword: file+kMcsV1DirectoryOffset+kMcsV1ObjectDataPtrRel+i*12
+ const uint32 addressOffset = kMcsV1DirectoryOffset + kMcsV1ObjectDataPtrRel + (uint32)i * 0xC;
_fileStream->seek(addressOffset, SEEK_SET);
uint32 objectOffset = _fileStream->readUint32LE();
if (objectOffset == 0) {
@@ -202,7 +219,7 @@ void Macs2Engine::readResourceFile() {
gameObject->_index = i;
gameObject->_dataOffset = objectOffset;
- // This loading happens around the l0037_082D: mark
+ // Object header (ReadyObject / initGameObject): x, y, scene, orientation, vertical scale
uint16 x = _fileStream->readUint16LE();
uint16 y = _fileStream->readUint16LE();
gameObject->_position = Common::Point(x, y);
@@ -212,8 +229,8 @@ void Macs2Engine::readResourceFile() {
const uint16 animSlotCount = maxAnimSlots();
for (int j = 1; j <= (int)animSlotCount; j++) {
- // Per-slot data in file: 2 bytes animID, 2 bytes sourceKey, 4 bytes dataSize, data, 2 bytes speed, 1 byte mirrorFlag, 1 byte (discarded)
- _fileStream->readUint16LE(); // runtime+0x24: animation slot ID (not used at runtime, editor metadata)
+ // Per-slot: animID, sourceKey, dataSize, data, speed, mirrorFlag, discarded byte
+ _fileStream->readUint16LE(); // runtime+0x24: animation slot ID (editor metadata)
uint16 blobSourceKey = _fileStream->readUint16LE();
uint32 dataSize = _fileStream->readUint32LE();
uint8 *data = new uint8[dataSize];
@@ -221,19 +238,12 @@ void Macs2Engine::readResourceFile() {
gameObject->_blobs.push_back(Common::Array<uint8>(data, dataSize));
delete[] data;
gameObject->_blobSourceKeys.push_back(blobSourceKey);
- // Per-slot wAnimSpeed (slot+0x0C in runtime, walk speed used by walkAlongPath)
uint16 blobSpeed = _fileStream->readUint16LE();
gameObject->_blobWalkSpeeds.push_back(blobSpeed);
- // Mirror flag (+0x32 in runtime)
uint16 blobMirrorFlag = _fileStream->readByte();
_fileStream->readByte(); // slot loaded flag (runtime-only, discarded from file)
gameObject->_blobMirrorFlags.push_back(blobMirrorFlag != 0);
- // In order to get to l0037_0BBA: where the blob will be mirrored,
- // the bytes at +Eh and +Fh must be != 0
- // +Fh is set related to the inner loop - I think it means that
- // the blob is empty
- // +Eh is read here
if (blobMirrorFlag != 0) {
debugC(kDebugScript, "Object %.4x need to mirror blob %4.x", i, j);
if (dataSize > 0) {
@@ -241,16 +251,14 @@ void Macs2Engine::readResourceFile() {
}
}
}
- // Per-object rendering flags (after all 21 animation slots):
- // Binary loadObjectData reads these into runtime+0x184, +0x185, +0x186
- _fileStream->readByte(); // runtime+0x184: hasInventoryIcon (container flag) - derived dynamically from _blobs[0x13] presence
- gameObject->_hasShading = _fileStream->readByte() != 0; // runtime+0x185: shading enabled
- gameObject->_hasScaling = _fileStream->readByte() != 0; // runtime+0x186: scaling enabled
+ // Per-object flags after anim slots (loadObjectData -> runtime+0x184..+0x186)
+ _fileStream->readByte(); // hasInventoryIcon (derived from slot 0x13)
+ gameObject->_hasShading = _fileStream->readByte() != 0; // runtime+0x185
+ gameObject->_hasScaling = _fileStream->readByte() != 0; // runtime+0x186
- // Read the object script (resource offset table + script bytecode)
- // Binary: scene table at +0x17F8 holds the script data file offset for each object
- addressOffset = 0x17F8 + (0xC + 0x04) + i * 0xC;
- _fileStream->seek(addressOffset, SEEK_SET);
+ // Object SCRIPT ptr in directory (+0x17F8). Zero SCRIPT keeps the object (no script table).
+ const uint32 scriptPtrOffset = kMcsV1DirectoryOffset + kMcsV1ObjectScriptPtrRel + (uint32)i * 0xC;
+ _fileStream->seek(scriptPtrOffset, SEEK_SET);
objectOffset = _fileStream->readUint32LE();
// Binary loadResourceFile prunes an object slot ONLY when its DATA offset
@@ -266,8 +274,7 @@ void Macs2Engine::readResourceFile() {
continue;
}
_fileStream->seek(objectOffset, SEEK_SET);
- // Resource offset table at +0x18D equivalent in file (128 bytes = 32 dword offsets).
- // Binary loadObjectData reads this into runtime+0x18D.
+ // Resource offset table (32 dwords).
const uint maxObjRes = maxObjectResources();
for (uint r = 0; r < maxObjRes; r++) {
gameObject->_resourceOffsets[r] = _fileStream->readUint32LE();
@@ -811,8 +818,8 @@ void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
_hotspotOverrides[i] = 0xFFFF;
// Match DOS changeScene: when View1 is not up yet (first call from
- // readAmigaResources), only load scene data. Entry init/repeat â including
- // intro frameWait/changeScene â must run from View1::tick so waits can
+ // readAmigaResources), only load scene data. Entry init/repeat - including
+ // intro frameWait/changeScene - must run from View1::tick so waits can
// complete in the game loop.
if (executeScript && currentView != nullptr)
_scriptExecutor->runSceneEntryScriptPasses();
@@ -1215,7 +1222,7 @@ int Macs2Engine::walkableDistance(int nodeA, int nodeB) {
const Common::Point &b = pathfindingPoints[nodeB - 1]._position;
if (!isPathWalkable(a.y, a.x, b.y, b.x))
return 0x500;
- // Binary search for integer sqrt(dx² + dy²), matching binary at 1008:1293
+ // Binary search for integer sqrt(dx^2 + dy^2), matching binary at 1008:1293
int32 dx = abs((int)(b.x - a.x));
int32 dy = abs((int)(b.y - a.y));
int32 distSq = dx * dx + dy * dy;
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index 39d8d2e352d..b692e855fe8 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -255,7 +255,16 @@ protected:
public:
Graphics::ManagedSurface readRLEImage(int64 offs, Common::MemoryReadStream *stream);
+ /** Open RESOURCE.MCS, check magic, dispatch to loadResourceFileV1. */
void readResourceFile();
+ /** MCS dialect from the 12-byte file magic. */
+ enum class McsFileVersion {
+ Unknown = 0,
+ V1 // AHFFMACS0100
+ };
+ McsFileVersion detectMcsFileVersion(Common::SeekableReadStream &stream) const;
+ /** Load AHFFMACS0100 layout (loadResourceFile @ 1008:2e8d). */
+ void loadResourceFileV1();
/** Amiga: open DataA/Mdir, load OO objects as GameObjects, cursors, and scene stubs. */
void readAmigaResources();
void applyAmigaUiPalette();
@@ -401,6 +410,7 @@ public:
Common::Array<BackgroundAnimationBlob> _backgroundAnimationsBlobs;
Common::MemoryReadStream *_fileStream = nullptr;
+ McsFileVersion _mcsFileVersion = McsFileVersion::Unknown;
/** Amiga MXFF line pitch: measureTextWidth @ 00224420 uses (font[+8] - 1). */
uint16 amigaTextLinePitch = 0;
@@ -560,8 +570,9 @@ public:
bool isDemo() const { return getFeatures() & ADGF_DEMO; }
- /** MCS directory base; DOS resource layout uses 0x10. */
- uint32 getMcsDirectoryOffset() const { return 0x10; }
+ /** MCS directory base (v1: file+0x10 after magic + actor/scene words). */
+ uint32 getMcsDirectoryOffset() const { return kMcsV1DirectoryOffset; }
+ McsFileVersion getMcsFileVersion() const { return _mcsFileVersion; }
int screenWidth() const { return kScreenWidth; }
int screenWidthLast() const { return screenWidth() - 1; }
int gameHeight() const { return kGameHeight; }
@@ -591,7 +602,7 @@ public:
bool scriptValuesHaveHighWord() const { return false; }
/** Script stream: variable index is followed by a padding word. */
bool scriptVarIndexHasPaddingWord() const { return false; }
- /** Script coordinates â screen/runtime coordinates (identity on DOS). */
+ /** Script coordinates -> screen/runtime coordinates (identity on DOS). */
int16 scaleScriptCoord(int16 coord) const { return coord; }
/** Dialogue / text-box chrome (DOS l0037_B368 / B462). */
diff --git a/engines/macs2/macs2_constants.h b/engines/macs2/macs2_constants.h
index bc707dc8051..817f7bcb574 100644
--- a/engines/macs2/macs2_constants.h
+++ b/engines/macs2/macs2_constants.h
@@ -35,6 +35,30 @@ static constexpr int kUIHeight = 64;
static constexpr int kScreenHeight = kGameHeight + kUIHeight;
static constexpr int kScreenHeightLast = kScreenHeight - 1;
+/**
+ * RESOURCE.MCS header / layout (loadResourceFile @ 1008:2e8d).
+ * Magic is 12 ASCII bytes "AHFFMACS0100".
+ * (Input-recording uses a different 12-byte tag; do not confuse with MCS.)
+ */
+static constexpr uint kMcsMagicSize = 12;
+static constexpr const char *kMcsMagicV1 = "AHFFMACS0100";
+
+/** MCS v1 absolute file offsets (after validating AHFFMACS0100). */
+static constexpr uint32 kMcsV1ActorIndexOffset = 0x0C;
+static constexpr uint32 kMcsV1DirectoryOffset = 0x10;
+static constexpr uint32 kMcsV1DirectorySize = 0x3000; // 512 entries * 12 bytes
+/** Object DATA file-offset dword, relative to directory base (entry i at +rel + i*12). */
+static constexpr uint32 kMcsV1ObjectDataPtrRel = 0x17F4;
+/** Object SCRIPT/resource-table file-offset dword, relative to directory base. */
+static constexpr uint32 kMcsV1ObjectScriptPtrRel = 0x17F8;
+static constexpr uint32 kMcsV1VanillaPaletteOffset = kMcsV1DirectoryOffset + kMcsV1DirectorySize; // 0x3010
+static constexpr uint32 kMcsV1VanillaPaletteSize = 0x300;
+static constexpr uint32 kMcsV1ShadingTableOffset = kMcsV1VanillaPaletteOffset + kMcsV1VanillaPaletteSize; // 0x3310
+static constexpr uint32 kMcsV1ShadingTableSize = 0x800;
+static constexpr uint kMcsV1CursorImageCount = 0x21;
+static constexpr uint kMcsV1MapSceneOffsetCount = 256;
+static constexpr uint32 kMcsV1MapSceneOffsetsSize = kMcsV1MapSceneOffsetCount * 4; // 0x400
+
} // namespace Macs2
#endif // MACS2_CONSTANTS_H
diff --git a/engines/macs2/midiparser_macs2.cpp b/engines/macs2/midiparser_macs2.cpp
index 0ec816c2dd8..3cbefbac0e8 100644
--- a/engines/macs2/midiparser_macs2.cpp
+++ b/engines/macs2/midiparser_macs2.cpp
@@ -52,8 +52,8 @@ bool MidiParser_Macs2::loadMusic(const byte *data, uint32 size) {
// The original decrements _nextEventTimer on EVERY timer callback (120 Hz),
// not just on subdivision ticks. So delta=1 means 1/120th of a second.
- // MidiParser: µs per tick = tempo / ppqn. With ppqn=1, tempo = µs per tick.
- // At 120 callbacks/sec: µs per tick = 1000000/120 = 8333.
+ // MidiParser: us per tick = tempo / ppqn. With ppqn=1, tempo = us per tick.
+ // At 120 callbacks/sec: us per tick = 1000000/120 = 8333.
// Since our timer rate is also set to 1000000/120, setting tempo=8333 with
// ppqn=1 means exactly 1 tick per callback, matching the original behavior.
_ppqn = 1;
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 24404491fb3..4e203342a44 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -698,7 +698,7 @@ bool ScriptExecutor::loadNextScript() {
//
// Binary: when g_wExecutingScriptObjectId > 0x200 (e.g. opcode 0x29 sentinel),
// the outer loop stops without loading further object scripts. Check before the
- // sceneâobject transition so a scene-script sentinel is not wiped by resetting
+ // scene->object transition so a scene-script sentinel is not wiped by resetting
// _executingObjectIndex to 0.
if (_executingScriptObjectId > 0x200)
return false;
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index f928bb3e308..b88d77fbc19 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -3994,7 +3994,7 @@ void Character::update() {
}
// Revert position
pos = savedPos;
- // Wall-sliding: build push vector from ±1 and ±2 samples
+ // Wall-sliding: build push vector from +/-1 and +/-2 samples
int pushX = 0, pushY = 0;
if (Macs2Engine::isWalkabilityBlocking(lookupWalkability(Common::Point(pos.x + 1, pos.y))))
pushX--;
Commit: 3dad819e2d91f0305f8747280a578759e22797ae
https://github.com/scummvm/scummvm/commit/3dad819e2d91f0305f8747280a578759e22797ae
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00
Commit Message:
MACS2: fixed inventory item transfer in scumm verb ui mode
Changed paths:
engines/macs2/scummui.cpp
engines/macs2/scummui.h
engines/macs2/view1.cpp
diff --git a/engines/macs2/scummui.cpp b/engines/macs2/scummui.cpp
index 3231ad39e8c..de78c7a898d 100644
--- a/engines/macs2/scummui.cpp
+++ b/engines/macs2/scummui.cpp
@@ -285,6 +285,26 @@ bool ScummUI::handleClick(const Common::Point &pos, bool scriptsRunning) {
if (scriptsRunning)
return true;
+ const Script::MouseMode mode = g_engine->_scriptExecutor->_cursorMode;
+ const bool takingFromContainer =
+ mode == Script::MouseMode::UseInventory &&
+ _view->_activeInventoryItem != nullptr &&
+ _view->_uiPanelState == View1::kUiPanelContainerInventory;
+
+ // Drop a held container item onto the protagonist strip (classic Take/Drop).
+ if (takingFromContainer && isPointInInventoryStrip(pos)) {
+ _view->transferInventoryItem(_view->_activeInventoryItem,
+ GameObjects::instance().getProtagonistObject());
+ _view->_activeInventoryItem = nullptr;
+ g_engine->_scriptExecutor->_inventoryActionFlag = true;
+ g_engine->setCursorMode(Script::MouseMode::Use);
+ _view->updateCursor();
+ _view->setInventorySource(_view->_inventorySource);
+ syncInventory();
+ _view->redraw();
+ return true;
+ }
+
const Common::Array<GameObject *> items = getProtagonistItems();
const int maxVisible = kInvCols * kInvRows;
for (int i = 0; i < maxVisible; i++) {
@@ -296,7 +316,6 @@ bool ScummUI::handleClick(const Common::Point &pos, bool scriptsRunning) {
continue;
GameObject *item = items[itemIdx];
- const Script::MouseMode mode = g_engine->_scriptExecutor->_cursorMode;
if (mode == Script::MouseMode::Look) {
g_engine->_scriptExecutor->_interactedObjectID = 0x400 + item->_index;
@@ -405,6 +424,13 @@ Common::Rect ScummUI::getInvScrollLeftRect() const {
return Common::Rect(getInvArrowX(), kVerbY, getInvArrowX() + scrollW, kVerbY + kVerbH * kVerbRows);
}
+bool ScummUI::isPointInInventoryStrip(const Common::Point &pos) const {
+ const Common::Rect left = getInvScrollLeftRect();
+ const Common::Rect right = getInvScrollRightRect();
+ const Common::Rect strip(left.left, left.top, right.right, left.bottom);
+ return strip.contains(pos);
+}
+
Common::Rect ScummUI::getInvScrollRightRect() const {
const int scrollW = getScrollButtonWidth();
const int x = getInvArrowX() + scrollW + kInvCols * kInvItemW;
diff --git a/engines/macs2/scummui.h b/engines/macs2/scummui.h
index 694482f9212..7cfc2c439d3 100644
--- a/engines/macs2/scummui.h
+++ b/engines/macs2/scummui.h
@@ -85,6 +85,8 @@ private:
Common::Rect getInvItemRect(int index) const;
Common::Rect getInvScrollLeftRect() const;
Common::Rect getInvScrollRightRect() const;
+ /** True if pos hits the inventory scroll/item area of the strip. */
+ bool isPointInInventoryStrip(const Common::Point &pos) const;
View1 *_view;
int _activeVerbIndex;
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index b88d77fbc19..c5764a8ebce 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -171,7 +171,9 @@ bool View1::shouldShowScummVerbUI() const {
return false;
if (_isShowingTextBox || _isShowingDialoguePanel)
return false;
- if (_uiPanelState == kUiPanelContainerInventory || _uiPanelState == kUiPanelSaveLoad)
+ // Keep the strip visible during container inventory so items can be taken
+ // into the protagonist inventory (Drop/Take is suppressed on that panel).
+ if (_uiPanelState == kUiPanelSaveLoad)
return false;
if (g_engine->_scriptExecutor->_cursorMode == Script::MouseMode::Disabled)
return false;
Commit: 151c31226ba2140a0efc28f4bf1e93e0f27b3d84
https://github.com/scummvm/scummvm/commit/151c31226ba2140a0efc28f4bf1e93e0f27b3d84
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-02T21:27:46+02:00
Commit Message:
MACS2: replaced opcode functions with script callbacks
Changed paths:
engines/macs2/scriptexecutor.cpp
engines/macs2/scriptexecutor.h
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 4e203342a44..dcc818d8ed6 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -253,7 +253,7 @@ void ScriptExecutor::scriptSaveVariableHelper(uint32 value) {
setVariableValue(variableID, value);
}
-void ScriptExecutor::scriptChangeAnimation() {
+OpcodeResult ScriptExecutor::scriptChangeAnimation() {
// scriptChangeAnimation (1008:b6be). Changes a background animation's
// current frame by calling advanceAnimFrame with a target position.
// Binary calls scriptReadValue() twice (16-bit reads); literals like 4097
@@ -263,22 +263,22 @@ void ScriptExecutor::scriptChangeAnimation() {
debugC(kDebugScript, "SCRIPT::changeAnimation(bgAnim=%u, targetFrame=%u)", backgroundAnimationIndex, targetFrameIndex);
if (backgroundAnimationIndex < 1) {
setScriptError(8);
- return;
+ return OpcodeResult::Continue;
}
if (backgroundAnimationIndex > _engine->_backgroundAnimationsBlobs.size()) {
setScriptError(8);
- return;
+ return OpcodeResult::Continue;
}
// Binary is 1-indexed, C++ array is 0-indexed
BackgroundAnimationBlob &blob = _engine->_backgroundAnimationsBlobs[backgroundAnimationIndex - 1];
if (blob._blob.empty()) {
setScriptError(8);
- return;
+ return OpcodeResult::Continue;
}
const uint16 sequenceLength = BackgroundAnimationBlob::getAnimFrameCount(blob._blob);
if (targetFrameIndex > sequenceLength) {
setScriptError(9);
- return;
+ return OpcodeResult::Continue;
}
BackgroundAnimationBlob::advanceAnimFrame(blob._blob, true, targetFrameIndex + 0x64);
// Match save/load restore: keep the requested sequence position in blob[+2]
@@ -291,6 +291,7 @@ void ScriptExecutor::scriptChangeAnimation() {
View1 *currentView = (View1 *)_engine->findView("View1");
if (currentView != nullptr)
currentView->presentFrame();
+ return OpcodeResult::Continue;
}
uint16 ScriptExecutor::getAreaAtPoint(uint16 x, uint16 y) {
@@ -647,13 +648,13 @@ void ScriptExecutor::step() {
shouldContinue = loadNextScript();
} else {
// Let the current script continue
- ExecutionResult result = executeOpcodes();
+ OpcodeResult result = executeOpcodes();
if (hasScriptError()) {
recordScriptErrorPosition();
shouldContinue = false;
break;
}
- if (result == ExecutionResult::WaitingForCallback) {
+ if (result == OpcodeResult::WaitForCallback) {
// We need to change our state as well now
_state = ExecutorState::WaitingForCallback;
if (!_debugPaused && !_waitingForUiClick) {
@@ -794,7 +795,7 @@ uint16 Script::ScriptExecutor::readUint16() {
return _stream->readUint16LE();
}
-void Script::ScriptExecutor::scriptSetVar() {
+OpcodeResult Script::ScriptExecutor::scriptSetVar() {
// This writes to a script variable.
(void)readByte();
uint16 variableIndex = readUint16();
@@ -803,9 +804,10 @@ void Script::ScriptExecutor::scriptSetVar() {
scriptReadValuePair(var.a, var.b);
debugC(kDebugScript, "SCRIPT::setVar(variableIndex=%u, value1=%u, value2=%u)", variableIndex, var.a, var.b);
_variables[variableIndex] = var;
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptSetVarOr() {
+OpcodeResult Script::ScriptExecutor::scriptSetVarOr() {
// Padding/type byte (same as opcode 0x01) - read and discarded
(void)readByte();
uint16 variableIndex = readUint16();
@@ -821,9 +823,10 @@ void Script::ScriptExecutor::scriptSetVarOr() {
value3 |= 0x00;
debugC(kDebugScript, "SCRIPT::setVarOr(variableIndex=%u, value1=%u, value2=%u, value3=%u)", variableIndex, value1, value2, value3);
setVariableValue(variableIndex, value2, value3);
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptIfTrue() {
+OpcodeResult Script::ScriptExecutor::scriptIfTrue() {
uint16 res1;
uint16 res2;
scriptReadValuePair(res1, res2);
@@ -833,9 +836,10 @@ void Script::ScriptExecutor::scriptIfTrue() {
scriptSkipBlock();
}
_expectedEndLocation = _stream->pos();
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptIfFalse() {
+OpcodeResult Script::ScriptExecutor::scriptIfFalse() {
uint16 result1;
uint16 result2;
scriptReadValuePair(result1, result2);
@@ -845,9 +849,10 @@ void Script::ScriptExecutor::scriptIfFalse() {
scriptSkipBlock();
}
_expectedEndLocation = _stream->pos();
+ return OpcodeResult::Continue;
}
-bool Script::ScriptExecutor::scriptCompare() {
+OpcodeResult Script::ScriptExecutor::scriptCompare() {
// Comparison opcode from executeOpcodes (1008:db56).
// Reads a comparison sub-opcode, two 32-bit values (v1:v2 and v3:v4),
// and skips the following block if the condition is NOT met.
@@ -896,10 +901,10 @@ bool Script::ScriptExecutor::scriptCompare() {
scriptSkipBlock();
}
- return true;
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptIfInteraction() {
+OpcodeResult Script::ScriptExecutor::scriptIfInteraction() {
// "Use item on object" comparison from executeOpcodes (1008:db56).
// Reads sub-opcode (1=match, 2=NOT match), then the interacted pair
// and two comparison objects. Checks both orderings.
@@ -920,34 +925,39 @@ void Script::ScriptExecutor::scriptIfInteraction() {
if (!matched) {
scriptSkipBlock();
}
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptEndIf() {
+OpcodeResult Script::ScriptExecutor::scriptEndIf() {
// Opcode 0x07: no-op (confirmed: no handler in disassembly, falls through to loop).
// It has no specific case handling code in the original.
debugC(kDebugScript, "SCRIPT::endIf()");
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptElse() {
+OpcodeResult Script::ScriptExecutor::scriptElse() {
// This is some kind of skipping as well.
scriptSkipAlternate();
debugC(kDebugScript, "SCRIPT::else()");
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptNop09() {
+OpcodeResult Script::ScriptExecutor::scriptNop09() {
// ExecuteScript does not currently have a dedicated opcode 0x09 dispatch branch.
debugC(kDebugScript, "SCRIPT::nop09()");
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptPrintStringLeft() {
+OpcodeResult Script::ScriptExecutor::scriptPrintStringLeft() {
// l0037_DDE8:
debugC(kDebugScript, "SCRIPT::printStringLeft()");
scriptPrintString();
// Ends execution (confirmed: jumps to e3bd in disassembly).
endBuffering(_lastOpcodeTriggeredSkip);
+ return OpcodeResult::WaitForCallback;
}
-void Script::ScriptExecutor::scriptMoveObject() {
+OpcodeResult Script::ScriptExecutor::scriptMoveObject() {
// scriptMoveObject (1008:aa83). Moves an object to a new scene/position.
// Handles render list updates for both source and destination scenes.
const uint32 objectID = scriptReadValue32() - 0x400;
@@ -958,12 +968,12 @@ void Script::ScriptExecutor::scriptMoveObject() {
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::FinishScript;
}
GameObject *object = GameObjects::getObjectByIndex(objectID);
if (object == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::FinishScript;
}
clearScriptError();
@@ -972,14 +982,14 @@ void Script::ScriptExecutor::scriptMoveObject() {
GameObject *oldParent = GameObjects::getObjectByIndex(object->_sceneIndex - 0x400);
if (oldParent == nullptr) {
setScriptError(2);
- return;
+ return OpcodeResult::FinishScript;
}
}
if (sceneID > 0x400) {
GameObject *newParent = GameObjects::getObjectByIndex(sceneID - 0x400);
if (newParent == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::FinishScript;
}
}
@@ -1162,9 +1172,15 @@ void Script::ScriptExecutor::scriptMoveObject() {
}
currentView->rebuildCharacterLookupTable();
+ if (hasScriptError())
+ return OpcodeResult::FinishScript;
+ // Binary: after scriptMoveObject(), exits if position >= end.
+ if (_stream->pos() >= _stream->size())
+ return OpcodeResult::FinishScript;
+ return OpcodeResult::Continue;
}
-ExecutionResult Script::ScriptExecutor::scriptChangeScene() {
+OpcodeResult Script::ScriptExecutor::scriptChangeScene() {
// Scene change from scriptChangeScene (1008:ad6e).
// Original behavior:
// 1. Read sceneID, transitionMode, transitionSpeed
@@ -1194,21 +1210,21 @@ ExecutionResult Script::ScriptExecutor::scriptChangeScene() {
endTimer();
endFrameWait();
endBuffering(_lastOpcodeTriggeredSkip);
- return ExecutionResult::ScriptFinished;
+ return OpcodeResult::ReturnFinished;
}
if (transitionMode == 0 && (transitionSpeed == 0 || transitionSpeed > 0x40)) {
setScriptError(0x26);
endTimer();
endFrameWait();
endBuffering(_lastOpcodeTriggeredSkip);
- return ExecutionResult::ScriptFinished;
+ return OpcodeResult::ReturnFinished;
}
if (transitionMode > 1) {
setScriptError(4);
endTimer();
endFrameWait();
endBuffering(_lastOpcodeTriggeredSkip);
- return ExecutionResult::ScriptFinished;
+ return OpcodeResult::ReturnFinished;
}
// Binary scriptChangeScene (1008:ad6e): beginFrame, hourglass cursor, flipScreen
@@ -1273,11 +1289,11 @@ ExecutionResult Script::ScriptExecutor::scriptChangeScene() {
// re-entering scene 6 from the bar).
if (!isScriptWaitDeferred() && _state == ExecutorState::WaitingForCallback)
_state = ExecutorState::Executing;
- return isScriptWaitDeferred() ? ExecutionResult::WaitingForCallback
- : ExecutionResult::ScriptFinished;
+ return isScriptWaitDeferred() ? OpcodeResult::WaitForCallback
+ : OpcodeResult::ReturnFinished;
}
-ExecutionResult Script::ScriptExecutor::scriptShowDialogue() {
+OpcodeResult Script::ScriptExecutor::scriptShowDialogue() {
// Show a dialogue option (1008:b2a8).
debugC(kDebugScript, "scriptShowDialogue: walkTarget=%d", _walkTargetObjectIndex);
const uint32 objectID = scriptReadValue32() - 0x400;
@@ -1292,29 +1308,29 @@ ExecutionResult Script::ScriptExecutor::scriptShowDialogue() {
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
endBuffering(_lastOpcodeTriggeredSkip);
- return ExecutionResult::ScriptFinished;
+ return OpcodeResult::FinishScript;
}
GameObject *speaker = GameObjects::getObjectByIndex(objectID);
if (speaker == nullptr) {
setScriptError(0x19);
endBuffering(_lastOpcodeTriggeredSkip);
- return ExecutionResult::ScriptFinished;
+ return OpcodeResult::FinishScript;
}
if (speaker->_dataOffset == 0) {
setScriptError(2);
endBuffering(_lastOpcodeTriggeredSkip);
- return ExecutionResult::ScriptFinished;
+ return OpcodeResult::FinishScript;
}
if (side > 1) {
setScriptError(5);
endBuffering(_lastOpcodeTriggeredSkip);
- return ExecutionResult::ScriptFinished;
+ return OpcodeResult::FinishScript;
}
// Binary: bSlotLoaded for portrait slots 0x12 and 0x13 (runtime+0x153, +0x163).
if (speaker->_blobs.size() < 19 || speaker->_blobs[17].empty() || speaker->_blobs[18].empty()) {
setScriptError(6);
endBuffering(_lastOpcodeTriggeredSkip);
- return ExecutionResult::ScriptFinished;
+ return OpcodeResult::FinishScript;
}
View1 *currentView = (View1 *)_engine->findView("View1");
@@ -1347,10 +1363,10 @@ ExecutionResult Script::ScriptExecutor::scriptShowDialogue() {
endTimer();
endFrameWait();
endBuffering(_lastOpcodeTriggeredSkip);
- return ExecutionResult::WaitingForCallback;
+ return OpcodeResult::WaitForCallback;
}
-void Script::ScriptExecutor::scriptWalkToPosition() {
+OpcodeResult Script::ScriptExecutor::scriptWalkToPosition() {
// Binary scriptWalkToPosition (1008:b843):
// Sets up runtime movement state. Does NOT block - walkAlongPath handles
// actual movement per-frame from the game tick.
@@ -1362,16 +1378,16 @@ void Script::ScriptExecutor::scriptWalkToPosition() {
clearScriptError();
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
GameObject *object = GameObjects::getObjectByIndex(objectID);
if (object == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::Continue;
}
if (object->_dataOffset == 0) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
View1 *currentView = (View1 *)_engine->findView("View1");
@@ -1412,9 +1428,10 @@ void Script::ScriptExecutor::scriptWalkToPosition() {
c->_motionTargetVerticalOffset = object->_verticalOffsetScale;
saveWalkRuntime(c, object);
+ return OpcodeResult::Continue;
}
-ExecutionResult Script::ScriptExecutor::scriptWaitForWalk() {
+OpcodeResult Script::ScriptExecutor::scriptWaitForWalk() {
// Wait for walk completion from executeOpcodes (1008:db56).
// Original: validates object, checks runtime data exists, checks frozen flag,
// sets g_wWalkTargetObjectIndex, hides cursor, returns to gameTick.
@@ -1424,25 +1441,25 @@ ExecutionResult Script::ScriptExecutor::scriptWaitForWalk() {
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
endBuffering(_lastOpcodeTriggeredSkip);
- return ExecutionResult::ScriptFinished;
+ return OpcodeResult::ReturnFinished;
}
GameObject *walkObject = GameObjects::getObjectByIndex(objectID);
if (walkObject == nullptr) {
setScriptError(0x19);
endBuffering(_lastOpcodeTriggeredSkip);
- return ExecutionResult::ScriptFinished;
+ return OpcodeResult::ReturnFinished;
}
// Binary (1008:db56 opcode 0x11): object+0x0A runtime must exist; no on-screen Character required.
if (walkObject->_dataOffset == 0) {
setScriptError(2);
endBuffering(_lastOpcodeTriggeredSkip);
- return ExecutionResult::ScriptFinished;
+ return OpcodeResult::ReturnFinished;
}
// Original checks runtime+0x231 (frozen/attached flag) -> error 0x1F
if (walkObject->_hasBoundsAttachment) {
setScriptError(0x1F);
endBuffering(_lastOpcodeTriggeredSkip);
- return ExecutionResult::ScriptFinished;
+ return OpcodeResult::ReturnFinished;
}
_walkTargetObjectIndex = objectID;
endTimer();
@@ -1451,12 +1468,12 @@ ExecutionResult Script::ScriptExecutor::scriptWaitForWalk() {
enterBlockingWaitCursor();
// Binary: opcode 0x11 exits executeOpcodes with g_wScriptIsExecuting still true.
// runScriptExecutor returns immediately (no object iteration, no cursor restore).
- // ScummVM equivalent: return WaitingForCallback so step() pauses execution.
+ // ScummVM equivalent: return WaitForCallback so step() pauses execution.
// step() handles cursor save/set to Disabled automatically.
- return ExecutionResult::WaitingForCallback;
+ return OpcodeResult::WaitForCallback;
}
-void Script::ScriptExecutor::scriptSkipUntil14() {
+OpcodeResult Script::ScriptExecutor::scriptSkipUntil14() {
// scriptSkipUntil14 @ 1008:a439: read tag, scan from script start for opcode 0x14
// with matching tag, then continue execution after that label (not at 0x13 block end).
const uint16 tag = readUint16();
@@ -1469,7 +1486,7 @@ void Script::ScriptExecutor::scriptSkipUntil14() {
uint16 tag14 = readUint16();
if (tag14 == tag) {
_expectedEndLocation = _stream->pos();
- return;
+ return OpcodeResult::Continue;
}
} else {
_stream->seek(length, SEEK_CUR);
@@ -1477,23 +1494,26 @@ void Script::ScriptExecutor::scriptSkipUntil14() {
}
setScriptError(0x20);
_expectedEndLocation = _stream->pos();
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptSkipWord() {
+OpcodeResult Script::ScriptExecutor::scriptSkipWord() {
// If we reach opcode 14 regularly, just discard the payload and continue.
const uint16 val = readUint16();
debugC(kDebugScript, "SCRIPT::skipWord(%u)", val);
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptClearDialogueChoices() {
+OpcodeResult Script::ScriptExecutor::scriptClearDialogueChoices() {
// Mark that we are gathering strings for setting up a dialogue choice.
_dialogueChoices.clear();
_chosenDialogueOption = 0;
_dialogueChoiceScriptIndices.clear();
debugC(kDebugScript, "SCRIPT::clearDialogueChoices()");
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptAddDialogueChoice() {
+OpcodeResult Script::ScriptExecutor::scriptAddDialogueChoice() {
// Add a dialogue choice.
const uint16 index = scriptReadValue16();
const uint16 offset = readUint16();
@@ -1502,7 +1522,7 @@ void Script::ScriptExecutor::scriptAddDialogueChoice() {
// Binary (1008:c75a): if choice count already 16, set error 0x0E and discard entry.
if (_dialogueChoices.size() >= 16) {
setScriptError(0x0E);
- return;
+ return OpcodeResult::Continue;
}
// Binary stores this index at scene+0x5351+choiceIndex*6 (first field of each 6-byte entry).
@@ -1528,9 +1548,10 @@ void Script::ScriptExecutor::scriptAddDialogueChoice() {
"Opcode 16 choice text: index=%u textOffset=%u numLines=%u scriptObject=%u text=\"%s\"",
index, offset, numLines, _executingScriptObjectId, joinDebugStrings(lines).c_str());
_dialogueChoices.push_back(lines);
+ return OpcodeResult::Continue;
}
-ExecutionResult Script::ScriptExecutor::scriptShowDialogueChoice() {
+OpcodeResult Script::ScriptExecutor::scriptShowDialogueChoice() {
// Finish the dialogue choice.
const uint32 x = scriptReadValue32();
const uint32 y = scriptReadValue32();
@@ -1554,23 +1575,23 @@ ExecutionResult Script::ScriptExecutor::scriptShowDialogueChoice() {
// NOTE: EndTimer prevents race conditions from overlapping waits
endTimer();
endBuffering(_lastOpcodeTriggeredSkip);
- return ExecutionResult::WaitingForCallback;
+ return OpcodeResult::WaitForCallback;
}
-ExecutionResult Script::ScriptExecutor::scriptDismissPanel() {
+OpcodeResult Script::ScriptExecutor::scriptDismissPanel() {
// Set the stream to the end and let the calling code figure out that we are done
// for this run.
debugLogActorWalkState("dismissPanel (opcode 0x18)");
_stream->seek(_stream->size(), SEEK_SET);
endBuffering(_lastOpcodeTriggeredSkip);
- return ExecutionResult::ScriptFinished;
+ return OpcodeResult::ReturnFinished;
}
-void Script::ScriptExecutor::scriptWalkToAndPickup() {
+OpcodeResult Script::ScriptExecutor::scriptWalkToAndPickup() {
// Walk to and pick up an object (1008:c475).
// Binary returns immediately if pickup already in progress, without reading params.
if (_pickupInProgress) {
- return;
+ return OpcodeResult::FinishScript;
}
const uint32 actorIndex = scriptReadValue32() - 0x400;
@@ -1585,35 +1606,35 @@ void Script::ScriptExecutor::scriptWalkToAndPickup() {
if (actorIndex < 1 || actorIndex > 0x200 || objectIndex < 1 || objectIndex > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::FinishScript;
}
GameObject *actorObject = GameObjects::getObjectByIndex(actorIndex);
GameObject *targetObject = GameObjects::getObjectByIndex(objectIndex);
if (actorObject == nullptr || targetObject == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::FinishScript;
}
if (targetObject->_dataOffset != 0) {
_engine->loadObjectData(targetObject);
if (hasScriptError())
- return;
+ return OpcodeResult::FinishScript;
}
if (actorObject->_dataOffset == 0) {
setScriptError(0x19);
- return;
+ return OpcodeResult::FinishScript;
}
if (actorIndex == objectIndex || targetObject->_sceneIndex == actorObject->_index) {
setScriptError(2);
- return;
+ return OpcodeResult::FinishScript;
}
if (actorObject->_hasBoundsAttachment) {
setScriptError(0x1F);
- return;
+ return OpcodeResult::FinishScript;
}
View1 *currentView = (View1 *)_engine->findView("View1");
@@ -1635,9 +1656,10 @@ void Script::ScriptExecutor::scriptWalkToAndPickup() {
if (!hasScriptError()) {
_walkTargetObjectIndex = actorIndex;
}
+ return OpcodeResult::FinishScript;
}
-bool Script::ScriptExecutor::scriptSetPickupFrames() {
+OpcodeResult Script::ScriptExecutor::scriptSetPickupFrames() {
const int32 objectID = (int32)scriptReadValue32() - 0x400;
const uint16 frameStart = scriptReadValue16();
const uint16 frameEnd = scriptReadValue16();
@@ -1646,25 +1668,25 @@ bool Script::ScriptExecutor::scriptSetPickupFrames() {
clearScriptError();
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
- return true;
+ return OpcodeResult::Continue;
}
GameObject *object = GameObjects::getObjectByIndex((uint16)objectID);
if (object == nullptr) {
setScriptError(0x19);
- return true;
+ return OpcodeResult::Continue;
}
if (object->_dataOffset == 0) {
setScriptError(2);
- return true;
+ return OpcodeResult::Continue;
}
object->_pickupFrameStart = frameStart;
object->_pickupFrameEnd = frameEnd;
- return true;
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptSetupObject() {
+OpcodeResult Script::ScriptExecutor::scriptSetupObject() {
const int32 objectID = (int32)scriptReadValue32() - 0x400;
const uint16 slotID = scriptReadValue16();
const uint16 value = scriptReadValue16();
@@ -1673,47 +1695,50 @@ void Script::ScriptExecutor::scriptSetupObject() {
clearScriptError();
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
GameObject *object = GameObjects::getObjectByIndex((uint16)objectID);
if (object == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::Continue;
}
if (object->_dataOffset == 0) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
if (slotID < 1 || slotID > _engine->maxAnimSlots()) {
setScriptError(0x10);
- return;
+ return OpcodeResult::Continue;
}
if (!object->isAnimSlotLoaded(slotID)) {
setScriptError(0x10);
- return;
+ return OpcodeResult::Continue;
}
// Binary writes to runtime+slot*0x10+0x30 which is slot_base+0x0C = wAnimSpeed.
if ((uint)(slotID - 1) < object->_blobWalkSpeeds.size()) {
object->_blobWalkSpeeds[slotID - 1] = value;
}
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptSetSkippable() {
+OpcodeResult Script::ScriptExecutor::scriptSetSkippable() {
// Sets g_wScriptSkippable [102Ah] = 1.
_scriptSkippable = true;
debugC(kDebugScript, "SCRIPT::setSkippable()");
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptClearSkippable() {
+OpcodeResult Script::ScriptExecutor::scriptClearSkippable() {
// Sets g_wScriptSkippable [102Ah] = 0.
_scriptSkippable = false;
debugC(kDebugScript, "SCRIPT::clearSkippable()");
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptPlayAnimation() {
+OpcodeResult Script::ScriptExecutor::scriptPlayAnimation() {
// scriptPlayAnimation (1008:bd58).
const uint32 objectID = scriptReadValue32() - 0x400;
const uint32 slotID = scriptReadValue16();
@@ -1723,36 +1748,36 @@ void Script::ScriptExecutor::scriptPlayAnimation() {
clearScriptError();
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
GameObject *gameObject = GameObjects::getObjectByIndex(objectID);
if (gameObject == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::Continue;
}
if (gameObject->_dataOffset == 0) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
if (slotID < 1 || slotID > _engine->maxAnimSlots()) {
setScriptError(0x10);
- return;
+ return OpcodeResult::Continue;
}
if (!gameObject->isAnimSlotLoaded((uint16)slotID)) {
setScriptError(0x10);
- return;
+ return OpcodeResult::Continue;
}
Common::Array<uint8> *blob = gameObject->getAnimSlotBlob((uint16)slotID);
if (blob == nullptr || blob->empty()) {
setScriptError(0x10);
- return;
+ return OpcodeResult::Continue;
}
AnimBlobView view(*blob);
if (!view.isValid()) {
setScriptError(8);
- return;
+ return OpcodeResult::Continue;
}
// Binary getAnimFrameCount (1010:168c) returns sequence length (blob+0x0A+1),
// not the pixel frame count word at frameDataOffset. advanceAnimFrame uses
@@ -1760,13 +1785,14 @@ void Script::ScriptExecutor::scriptPlayAnimation() {
const uint16 seqLength = view.sequenceLength();
if (frameOffset < 0 || (uint16)frameOffset > seqLength) {
setScriptError(0x12);
- return;
+ return OpcodeResult::Continue;
}
BackgroundAnimationBlob::advanceAnimFrame(*blob, true, (uint16)frameOffset + 0x64);
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptTestPathfinding() {
+OpcodeResult Script::ScriptExecutor::scriptTestPathfinding() {
const uint32 objectID = scriptReadValue32() - 0x400;
const uint32 x = scriptReadValue32();
const uint32 y = scriptReadValue32();
@@ -1776,18 +1802,19 @@ void Script::ScriptExecutor::scriptTestPathfinding() {
_pathWalkableResult = false;
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
GameObject *object = GameObjects::getObjectByIndex(objectID);
if (object == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::Continue;
}
if (object->_dataOffset == 0) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
_pathWalkableResult = _engine->isPathWalkable(y, x, object->_position.y, object->_position.x);
+ return OpcodeResult::Continue;
}
Character *Script::ScriptExecutor::getOrCreateCharacter(uint16 objectID) {
@@ -1929,7 +1956,7 @@ void Script::ScriptExecutor::restoreOpenInventoryScriptContext() {
_isRunningScript = true;
}
-void Script::ScriptExecutor::scriptSetYOffset() {
+OpcodeResult Script::ScriptExecutor::scriptSetYOffset() {
// scriptSetYOffset (1008:c047). Sets object field +8 (vertical offset)
// AND mirrors it into runtime field +0x21D (motion target).
const int32 objectID = (int32)scriptReadValue32() - 0x400;
@@ -1939,17 +1966,17 @@ void Script::ScriptExecutor::scriptSetYOffset() {
clearScriptError();
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
GameObject *object = GameObjects::getObjectByIndex((uint16)objectID);
if (object == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::Continue;
}
if (object->_dataOffset == 0) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
object->_verticalOffsetScale = offset;
@@ -1958,9 +1985,10 @@ void Script::ScriptExecutor::scriptSetYOffset() {
}
object->_storedWalkRuntime.valid = true;
object->_storedWalkRuntime.motionTargetVerticalOffset = offset;
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptSetMotion() {
+OpcodeResult Script::ScriptExecutor::scriptSetMotion() {
const int32 objectID = (int32)scriptReadValue32() - 0x400;
const uint16 targetVerticalOffset = scriptReadValue16();
const uint16 verticalOffsetDelta = scriptReadValue16();
@@ -1971,25 +1999,26 @@ void Script::ScriptExecutor::scriptSetMotion() {
clearScriptError();
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
GameObject *object = GameObjects::getObjectByIndex((uint16)objectID);
if (object == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::Continue;
}
if (object->_dataOffset == 0) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
Character *character = getOrCreateCharacter((uint16)objectID);
seedMotionState(object, character, targetVerticalOffset, verticalOffsetDelta, motionDistance);
debugLogActorWalkState("after setMotion");
+ return OpcodeResult::Continue;
}
-bool Script::ScriptExecutor::scriptSetOrientation() {
+OpcodeResult Script::ScriptExecutor::scriptSetOrientation() {
const int32 objectID = (int32)scriptReadValue32() - 0x400;
const uint16 animIndex = scriptReadValue16();
debugC(kDebugScript, "SCRIPT::setOrientation(objectID=%d, animIndex=%u)", objectID, animIndex);
@@ -1997,24 +2026,24 @@ bool Script::ScriptExecutor::scriptSetOrientation() {
clearScriptError();
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
- return true;
+ return OpcodeResult::Continue;
}
GameObject *object = GameObjects::getObjectByIndex((uint16)objectID);
if (object == nullptr) {
setScriptError(0x19);
- return true;
+ return OpcodeResult::Continue;
}
if (animIndex < 9 || animIndex > 0x10) {
setScriptError(0x14);
- return true;
+ return OpcodeResult::Continue;
}
object->_orientation = animIndex;
- return true;
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptMoveToPosition() {
+OpcodeResult Script::ScriptExecutor::scriptMoveToPosition() {
// Opcode 0x23 scriptMoveToPosition (1008:bafc): seeds runtime walk state directly;
// does not pathfind or use time-based lerp.
const int32 objectID = (int32)scriptReadValue32() - 0x400;
@@ -2026,17 +2055,17 @@ void Script::ScriptExecutor::scriptMoveToPosition() {
clearScriptError();
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
GameObject *object = GameObjects::getObjectByIndex((uint16)objectID);
if (object == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::Continue;
}
if (object->_dataOffset == 0) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
const Common::Point target(x, y);
@@ -2044,15 +2073,16 @@ void Script::ScriptExecutor::scriptMoveToPosition() {
if (!_engine->isPathWalkable(y, x, object->_position.y, object->_position.x) &&
Macs2Engine::isWalkabilityWalkable(_engine->getWalkabilityAt(target))) {
setScriptError(0x15);
- return;
+ return OpcodeResult::Continue;
}
View1 *currentView = (View1 *)_engine->findView("View1");
Character *c = currentView ? getOrCreateCharacter((uint16)objectID) : nullptr;
seedMoveToPositionState(object, c, target, targetVerticalOffset);
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptAddValues() {
+OpcodeResult Script::ScriptExecutor::scriptAddValues() {
// Adds two values read and saves them to a script variable.
// ;; fn0037_C7E6: 0037:C7E6
const uint32 a = scriptReadValue32();
@@ -2067,9 +2097,10 @@ void Script::ScriptExecutor::scriptAddValues() {
scriptSaveVariableHelper(result);
// Skip forward across the second 9F4D read's data.
_stream->seek(3, SEEK_CUR);
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptSubValues() {
+OpcodeResult Script::ScriptExecutor::scriptSubValues() {
// Subtracts two values read and saves them to a script variable.
// ;; fn0037_C82E: 0037:C82E
const uint32 a = scriptReadValue32();
@@ -2080,9 +2111,10 @@ void Script::ScriptExecutor::scriptSubValues() {
_stream->seek(-6, SEEK_CUR);
scriptSaveVariableHelper(result);
_stream->seek(3, SEEK_CUR);
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptLoadSpecialAnim() {
+OpcodeResult Script::ScriptExecutor::scriptLoadSpecialAnim() {
// This one loads a special animation set into the overload slot (1008:c991).
const uint32 id = scriptReadValue32() - 0x400;
const uint16 shouldMirror = scriptReadValue16();
@@ -2092,16 +2124,16 @@ void Script::ScriptExecutor::scriptLoadSpecialAnim() {
clearScriptError();
if (id < 1 || id > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
GameObject *object = GameObjects::getObjectByIndex(id);
if (object == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::Continue;
}
if (object->_dataOffset == 0) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
const Common::Array<uint8> &blob = Scenes::instance().readSpecialAnimBlob(animationID, g_engine->_fileStream);
@@ -2113,9 +2145,10 @@ void Script::ScriptExecutor::scriptLoadSpecialAnim() {
while (object->_blobs.size() <= 20)
object->_blobs.push_back(Common::Array<uint8>());
object->_blobs[20] = object->_overloadAnimation;
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptSetDirection() {
+OpcodeResult Script::ScriptExecutor::scriptSetDirection() {
// scriptSetDirection (1008:c858). Writes to runtime field +0x22D.
const uint32 characterID = scriptReadValue32() - 0x400;
const uint16 value = scriptReadValue16();
@@ -2124,21 +2157,22 @@ void Script::ScriptExecutor::scriptSetDirection() {
clearScriptError();
if (characterID < 1 || characterID > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
GameObject *object = GameObjects::getObjectByIndex(characterID);
if (object == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::Continue;
}
if (object->_dataOffset == 0) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
object->_overloadAnimTriggerDirection = value;
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptStopAnimation() {
+OpcodeResult Script::ScriptExecutor::scriptStopAnimation() {
// scriptStopAnimation (1008:c8e4).
const uint32 characterID = scriptReadValue32() - 0x400;
debugC(kDebugScript, "SCRIPT::stopAnimation(characterID=%u)", characterID);
@@ -2146,53 +2180,62 @@ void Script::ScriptExecutor::scriptStopAnimation() {
clearScriptError();
if (characterID < 1 || characterID > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
GameObject *obj = GameObjects::getObjectByIndex(characterID);
if (obj == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::Continue;
}
if (obj->_dataOffset == 0) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
obj->_overloadAnimTriggerDirection = 0x7FFF;
obj->_useOverloadAnimation = false;
obj->_overloadAnimation.clear();
if (obj->_blobs.size() > 20)
obj->_blobs[20].clear();
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptOpenInventory() {
+OpcodeResult Script::ScriptExecutor::scriptOpenInventory() {
const uint32 objectID = scriptReadValue32() - 0x400;
debugC(kDebugScript, "SCRIPT::openInventory(objectID=%u)", objectID);
clearScriptError();
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
- return;
- }
- GameObject *inventorySource = GameObjects::getObjectByIndex(objectID);
- if (inventorySource == nullptr) {
- setScriptError(0x19);
- return;
+ } else {
+ GameObject *inventorySource = GameObjects::getObjectByIndex(objectID);
+ if (inventorySource == nullptr) {
+ setScriptError(0x19);
+ } else {
+ View1 *currentView = (View1 *)_engine->findView("View1");
+ _savedExternalInventoryMouseMode = _cursorMode == MouseMode::UseInventory ? MouseMode::Use : _cursorMode;
+ _hasPendingExternalInventoryResume = true;
+ _externalInventorySourceObjectID = objectID;
+ saveOpenInventoryScriptContext();
+ // Save script click state (original saves at 0xf94-0xf9a equivalents)
+ _savedScriptClickFlag = _scriptClickFlag;
+ _savedScriptClickX = _scriptClickX;
+ _savedScriptClickY = _scriptClickY;
+ _savedScriptClickResult = _scriptClickResult;
+ currentView->openInventory(inventorySource);
+ }
}
- View1 *currentView = (View1 *)_engine->findView("View1");
- _savedExternalInventoryMouseMode = _cursorMode == MouseMode::UseInventory ? MouseMode::Use : _cursorMode;
- _hasPendingExternalInventoryResume = true;
- _externalInventorySourceObjectID = objectID;
- saveOpenInventoryScriptContext();
- // Save script click state (original saves at 0xf94-0xf9a equivalents)
- _savedScriptClickFlag = _scriptClickFlag;
- _savedScriptClickX = _scriptClickX;
- _savedScriptClickY = _scriptClickY;
- _savedScriptClickResult = _scriptClickResult;
- currentView->openInventory(inventorySource);
-}
-
-void Script::ScriptExecutor::scriptLoadObjectAnim() {
+ // Binary executeOpcodes @ 1008:e0f9 (full + demo):
+ // after openInventory: set script position to end, object id 0x201, finish.
+ // loadNextScript() honors ScriptObjectId > 0x200 and will not start/continue
+ // the object-script walk (including when 0x29 ran from the scene script).
+ _stream->seek(_stream->size(), SEEK_SET);
+ _executingObjectIndex = 0x201;
+ _executingScriptObjectId = 0x201;
+ return OpcodeResult::FinishScript;
+}
+
+OpcodeResult Script::ScriptExecutor::scriptLoadObjectAnim() {
const uint16 objectID = scriptReadValue16() - 0x400;
const uint16 slotID = scriptReadValue16();
const bool shouldMirror = scriptReadValue16() != 0;
@@ -2203,47 +2246,48 @@ void Script::ScriptExecutor::scriptLoadObjectAnim() {
clearScriptError();
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
GameObject *object = GameObjects::getObjectByIndex(objectID);
if (object == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::Continue;
}
if (object->_dataOffset == 0) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
if (slotID < 1 || slotID > _engine->maxAnimSlots()) {
setScriptError(0x13);
- return;
+ return OpcodeResult::Continue;
}
g_engine->loadAnimationFromSceneData(objectID, slotID, arrayIndex, shouldMirror, _executingScriptObjectId);
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptCheckObjectData() {
+OpcodeResult Script::ScriptExecutor::scriptCheckObjectData() {
const uint16 objectID = scriptReadValue16() - 0x400;
debugC(kDebugScript, "SCRIPT::checkObjectData(objectID=%u)", objectID);
clearScriptError();
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
GameObject *object = GameObjects::getObjectByIndex(objectID);
if (object == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::Continue;
}
if (object->_dataOffset == 0) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
View1 *currentView = (View1 *)_engine->findView("View1");
if (currentView == nullptr)
- return;
+ return OpcodeResult::Continue;
const Common::Point posBefore = object->_position;
const bool snapBefore = object->_snapToTarget;
@@ -2259,7 +2303,7 @@ void Script::ScriptExecutor::scriptCheckObjectData() {
// Binary: sortObjectsByDepth (1008:0d79) then loadObjectData (1008:08ec).
_engine->sortObjectsByDepth(objectID);
if (!_engine->loadObjectData(object))
- return;
+ return OpcodeResult::Continue;
debugC(kDebugPath,
"WALK::checkObjectData obj=%u pos before=(%d,%d) after=(%d,%d) snap %d->%d (loadObjectData clears runtime)",
@@ -2277,9 +2321,10 @@ void Script::ScriptExecutor::scriptCheckObjectData() {
currentView->_characters.push_back(character);
}
currentView->rebuildCharacterLookupTable();
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptCheckInventory() {
+OpcodeResult Script::ScriptExecutor::scriptCheckInventory() {
const uint16 objectID = scriptReadValue16() - 0x400;
const uint16 parentID = scriptReadValue16();
debugC(kDebugScript, "SCRIPT::checkInventory(objectID=%u, parentID=%u)", objectID, parentID);
@@ -2287,17 +2332,18 @@ void Script::ScriptExecutor::scriptCheckInventory() {
clearScriptError();
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
const GameObject *object = GameObjects::getObjectByIndex(objectID);
if (object == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::Continue;
}
_inventoryCheckResult = object->_sceneIndex == parentID;
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptSetSnapToTarget() {
+OpcodeResult Script::ScriptExecutor::scriptSetSnapToTarget() {
const uint16 objectID = scriptReadValue16() - 0x400;
const bool enabled = scriptReadValue16() != 0;
debugC(kDebugScript, "SCRIPT::setSnapToTarget(objectID=%u, enabled=%u)", objectID, enabled);
@@ -2305,21 +2351,22 @@ void Script::ScriptExecutor::scriptSetSnapToTarget() {
clearScriptError();
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
GameObject *object = GameObjects::getObjectByIndex(objectID);
if (object == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::Continue;
}
if (object->_dataOffset == 0) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
object->_snapToTarget = enabled;
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptTestObjectAnimFrame() {
+OpcodeResult Script::ScriptExecutor::scriptTestObjectAnimFrame() {
// scriptTestObjectAnimFrame (1008:be91).
uint32 objectID = scriptReadValue32() - 0x400;
uint16 slotID = scriptReadValue16();
@@ -2332,46 +2379,48 @@ void Script::ScriptExecutor::scriptTestObjectAnimFrame() {
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
GameObject *object = GameObjects::getObjectByIndex(objectID);
if (object == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::Continue;
}
if (object->_dataOffset == 0) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
if (slotID < 1 || slotID > _engine->maxAnimSlots()) {
setScriptError(0x10);
- return;
+ return OpcodeResult::Continue;
}
const Common::Array<uint8> *blob = object->getAnimSlotBlob(slotID);
if (blob == nullptr || blob->empty()) {
setScriptError(0x10);
- return;
+ return OpcodeResult::Continue;
}
AnimBlobView view(*blob);
if (!view.isValid()) {
setScriptError(8);
- return;
+ return OpcodeResult::Continue;
}
const uint16 seqPos = view.sequencePosition();
_animBlobRangeTestResult = seqPos >= minFrame && seqPos <= maxFrame;
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptPrintStringRight() {
+OpcodeResult Script::ScriptExecutor::scriptPrintStringRight() {
// Opcode 0x30: scriptPrintString with flag=1 (vs opcode 0x0A with flag=0)
// which changes behaviour in the function.
debugC(kDebugScript, "SCRIPT::printStringRight()");
scriptPrintString(true);
endBuffering(_lastOpcodeTriggeredSkip);
+ return OpcodeResult::WaitForCallback;
}
-void Script::ScriptExecutor::scriptSetPaletteDarkness() {
+OpcodeResult Script::ScriptExecutor::scriptSetPaletteDarkness() {
// Binary (1008:ce0b): clamps value to 0..100, writes to sceneData+0x5205
// (darkenPercent), then calls applyPaletteInterpolation to update displayed palette.
int16 darkenPercent = (int16)scriptReadValue16();
@@ -2385,9 +2434,10 @@ void Script::ScriptExecutor::scriptSetPaletteDarkness() {
View1 *view = (View1 *)g_engine->findView("View1");
if (view)
view->_paletteDirty = true;
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptSetObjectShading() {
+OpcodeResult Script::ScriptExecutor::scriptSetObjectShading() {
// Opcode 0x32 (1008:b9ba): runtime+0x185 bHasShading (NOT clickable - misnamed in script docs).
const uint16 objectID = scriptReadValue16() - 0x400;
const uint16 hasShading = scriptReadValue16();
@@ -2396,21 +2446,22 @@ void Script::ScriptExecutor::scriptSetObjectShading() {
clearScriptError();
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
GameObject *object = GameObjects::getObjectByIndex(objectID);
if (object == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::Continue;
}
if (object->_dataOffset == 0) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
object->_hasShading = hasShading != 0;
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptSetObjectScaling() {
+OpcodeResult Script::ScriptExecutor::scriptSetObjectScaling() {
// Opcode 0x33 (1008:ba5b): runtime+0x186 bHasScaling (NOT visible - misnamed in script docs).
const uint16 objectID = scriptReadValue16() - 0x400;
const uint16 hasScaling = scriptReadValue16();
@@ -2419,21 +2470,22 @@ void Script::ScriptExecutor::scriptSetObjectScaling() {
clearScriptError();
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
GameObject *object = GameObjects::getObjectByIndex(objectID);
if (object == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::Continue;
}
if (object->_dataOffset == 0) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
object->_hasScaling = hasScaling != 0;
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptSetHotspotOverride() {
+OpcodeResult Script::ScriptExecutor::scriptSetHotspotOverride() {
// Opcode 0x34 scriptSetHotspotOverride (1008:ce40): scene+0x5BD1 hotspot remap table.
const uint16 v1 = scriptReadValue16() - 0x800;
const uint16 v2 = scriptReadValue16() - 0x800;
@@ -2443,16 +2495,17 @@ void Script::ScriptExecutor::scriptSetHotspotOverride() {
const uint16 maxHotspot = _engine->maxHotspots();
if (v1 < 1 || v1 > maxHotspot || v2 < 1 || v2 > maxHotspot) {
setScriptError(0x1e);
- return;
+ return OpcodeResult::Continue;
}
if (v1 == v2) {
g_engine->_hotspotOverrides[v1] = 0xFFFF;
} else {
g_engine->_hotspotOverrides[v1] = v2;
}
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptSetObjectBounds() {
+OpcodeResult Script::ScriptExecutor::scriptSetObjectBounds() {
// Opcode 0x35 scriptSetObjectBounds (1008:c19f): runtime+0x231..+0x238 attachment.
const uint16 objectID = scriptReadValue16() - 0x400;
const uint16 otherObjectID = scriptReadValue16() - 0x400;
@@ -2464,17 +2517,17 @@ void Script::ScriptExecutor::scriptSetObjectBounds() {
clearScriptError();
if (objectID < 1 || objectID > 0x200 || otherObjectID < 1 || otherObjectID > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
GameObject *object = GameObjects::getObjectByIndex(objectID);
GameObject *otherObject = GameObjects::getObjectByIndex(otherObjectID);
if (object == nullptr || otherObject == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::Continue;
}
if (object->_dataOffset == 0 || otherObject->_dataOffset == 0) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
if (objectID == otherObjectID) {
@@ -2490,16 +2543,17 @@ void Script::ScriptExecutor::scriptSetObjectBounds() {
object->_boundsAttachmentValue2 = value2;
object->_boundsAttachmentValue3 = value3;
}
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptDismissAllPanels() {
+OpcodeResult Script::ScriptExecutor::scriptDismissAllPanels() {
// Opcode 0x36 -> scriptDismissPanel (1008:d6dd): restore pending UI background only.
View1 *currentView = (View1 *)_engine->findView("View1");
if (currentView == nullptr)
- return;
+ return OpcodeResult::Continue;
if (currentView->_pendingPanelRequest == View1::kPanelRequestNone ||
!currentView->_uiBackgroundRestorePending) {
- return;
+ return OpcodeResult::Continue;
}
debugC(kDebugScript, "SCRIPT::dismissPanel()");
currentView->redraw();
@@ -2509,9 +2563,10 @@ void Script::ScriptExecutor::scriptDismissAllPanels() {
currentView->_isDialogueChoiceInputActive = false;
currentView->_activeInventoryItem = nullptr;
currentView->clearClickedButtonIndex();
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptResetToSceneScript() {
+OpcodeResult Script::ScriptExecutor::scriptResetToSceneScript() {
// scriptResetToSceneScript (1008:ad3e). Resets script execution
// context back to the current scene script at position 0.
debugC(kDebugScript, "SCRIPT::resetToSceneScript()");
@@ -2520,9 +2575,10 @@ void Script::ScriptExecutor::scriptResetToSceneScript() {
_scriptExecutionState = ScriptExecutionState::ExecutingSceneScript;
setCurrentSceneScriptAt(0);
_expectedEndLocation = 0;
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptLoadOverlayFont() {
+OpcodeResult Script::ScriptExecutor::scriptLoadOverlayFont() {
// scriptLoadOverlayFont (1008:d749). Loads a font resource for
// overlay text into the overlay font buffer.
uint8 resourceIndex = readByte();
@@ -2531,13 +2587,14 @@ void Script::ScriptExecutor::scriptLoadOverlayFont() {
if (!_engine->loadOverlayFont(resourceIndex, _executingScriptObjectId)) {
warning("Opcode 0x38: failed to load overlay font resource %u", resourceIndex);
}
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptAddOverlayTextEntry() {
+OpcodeResult Script::ScriptExecutor::scriptAddOverlayTextEntry() {
// Opcode 0x3A scriptAddOverlayTextEntry (1008:d82c).
View1 *currentView = (View1 *)_engine->findView("View1");
if (currentView == nullptr)
- return;
+ return OpcodeResult::Continue;
clearScriptError();
const uint16 x = scriptReadValue16();
@@ -2549,15 +2606,15 @@ void Script::ScriptExecutor::scriptAddOverlayTextEntry() {
if (!_overlayTextStageActive) {
setScriptError(0x21);
- return;
+ return OpcodeResult::Continue;
}
if (currentView->_overlayTextEntries.size() >= 10) {
setScriptError(0x22);
- return;
+ return OpcodeResult::Continue;
}
if (entryType != 1) {
setScriptError(0x23);
- return;
+ return OpcodeResult::Continue;
}
Common::StringArray strings;
@@ -2570,11 +2627,11 @@ void Script::ScriptExecutor::scriptAddOverlayTextEntry() {
}
if (strings.empty()) {
warning("Empty overlay text entry at offset %u", stringOffset);
- return;
+ return OpcodeResult::Continue;
}
if (strings[0].size() >= 0x29) {
setScriptError(0x24);
- return;
+ return OpcodeResult::Continue;
}
debugC(kDebugScript,
"Opcode 3A overlay text: rawPos=(%u,%u) align=%u textOffset=%u entryType=%u scriptObject=%u text=\"%s\"",
@@ -2585,30 +2642,33 @@ void Script::ScriptExecutor::scriptAddOverlayTextEntry() {
entry.alignment = alignment;
entry.text = strings[0];
currentView->addOverlayTextEntry(entry);
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptClearOverlayText() {
+OpcodeResult Script::ScriptExecutor::scriptClearOverlayText() {
debugC(kDebugScript, "SCRIPT::clearOverlayText()");
View1 *currentView = (View1 *)_engine->findView("View1");
if (currentView != nullptr) {
currentView->clearOverlayTextEntries();
}
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptFadeToBlack() {
+OpcodeResult Script::ScriptExecutor::scriptFadeToBlack() {
const uint16 fadeSpeed = scriptReadValue16();
debugC(kDebugScript, "SCRIPT::fadeToBlack(speed=%u)", fadeSpeed);
View1 *currentView = (View1 *)_engine->findView("View1");
// Binary (executeOpcodes 0x3C): skip fade when g_wHelpButtonDisabled is set.
if (currentView != nullptr && currentView->isHelpButtonDisabled()) {
- return;
+ return OpcodeResult::Continue;
}
if (currentView != nullptr && fadeSpeed != 0) {
currentView->startFadeToBlack(fadeSpeed);
}
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptFrameWait() {
+OpcodeResult Script::ScriptExecutor::scriptFrameWait() {
// The original interpreter stores a frame countdown that is decremented
// once per game tick, rather than using a wall-clock timer.
uint16 duration = scriptReadValue16();
@@ -2617,9 +2677,10 @@ void Script::ScriptExecutor::scriptFrameWait() {
startFrameWait(duration);
enterBlockingWaitCursor();
endBuffering(_lastOpcodeTriggeredSkip);
+ return OpcodeResult::WaitForCallback;
}
-void Script::ScriptExecutor::scriptSetPathfinding() {
+OpcodeResult Script::ScriptExecutor::scriptSetPathfinding() {
// scriptSetPathfinding (1008:c6d7). Sets/clears a pathfinding override.
// Index must be in range 200..0xEF (walkability values).
// Writes to scene data at index*5 + 0x4EA5 (enable byte) and +0x4EA6 (value).
@@ -2631,7 +2692,7 @@ void Script::ScriptExecutor::scriptSetPathfinding() {
(!active || Macs2Engine::isWalkabilityWalkable(overrideValue)) ? "yes" : "NO");
if (areaID < 200 || areaID > 0xEF) {
setScriptError(0x0D);
- return;
+ return OpcodeResult::Continue;
}
if (active) {
g_engine->setPathfindingOverride(areaID, overrideValue);
@@ -2639,9 +2700,10 @@ void Script::ScriptExecutor::scriptSetPathfinding() {
g_engine->removePathfindingOverride(areaID);
}
debugLogActorWalkState("after setPathfinding");
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptTestSceneAnimFrame() {
+OpcodeResult Script::ScriptExecutor::scriptTestSceneAnimFrame() {
// scriptTestSceneAnimFrame (1008:b78d).
uint32 sceneAnimIndex = scriptReadValue32() - 0x1000;
uint32 minFrame = scriptReadValue32();
@@ -2653,68 +2715,73 @@ void Script::ScriptExecutor::scriptTestSceneAnimFrame() {
if (sceneAnimIndex == 0 || sceneAnimIndex > _engine->_backgroundAnimationsBlobs.size()) {
setScriptError(8);
- return;
+ return OpcodeResult::Continue;
}
const BackgroundAnimationBlob &blob = _engine->_backgroundAnimationsBlobs[sceneAnimIndex - 1];
if (blob._blob.empty()) {
setScriptError(8);
- return;
+ return OpcodeResult::Continue;
}
AnimBlobView view(blob._blob);
if (!view.isValid()) {
setScriptError(8);
- return;
+ return OpcodeResult::Continue;
}
const uint16 seqPos = view.sequencePosition();
_animBlobRangeTestResult = seqPos >= minFrame && seqPos <= maxFrame;
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptEndOverlayText() {
+OpcodeResult Script::ScriptExecutor::scriptEndOverlayText() {
debugC(kDebugScript, "SCRIPT::endOverlayText()");
if (_overlayTextStageActive) {
_overlayTextStageActive = false;
}
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptFadeFromBlack() {
+OpcodeResult Script::ScriptExecutor::scriptFadeFromBlack() {
const uint16 fadeSpeed = scriptReadValue16();
debugC(kDebugScript, "SCRIPT::fadeFromBlack(speed = %u)", fadeSpeed);
View1 *currentView = (View1 *)_engine->findView("View1");
if (currentView != nullptr && currentView->isHelpButtonDisabled()) {
- return;
+ return OpcodeResult::Continue;
}
if (currentView != nullptr && fadeSpeed != 0) {
currentView->startFading(fadeSpeed);
}
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptLoadPcmSound() {
+OpcodeResult Script::ScriptExecutor::scriptLoadPcmSound() {
const uint8 resourceIndex = readByte();
Common::Array<uint8> soundData;
int rateHz = 0x1F40;
int headerSkip = 2;
if (!loadSoundResource(soundData, resourceIndex, rateHz, headerSkip)) {
warning("Opcode 0x3E: failed to load PCM sound resource %u", resourceIndex);
- return;
+ return OpcodeResult::Continue;
}
// Opcode 0x3E only installs the buffer; playback is opcode 0x40.
_engine->setCurrentSoundData(soundData, rateHz, headerSkip);
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptPlayPcmSound() {
+OpcodeResult Script::ScriptExecutor::scriptPlayPcmSound() {
// Binary (1000:0c7f): no error path; no-op when Sound Blaster disabled.
// Buffer was installed by opcode 0x3E; only start playback here.
if (_soundEnabled) {
if (!_engine->hasCurrentSoundData()) {
warning("Opcode 0x40: playPcmSound with no loaded sound data");
- return;
+ return OpcodeResult::Continue;
}
_engine->playSample();
}
+ return OpcodeResult::Continue;
}
-bool Script::ScriptExecutor::scriptWaitForSound() {
+OpcodeResult Script::ScriptExecutor::scriptWaitForSound() {
if (_soundEnabled && _soundSystemActive) {
_waitForPcmSound = true;
endTimer();
@@ -2723,42 +2790,44 @@ bool Script::ScriptExecutor::scriptWaitForSound() {
g_engine->isSamplePlaying() ? 1 : 0, g_engine->hasCurrentSoundData() ? 1 : 0);
debugLogActorWalkState("waitForSound start");
enterBlockingWaitCursor();
- return true;
+ return OpcodeResult::WaitForCallback;
}
debugC(kDebugScript, "SCRIPT::waitForSound skipped (soundEnabled=%d soundSystemActive=%d)",
_soundEnabled ? 1 : 0, _soundSystemActive ? 1 : 0);
- return false;
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptStopPcmSound() {
+OpcodeResult Script::ScriptExecutor::scriptStopPcmSound() {
if (_soundEnabled) {
_engine->stopSample();
}
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptLoadMusicSlot() {
+OpcodeResult Script::ScriptExecutor::scriptLoadMusicSlot() {
const uint16 slotID = scriptReadValue16();
const uint8 resourceIndex = readByte();
clearScriptError();
if (slotID < 1 || slotID > 2) {
setScriptError(0x27);
- return;
+ return OpcodeResult::Continue;
}
// Binary (1000:0cac): cannot reload slot while it is the active playing slot (error 0x28).
if (_activeMusicSlot == slotID) {
setScriptError(0x28);
- return;
+ return OpcodeResult::Continue;
}
Common::Array<uint8> slotData;
if (loadMusicResource(slotData, resourceIndex)) {
_musicSlots[slotID - 1] = Common::move(slotData);
}
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptPlayMusicSlot() {
+OpcodeResult Script::ScriptExecutor::scriptPlayMusicSlot() {
const uint16 slotID = scriptReadValue16();
const uint16 startMuted = scriptReadValue16();
const uint16 fadeParam = scriptReadValue16();
@@ -2766,12 +2835,12 @@ void Script::ScriptExecutor::scriptPlayMusicSlot() {
clearScriptError();
if (slotID < 1 || slotID > 2) {
setScriptError(0x27);
- return;
+ return OpcodeResult::Continue;
}
if (!_musicEnabled || !_soundSystemActive) {
_activeMusicSlot = slotID;
- return;
+ return OpcodeResult::Continue;
}
if (_activeMusicSlot != 0) {
@@ -2782,7 +2851,7 @@ void Script::ScriptExecutor::scriptPlayMusicSlot() {
if (_musicSlots[slotID - 1].empty()) {
warning("Opcode 0x44: playMusicSlot with empty slot %u", slotID);
_activeMusicSlot = slotID;
- return;
+ return OpcodeResult::Continue;
}
if (!_engine->getMusic()->playSongData(_musicSlots[slotID - 1])) {
@@ -2790,7 +2859,7 @@ void Script::ScriptExecutor::scriptPlayMusicSlot() {
_musicControlStep = 0;
_musicControlVolume = 0;
_activeMusicSlot = slotID;
- return;
+ return OpcodeResult::Continue;
}
if (startMuted == 0) {
@@ -2806,9 +2875,10 @@ void Script::ScriptExecutor::scriptPlayMusicSlot() {
}
_activeMusicSlot = slotID;
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptStopMusicSlot() {
+OpcodeResult Script::ScriptExecutor::scriptStopMusicSlot() {
const uint16 slotID = scriptReadValue16();
const uint16 stopImmediately = scriptReadValue16();
const uint16 fadeParam = scriptReadValue16();
@@ -2816,12 +2886,12 @@ void Script::ScriptExecutor::scriptStopMusicSlot() {
clearScriptError();
if (slotID < 1 || slotID > 2) {
setScriptError(0x27);
- return;
+ return OpcodeResult::Continue;
}
if (!_musicEnabled || !_soundSystemActive) {
_activeMusicSlot = 0;
- return;
+ return OpcodeResult::Continue;
}
if (_activeMusicSlot == slotID) {
@@ -2834,26 +2904,27 @@ void Script::ScriptExecutor::scriptStopMusicSlot() {
_activeMusicSlot = 0;
}
}
+ return OpcodeResult::Continue;
}
-bool Script::ScriptExecutor::scriptWaitForMusic() {
+OpcodeResult Script::ScriptExecutor::scriptWaitForMusic() {
if (_soundSystemActive && _musicEnabled) {
_waitForMusicControl = true;
endTimer();
endBuffering(_lastOpcodeTriggeredSkip);
enterBlockingWaitCursor();
- return true;
+ return OpcodeResult::WaitForCallback;
}
- return false;
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptFreeMusicSlot() {
+OpcodeResult Script::ScriptExecutor::scriptFreeMusicSlot() {
const uint16 slotID = scriptReadValue16();
clearScriptError();
if (slotID < 1 || slotID > 2) {
setScriptError(0x27);
- return;
+ return OpcodeResult::Continue;
}
if (_activeMusicSlot == slotID) {
@@ -2863,77 +2934,82 @@ void Script::ScriptExecutor::scriptFreeMusicSlot() {
_activeMusicSlot = 0;
}
_musicSlots[slotID - 1].clear();
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptGetObjectX() {
+OpcodeResult Script::ScriptExecutor::scriptGetObjectX() {
// Opcode 0x48 scriptGetObjectX (1008:d917): saves object field +0 (X).
int32 objectID = (int32)scriptReadValue32() - 0x400;
clearScriptError();
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
GameObject *object = GameObjects::getObjectByIndex((uint16)objectID);
if (object == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::Continue;
}
scriptSaveVariableHelper(object->_position.x);
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptGetObjectY() {
+OpcodeResult Script::ScriptExecutor::scriptGetObjectY() {
// Opcode 0x49 scriptGetObjectY (1008:d977): saves object field +2 (Y).
int32 objectID = (int32)scriptReadValue32() - 0x400;
clearScriptError();
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
GameObject *object = GameObjects::getObjectByIndex((uint16)objectID);
if (object == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::Continue;
}
scriptSaveVariableHelper(object->_position.y);
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptGetObjectField8() {
+OpcodeResult Script::ScriptExecutor::scriptGetObjectField8() {
// Opcode 0x4A scriptGetObjectField8 (1008:d9d8): saves object field +8.
int32 objectID = (int32)scriptReadValue32() - 0x400;
clearScriptError();
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
GameObject *object = GameObjects::getObjectByIndex((uint16)objectID);
if (object == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::Continue;
}
scriptSaveVariableHelper(object->_verticalOffsetScale);
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptGetObjectOrientation() {
+OpcodeResult Script::ScriptExecutor::scriptGetObjectOrientation() {
// Opcode 0x4B scriptGetObjectOrientation (1008:da3a): saves object field +6.
int32 objectID = (int32)scriptReadValue32() - 0x400;
clearScriptError();
if (objectID < 1 || objectID > 0x200) {
setScriptError(2);
- return;
+ return OpcodeResult::Continue;
}
GameObject *object = GameObjects::getObjectByIndex((uint16)objectID);
if (object == nullptr) {
setScriptError(0x19);
- return;
+ return OpcodeResult::Continue;
}
scriptSaveVariableHelper(object->_orientation);
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptClearActorInventory() {
+OpcodeResult Script::ScriptExecutor::scriptClearActorInventory() {
for (GameObject *object : GameObjects::instance()._objects) {
if (object != nullptr && object->_sceneIndex == Scenes::instance()._currentActorIndex + 0x400) {
object->_sceneIndex = 0;
@@ -2948,9 +3024,10 @@ void Script::ScriptExecutor::scriptClearActorInventory() {
currentView->_activeInventoryItem = nullptr;
}
}
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptSetPathfindingRemap() {
+OpcodeResult Script::ScriptExecutor::scriptSetPathfindingRemap() {
// scriptSetPathfindingRemap (1008:dafb). Writes to scene+value*5+0x4EA8.
const uint16 sourceValue = scriptReadValue16();
const uint16 targetValue = scriptReadValue16();
@@ -2959,27 +3036,29 @@ void Script::ScriptExecutor::scriptSetPathfindingRemap() {
if (sourceValue < AREA_OVERRIDE_MIN || sourceValue > AREA_OVERRIDE_MAX ||
targetValue < AREA_OVERRIDE_MIN || targetValue > AREA_OVERRIDE_MAX) {
setScriptError(0x0D);
- return;
+ return OpcodeResult::Continue;
}
g_engine->_areaOverrides[sourceValue - AREA_OVERRIDE_MIN] = targetValue;
+ return OpcodeResult::Continue;
}
-bool Script::ScriptExecutor::scriptWaitForAdlib() {
+OpcodeResult Script::ScriptExecutor::scriptWaitForAdlib() {
if (_soundSystemActive && _musicEnabled) {
_waitForAdlibReady = true;
endTimer();
endBuffering(_lastOpcodeTriggeredSkip);
enterBlockingWaitCursor();
- return true;
+ return OpcodeResult::WaitForCallback;
}
- return false;
+ return OpcodeResult::Continue;
}
-void Script::ScriptExecutor::scriptFreePcmSound() {
+OpcodeResult Script::ScriptExecutor::scriptFreePcmSound() {
debugC(kDebugScript, "SCRIPT::freePcmSound()");
if (_soundEnabled)
_engine->stopSample();
_engine->clearCurrentSoundData();
+ return OpcodeResult::Continue;
}
void ScriptExecutor::setOpcodeTable(const OpcodeEntry *table, uint size) {
@@ -2994,511 +3073,92 @@ const char *ScriptExecutor::opcodeName(uint8 opcode) const {
return name ? name : "?";
}
-OpcodeResult ScriptExecutor::opSetVar() {
- scriptSetVar();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetVarOr() {
- scriptSetVarOr();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opIfFalse() {
- scriptIfFalse();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opIfTrue() {
- scriptIfTrue();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opCompare() {
- if (!scriptCompare())
- return OpcodeResult::FinishScript;
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opIfInteraction() {
- scriptIfInteraction();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opEndIf() {
- scriptEndIf();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opElse() {
- scriptElse();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opNop09() {
- scriptNop09();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opPrintStringLeft() {
- scriptPrintStringLeft();
- return OpcodeResult::WaitForCallback;
-}
-
-OpcodeResult ScriptExecutor::opMoveObject() {
- scriptMoveObject();
- if (hasScriptError())
- return OpcodeResult::FinishScript;
- // Binary: after scriptMoveObject(), exits if position >= end.
- if (_stream->pos() >= _stream->size())
- return OpcodeResult::FinishScript;
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opChangeScene() {
- const ExecutionResult result = scriptChangeScene();
- if (result == ExecutionResult::WaitingForCallback)
- return OpcodeResult::WaitForCallback;
- return OpcodeResult::ReturnFinished;
-}
-
-OpcodeResult ScriptExecutor::opShowDialogue() {
- const ExecutionResult dialogueResult = scriptShowDialogue();
- if (dialogueResult == ExecutionResult::WaitingForCallback)
- return OpcodeResult::WaitForCallback;
- return OpcodeResult::FinishScript;
-}
-
-OpcodeResult ScriptExecutor::opChangeAnimation() {
- scriptChangeAnimation();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opFrameWait() {
- scriptFrameWait();
- return OpcodeResult::WaitForCallback;
-}
-
-OpcodeResult ScriptExecutor::opWalkToPosition() {
- scriptWalkToPosition();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opWaitForWalk() {
- const ExecutionResult result = scriptWaitForWalk();
- if (result == ExecutionResult::WaitingForCallback)
- return OpcodeResult::WaitForCallback;
- return OpcodeResult::ReturnFinished;
-}
-
-OpcodeResult ScriptExecutor::opSetPathfinding() {
- scriptSetPathfinding();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSkipUntil14() {
- scriptSkipUntil14();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSkipWord() {
- scriptSkipWord();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opClearDialogueChoices() {
- scriptClearDialogueChoices();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opAddDialogueChoice() {
- scriptAddDialogueChoice();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opShowDialogueChoice() {
- const ExecutionResult result = scriptShowDialogueChoice();
- if (result == ExecutionResult::WaitingForCallback)
- return OpcodeResult::WaitForCallback;
- return OpcodeResult::ReturnFinished;
-}
-
-OpcodeResult ScriptExecutor::opDismissPanel() {
- const ExecutionResult result = scriptDismissPanel();
- if (result == ExecutionResult::WaitingForCallback)
- return OpcodeResult::WaitForCallback;
- return OpcodeResult::ReturnFinished;
-}
-
-OpcodeResult ScriptExecutor::opWalkToAndPickup() {
- scriptWalkToAndPickup();
- return OpcodeResult::FinishScript;
-}
-
-OpcodeResult ScriptExecutor::opSetPickupFrames() {
- scriptSetPickupFrames();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetupObject() {
- scriptSetupObject();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetSkippable() {
- scriptSetSkippable();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opClearSkippable() {
- scriptClearSkippable();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opPlayAnimation() {
- scriptPlayAnimation();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opTestPathfinding() {
- scriptTestPathfinding();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetYOffset() {
- scriptSetYOffset();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetMotion() {
- scriptSetMotion();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetOrientation() {
- scriptSetOrientation();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opMoveToPosition() {
- scriptMoveToPosition();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opAddValues() {
- scriptAddValues();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSubValues() {
- scriptSubValues();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opLoadSpecialAnim() {
- scriptLoadSpecialAnim();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetDirection() {
- scriptSetDirection();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opStopAnimation() {
- scriptStopAnimation();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opOpenInventory() {
- // Binary executeOpcodes @ 1008:e0f9 (full + demo):
- // scriptOpenInventory();
- // g_wScriptPosition = g_wScriptEndPosition;
- // g_wExecutingScriptObjectId = 0x201;
- // goto LAB_1008_e3bd;
- // loadNextScript() honors ScriptObjectId > 0x200 and will not start/continue
- // the object-script walk (including when 0x29 ran from the scene script).
- scriptOpenInventory();
- _stream->seek(_stream->size(), SEEK_SET);
- _executingObjectIndex = 0x201;
- _executingScriptObjectId = 0x201;
- return OpcodeResult::FinishScript;
-}
-
-OpcodeResult ScriptExecutor::opLoadObjectAnim() {
- scriptLoadObjectAnim();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opCheckObjectData() {
- scriptCheckObjectData();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opCheckInventory() {
- scriptCheckInventory();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetSnapToTarget() {
- scriptSetSnapToTarget();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opTestSceneAnimFrame() {
- scriptTestSceneAnimFrame();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opTestObjectAnimFrame() {
- scriptTestObjectAnimFrame();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opPrintStringRight() {
- scriptPrintStringRight();
- return OpcodeResult::WaitForCallback;
-}
-
-OpcodeResult ScriptExecutor::opSetPaletteDarkness() {
- scriptSetPaletteDarkness();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetObjectShading() {
- scriptSetObjectShading();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetObjectScaling() {
- scriptSetObjectScaling();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetHotspotOverride() {
- scriptSetHotspotOverride();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetObjectBounds() {
- scriptSetObjectBounds();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opDismissAllPanels() {
- scriptDismissAllPanels();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opResetToSceneScript() {
- scriptResetToSceneScript();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opLoadOverlayFont() {
- scriptLoadOverlayFont();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opEndOverlayText() {
- scriptEndOverlayText();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opAddOverlayTextEntry() {
- scriptAddOverlayTextEntry();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opClearOverlayText() {
- scriptClearOverlayText();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opFadeToBlack() {
- scriptFadeToBlack();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opFadeFromBlack() {
- scriptFadeFromBlack();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opLoadPcmSound() {
- scriptLoadPcmSound();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opFreePcmSound() {
- scriptFreePcmSound();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opPlayPcmSound() {
- scriptPlayPcmSound();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opWaitForSound() {
- if (scriptWaitForSound())
- return OpcodeResult::WaitForCallback;
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opStopPcmSound() {
- scriptStopPcmSound();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opLoadMusicSlot() {
- scriptLoadMusicSlot();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opPlayMusicSlot() {
- scriptPlayMusicSlot();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opStopMusicSlot() {
- scriptStopMusicSlot();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opFreeMusicSlot() {
- scriptFreeMusicSlot();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opWaitForMusic() {
- if (scriptWaitForMusic())
- return OpcodeResult::WaitForCallback;
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opGetObjectX() {
- scriptGetObjectX();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opGetObjectY() {
- scriptGetObjectY();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opGetObjectField8() {
- scriptGetObjectField8();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opGetObjectOrientation() {
- scriptGetObjectOrientation();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opClearActorInventory() {
- scriptClearActorInventory();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opSetPathfindingRemap() {
- scriptSetPathfindingRemap();
- return OpcodeResult::Continue;
-}
-
-OpcodeResult ScriptExecutor::opWaitForAdlib() {
- if (scriptWaitForAdlib())
- return OpcodeResult::WaitForCallback;
- return OpcodeResult::Continue;
-}
-
// Script dialect v1 opcode table (indexed by primary opcode byte).
// Later dialects can install an alternate table via setOpcodeTable().
const ScriptExecutor::OpcodeEntry ScriptExecutor::kV1OpcodeTable[] = {
{nullptr, nullptr},
- {"setVar", &ScriptExecutor::opSetVar},
- {"setVarOr", &ScriptExecutor::opSetVarOr},
- {"ifFalse", &ScriptExecutor::opIfFalse},
- {"ifTrue", &ScriptExecutor::opIfTrue},
- {"compare", &ScriptExecutor::opCompare},
- {"ifInteraction", &ScriptExecutor::opIfInteraction},
- {"endIf", &ScriptExecutor::opEndIf},
- {"else", &ScriptExecutor::opElse},
- {"nop09", &ScriptExecutor::opNop09},
- {"printStringLeft", &ScriptExecutor::opPrintStringLeft},
- {"moveObject", &ScriptExecutor::opMoveObject},
- {"changeScene", &ScriptExecutor::opChangeScene},
- {"showDialogue", &ScriptExecutor::opShowDialogue},
- {"changeAnimation", &ScriptExecutor::opChangeAnimation},
- {"frameWait", &ScriptExecutor::opFrameWait},
- {"walkToPosition", &ScriptExecutor::opWalkToPosition},
- {"waitForWalk", &ScriptExecutor::opWaitForWalk},
- {"setPathfinding", &ScriptExecutor::opSetPathfinding},
- {"skipUntil14", &ScriptExecutor::opSkipUntil14},
- {"skipWord", &ScriptExecutor::opSkipWord},
- {"clearDialogueChoices", &ScriptExecutor::opClearDialogueChoices},
- {"addDialogueChoice", &ScriptExecutor::opAddDialogueChoice},
- {"showDialogueChoice", &ScriptExecutor::opShowDialogueChoice},
- {"dismissPanel", &ScriptExecutor::opDismissPanel},
- {"walkToAndPickup", &ScriptExecutor::opWalkToAndPickup},
- {"setPickupFrames", &ScriptExecutor::opSetPickupFrames},
- {"setupObject", &ScriptExecutor::opSetupObject},
- {"setSkippable", &ScriptExecutor::opSetSkippable},
- {"clearSkippable", &ScriptExecutor::opClearSkippable},
- {"playAnimation", &ScriptExecutor::opPlayAnimation},
- {"testPathfinding", &ScriptExecutor::opTestPathfinding},
- {"setYOffset", &ScriptExecutor::opSetYOffset},
- {"setMotion", &ScriptExecutor::opSetMotion},
- {"setOrientation", &ScriptExecutor::opSetOrientation},
- {"moveToPosition", &ScriptExecutor::opMoveToPosition},
- {"addValues", &ScriptExecutor::opAddValues},
- {"subValues", &ScriptExecutor::opSubValues},
- {"loadSpecialAnim", &ScriptExecutor::opLoadSpecialAnim},
- {"setDirection", &ScriptExecutor::opSetDirection},
- {"stopAnimation", &ScriptExecutor::opStopAnimation},
- {"openInventory", &ScriptExecutor::opOpenInventory},
- {"loadObjectAnim", &ScriptExecutor::opLoadObjectAnim},
- {"checkObjectData", &ScriptExecutor::opCheckObjectData},
- {"checkInventory", &ScriptExecutor::opCheckInventory},
- {"setSnapToTarget", &ScriptExecutor::opSetSnapToTarget},
- {"testSceneAnimFrame", &ScriptExecutor::opTestSceneAnimFrame},
- {"testObjectAnimFrame", &ScriptExecutor::opTestObjectAnimFrame},
- {"printStringRight", &ScriptExecutor::opPrintStringRight},
- {"setPaletteDarkness", &ScriptExecutor::opSetPaletteDarkness},
- {"setObjectShading", &ScriptExecutor::opSetObjectShading},
- {"setObjectScaling", &ScriptExecutor::opSetObjectScaling},
- {"setHotspotOverride", &ScriptExecutor::opSetHotspotOverride},
- {"setObjectBounds", &ScriptExecutor::opSetObjectBounds},
- {"dismissAllPanels", &ScriptExecutor::opDismissAllPanels},
- {"resetToSceneScript", &ScriptExecutor::opResetToSceneScript},
- {"loadOverlayFont", &ScriptExecutor::opLoadOverlayFont},
- {"endOverlayText", &ScriptExecutor::opEndOverlayText},
- {"addOverlayTextEntry", &ScriptExecutor::opAddOverlayTextEntry},
- {"clearOverlayText", &ScriptExecutor::opClearOverlayText},
- {"fadeToBlack", &ScriptExecutor::opFadeToBlack},
- {"fadeFromBlack", &ScriptExecutor::opFadeFromBlack},
- {"loadPcmSound", &ScriptExecutor::opLoadPcmSound},
- {"freePcmSound", &ScriptExecutor::opFreePcmSound},
- {"playPcmSound", &ScriptExecutor::opPlayPcmSound},
- {"waitForSound", &ScriptExecutor::opWaitForSound},
- {"stopPcmSound", &ScriptExecutor::opStopPcmSound},
- {"loadMusicSlot", &ScriptExecutor::opLoadMusicSlot},
- {"playMusicSlot", &ScriptExecutor::opPlayMusicSlot},
- {"stopMusicSlot", &ScriptExecutor::opStopMusicSlot},
- {"freeMusicSlot", &ScriptExecutor::opFreeMusicSlot},
- {"waitForMusic", &ScriptExecutor::opWaitForMusic},
- {"getObjectX", &ScriptExecutor::opGetObjectX},
- {"getObjectY", &ScriptExecutor::opGetObjectY},
- {"getObjectField8", &ScriptExecutor::opGetObjectField8},
- {"getObjectOrientation", &ScriptExecutor::opGetObjectOrientation},
- {"clearActorInventory", &ScriptExecutor::opClearActorInventory},
- {"setPathfindingRemap", &ScriptExecutor::opSetPathfindingRemap},
- {"waitForAdlib", &ScriptExecutor::opWaitForAdlib}
+ {"setVar", &ScriptExecutor::scriptSetVar},
+ {"setVarOr", &ScriptExecutor::scriptSetVarOr},
+ {"ifFalse", &ScriptExecutor::scriptIfFalse},
+ {"ifTrue", &ScriptExecutor::scriptIfTrue},
+ {"compare", &ScriptExecutor::scriptCompare},
+ {"ifInteraction", &ScriptExecutor::scriptIfInteraction},
+ {"endIf", &ScriptExecutor::scriptEndIf},
+ {"else", &ScriptExecutor::scriptElse},
+ {"nop09", &ScriptExecutor::scriptNop09},
+ {"printStringLeft", &ScriptExecutor::scriptPrintStringLeft},
+ {"moveObject", &ScriptExecutor::scriptMoveObject},
+ {"changeScene", &ScriptExecutor::scriptChangeScene},
+ {"showDialogue", &ScriptExecutor::scriptShowDialogue},
+ {"changeAnimation", &ScriptExecutor::scriptChangeAnimation},
+ {"frameWait", &ScriptExecutor::scriptFrameWait},
+ {"walkToPosition", &ScriptExecutor::scriptWalkToPosition},
+ {"waitForWalk", &ScriptExecutor::scriptWaitForWalk},
+ {"setPathfinding", &ScriptExecutor::scriptSetPathfinding},
+ {"skipUntil14", &ScriptExecutor::scriptSkipUntil14},
+ {"skipWord", &ScriptExecutor::scriptSkipWord},
+ {"clearDialogueChoices", &ScriptExecutor::scriptClearDialogueChoices},
+ {"addDialogueChoice", &ScriptExecutor::scriptAddDialogueChoice},
+ {"showDialogueChoice", &ScriptExecutor::scriptShowDialogueChoice},
+ {"dismissPanel", &ScriptExecutor::scriptDismissPanel},
+ {"walkToAndPickup", &ScriptExecutor::scriptWalkToAndPickup},
+ {"setPickupFrames", &ScriptExecutor::scriptSetPickupFrames},
+ {"setupObject", &ScriptExecutor::scriptSetupObject},
+ {"setSkippable", &ScriptExecutor::scriptSetSkippable},
+ {"clearSkippable", &ScriptExecutor::scriptClearSkippable},
+ {"playAnimation", &ScriptExecutor::scriptPlayAnimation},
+ {"testPathfinding", &ScriptExecutor::scriptTestPathfinding},
+ {"setYOffset", &ScriptExecutor::scriptSetYOffset},
+ {"setMotion", &ScriptExecutor::scriptSetMotion},
+ {"setOrientation", &ScriptExecutor::scriptSetOrientation},
+ {"moveToPosition", &ScriptExecutor::scriptMoveToPosition},
+ {"addValues", &ScriptExecutor::scriptAddValues},
+ {"subValues", &ScriptExecutor::scriptSubValues},
+ {"loadSpecialAnim", &ScriptExecutor::scriptLoadSpecialAnim},
+ {"setDirection", &ScriptExecutor::scriptSetDirection},
+ {"stopAnimation", &ScriptExecutor::scriptStopAnimation},
+ {"openInventory", &ScriptExecutor::scriptOpenInventory},
+ {"loadObjectAnim", &ScriptExecutor::scriptLoadObjectAnim},
+ {"checkObjectData", &ScriptExecutor::scriptCheckObjectData},
+ {"checkInventory", &ScriptExecutor::scriptCheckInventory},
+ {"setSnapToTarget", &ScriptExecutor::scriptSetSnapToTarget},
+ {"testSceneAnimFrame", &ScriptExecutor::scriptTestSceneAnimFrame},
+ {"testObjectAnimFrame", &ScriptExecutor::scriptTestObjectAnimFrame},
+ {"printStringRight", &ScriptExecutor::scriptPrintStringRight},
+ {"setPaletteDarkness", &ScriptExecutor::scriptSetPaletteDarkness},
+ {"setObjectShading", &ScriptExecutor::scriptSetObjectShading},
+ {"setObjectScaling", &ScriptExecutor::scriptSetObjectScaling},
+ {"setHotspotOverride", &ScriptExecutor::scriptSetHotspotOverride},
+ {"setObjectBounds", &ScriptExecutor::scriptSetObjectBounds},
+ {"dismissAllPanels", &ScriptExecutor::scriptDismissAllPanels},
+ {"resetToSceneScript", &ScriptExecutor::scriptResetToSceneScript},
+ {"loadOverlayFont", &ScriptExecutor::scriptLoadOverlayFont},
+ {"endOverlayText", &ScriptExecutor::scriptEndOverlayText},
+ {"addOverlayTextEntry", &ScriptExecutor::scriptAddOverlayTextEntry},
+ {"clearOverlayText", &ScriptExecutor::scriptClearOverlayText},
+ {"fadeToBlack", &ScriptExecutor::scriptFadeToBlack},
+ {"fadeFromBlack", &ScriptExecutor::scriptFadeFromBlack},
+ {"loadPcmSound", &ScriptExecutor::scriptLoadPcmSound},
+ {"freePcmSound", &ScriptExecutor::scriptFreePcmSound},
+ {"playPcmSound", &ScriptExecutor::scriptPlayPcmSound},
+ {"waitForSound", &ScriptExecutor::scriptWaitForSound},
+ {"stopPcmSound", &ScriptExecutor::scriptStopPcmSound},
+ {"loadMusicSlot", &ScriptExecutor::scriptLoadMusicSlot},
+ {"playMusicSlot", &ScriptExecutor::scriptPlayMusicSlot},
+ {"stopMusicSlot", &ScriptExecutor::scriptStopMusicSlot},
+ {"freeMusicSlot", &ScriptExecutor::scriptFreeMusicSlot},
+ {"waitForMusic", &ScriptExecutor::scriptWaitForMusic},
+ {"getObjectX", &ScriptExecutor::scriptGetObjectX},
+ {"getObjectY", &ScriptExecutor::scriptGetObjectY},
+ {"getObjectField8", &ScriptExecutor::scriptGetObjectField8},
+ {"getObjectOrientation", &ScriptExecutor::scriptGetObjectOrientation},
+ {"clearActorInventory", &ScriptExecutor::scriptClearActorInventory},
+ {"setPathfindingRemap", &ScriptExecutor::scriptSetPathfindingRemap},
+ {"waitForAdlib", &ScriptExecutor::scriptWaitForAdlib}
};
const uint ScriptExecutor::kV1OpcodeTableSize = ARRAYSIZE(ScriptExecutor::kV1OpcodeTable);
-ExecutionResult Script::ScriptExecutor::executeOpcodes() {
+OpcodeResult Script::ScriptExecutor::executeOpcodes() {
debugC(kDebugScript, "----- Scripting function entered - scene: %.2x 1014: %.2x 1012: %.2x", Scenes::instance()._currentSceneIndex, _isSceneInitRun, _repeatRunFlag);
_isRunningScript = true;
// Confirmed: no interrupt mechanism exists. Wait states (frameWait, walkTarget,
@@ -3534,7 +3194,7 @@ ExecutionResult Script::ScriptExecutor::executeOpcodes() {
if (scriptDebuggerShouldPause()) {
_debugPaused = true;
_state = ExecutorState::Executing; // preserve state for resume
- return ExecutionResult::WaitingForCallback;
+ return OpcodeResult::WaitForCallback;
}
#endif
const byte opcode1 = readByte(); // [bp - 1h]
@@ -3567,9 +3227,9 @@ ExecutionResult Script::ScriptExecutor::executeOpcodes() {
const OpcodeResult opcodeResult = (this->*(entry->handler))();
if (opcodeResult == OpcodeResult::WaitForCallback)
- return ExecutionResult::WaitingForCallback;
+ return OpcodeResult::WaitForCallback;
if (opcodeResult == OpcodeResult::ReturnFinished)
- return ExecutionResult::ScriptFinished;
+ return OpcodeResult::ReturnFinished;
endBuffering(_lastOpcodeTriggeredSkip);
if (opcodeResult == OpcodeResult::FinishScript)
@@ -3579,7 +3239,7 @@ ExecutionResult Script::ScriptExecutor::executeOpcodes() {
if (hasScriptError())
recordScriptErrorPosition();
debugC(kDebugScript, "----- Scripting function left");
- return ExecutionResult::ScriptFinished;
+ return OpcodeResult::FinishScript;
}
void ScriptExecutor::run(bool firstRun) {
diff --git a/engines/macs2/scriptexecutor.h b/engines/macs2/scriptexecutor.h
index 59af9b705bf..30fd3af2ff1 100644
--- a/engines/macs2/scriptexecutor.h
+++ b/engines/macs2/scriptexecutor.h
@@ -67,25 +67,17 @@ enum class ExecutorState {
WaitingForCallback
};
-enum class ExecutionResult {
- // We have finished executing the script
- ScriptFinished,
-
- // We are now waiting for a callback
- WaitingForCallback
-};
-
-/** Per-opcode handler result for table-driven dispatch in executeOpcodes(). */
+/** Result of a script opcode handler / executeOpcodes(). */
enum class OpcodeResult {
/** Continue with the next opcode in the current script. */
Continue,
- /** Break the opcode loop, then return ScriptFinished (clears running flag). */
+ /** Break the opcode loop, then finish (clears running flag). */
FinishScript,
- /** Return WaitingForCallback immediately (leaves running flag set). */
+ /** Pause execution and wait for an external callback (leaves running flag set). */
WaitForCallback,
/**
- * Return ScriptFinished immediately (leaves running flag set).
- * Matches legacy `return scriptXxx()` paths such as changeScene / waitForWalk.
+ * Finish immediately (leaves running flag set).
+ * Used by changeScene / waitForWalk / showDialogueChoice / dismissPanel.
*/
ReturnFinished
};
@@ -133,164 +125,84 @@ private:
#ifdef DEMACS2
public:
#endif
- void scriptSetVar();
- void scriptSetVarOr();
- void scriptIfTrue();
- void scriptIfFalse();
- bool scriptCompare();
- void scriptIfInteraction();
- void scriptEndIf();
- void scriptElse();
- void scriptNop09();
- void scriptPrintStringLeft();
- void scriptMoveObject();
- ExecutionResult scriptChangeScene();
- ExecutionResult scriptShowDialogue();
- void scriptWalkToPosition();
- ExecutionResult scriptWaitForWalk();
- void scriptSkipWord();
- void scriptClearDialogueChoices();
- void scriptAddDialogueChoice();
- ExecutionResult scriptShowDialogueChoice();
- ExecutionResult scriptDismissPanel();
- void scriptWalkToAndPickup();
- bool scriptSetPickupFrames();
- void scriptSetupObject();
- void scriptSetSkippable();
- void scriptClearSkippable();
- void scriptPlayAnimation();
- void scriptTestPathfinding();
- void scriptSetYOffset();
- void scriptSetMotion();
- bool scriptSetOrientation();
- void scriptMoveToPosition();
- void scriptAddValues();
- void scriptSubValues();
- void scriptLoadSpecialAnim();
- void scriptSetDirection();
- void scriptStopAnimation();
- void scriptOpenInventory();
- void scriptLoadObjectAnim();
- void scriptCheckObjectData();
- void scriptCheckInventory();
- void scriptSetSnapToTarget();
- void scriptTestObjectAnimFrame();
- void scriptPrintStringRight();
- void scriptSetPaletteDarkness();
- void scriptSetObjectShading();
- void scriptSetObjectScaling();
- void scriptSetHotspotOverride();
- void scriptSetObjectBounds();
- void scriptDismissAllPanels();
- void scriptResetToSceneScript();
- void scriptLoadOverlayFont();
- void scriptAddOverlayTextEntry();
- void scriptClearOverlayText();
- void scriptFadeToBlack();
- void scriptLoadPcmSound();
- void scriptPlayPcmSound();
- bool scriptWaitForSound();
- void scriptStopPcmSound();
- void scriptLoadMusicSlot();
- void scriptPlayMusicSlot();
- void scriptStopMusicSlot();
- bool scriptWaitForMusic();
- void scriptFreeMusicSlot();
- void scriptGetObjectX();
- void scriptGetObjectY();
- void scriptGetObjectField8();
- void scriptGetObjectOrientation();
- void scriptClearActorInventory();
- void scriptSetPathfindingRemap();
- bool scriptWaitForAdlib();
- void scriptSkipUntil14();
- void scriptChangeAnimation();
- void scriptFrameWait();
- void scriptSetPathfinding();
- void scriptTestSceneAnimFrame();
- void scriptEndOverlayText();
- void scriptFadeFromBlack();
- void scriptFreePcmSound();
-
- // Table-driven opcode wrappers (return OpcodeResult for executeOpcodes).
- OpcodeResult opSetVar();
- OpcodeResult opSetVarOr();
- OpcodeResult opIfFalse();
- OpcodeResult opIfTrue();
- OpcodeResult opCompare();
- OpcodeResult opIfInteraction();
- OpcodeResult opEndIf();
- OpcodeResult opElse();
- OpcodeResult opNop09();
- OpcodeResult opPrintStringLeft();
- OpcodeResult opMoveObject();
- OpcodeResult opChangeScene();
- OpcodeResult opShowDialogue();
- OpcodeResult opChangeAnimation();
- OpcodeResult opFrameWait();
- OpcodeResult opWalkToPosition();
- OpcodeResult opWaitForWalk();
- OpcodeResult opSetPathfinding();
- OpcodeResult opSkipUntil14();
- OpcodeResult opSkipWord();
- OpcodeResult opClearDialogueChoices();
- OpcodeResult opAddDialogueChoice();
- OpcodeResult opShowDialogueChoice();
- OpcodeResult opDismissPanel();
- OpcodeResult opWalkToAndPickup();
- OpcodeResult opSetPickupFrames();
- OpcodeResult opSetupObject();
- OpcodeResult opSetSkippable();
- OpcodeResult opClearSkippable();
- OpcodeResult opPlayAnimation();
- OpcodeResult opTestPathfinding();
- OpcodeResult opSetYOffset();
- OpcodeResult opSetMotion();
- OpcodeResult opSetOrientation();
- OpcodeResult opMoveToPosition();
- OpcodeResult opAddValues();
- OpcodeResult opSubValues();
- OpcodeResult opLoadSpecialAnim();
- OpcodeResult opSetDirection();
- OpcodeResult opStopAnimation();
- OpcodeResult opOpenInventory();
- OpcodeResult opLoadObjectAnim();
- OpcodeResult opCheckObjectData();
- OpcodeResult opCheckInventory();
- OpcodeResult opSetSnapToTarget();
- OpcodeResult opTestSceneAnimFrame();
- OpcodeResult opTestObjectAnimFrame();
- OpcodeResult opPrintStringRight();
- OpcodeResult opSetPaletteDarkness();
- OpcodeResult opSetObjectShading();
- OpcodeResult opSetObjectScaling();
- OpcodeResult opSetHotspotOverride();
- OpcodeResult opSetObjectBounds();
- OpcodeResult opDismissAllPanels();
- OpcodeResult opResetToSceneScript();
- OpcodeResult opLoadOverlayFont();
- OpcodeResult opEndOverlayText();
- OpcodeResult opAddOverlayTextEntry();
- OpcodeResult opClearOverlayText();
- OpcodeResult opFadeToBlack();
- OpcodeResult opFadeFromBlack();
- OpcodeResult opLoadPcmSound();
- OpcodeResult opFreePcmSound();
- OpcodeResult opPlayPcmSound();
- OpcodeResult opWaitForSound();
- OpcodeResult opStopPcmSound();
- OpcodeResult opLoadMusicSlot();
- OpcodeResult opPlayMusicSlot();
- OpcodeResult opStopMusicSlot();
- OpcodeResult opFreeMusicSlot();
- OpcodeResult opWaitForMusic();
- OpcodeResult opGetObjectX();
- OpcodeResult opGetObjectY();
- OpcodeResult opGetObjectField8();
- OpcodeResult opGetObjectOrientation();
- OpcodeResult opClearActorInventory();
- OpcodeResult opSetPathfindingRemap();
- OpcodeResult opWaitForAdlib();
+ OpcodeResult scriptSetVar();
+ OpcodeResult scriptSetVarOr();
+ OpcodeResult scriptIfTrue();
+ OpcodeResult scriptIfFalse();
+ OpcodeResult scriptCompare();
+ OpcodeResult scriptIfInteraction();
+ OpcodeResult scriptEndIf();
+ OpcodeResult scriptElse();
+ OpcodeResult scriptNop09();
+ OpcodeResult scriptPrintStringLeft();
+ OpcodeResult scriptMoveObject();
+ OpcodeResult scriptChangeScene();
+ OpcodeResult scriptShowDialogue();
+ OpcodeResult scriptWalkToPosition();
+ OpcodeResult scriptWaitForWalk();
+ OpcodeResult scriptSkipWord();
+ OpcodeResult scriptClearDialogueChoices();
+ OpcodeResult scriptAddDialogueChoice();
+ OpcodeResult scriptShowDialogueChoice();
+ OpcodeResult scriptDismissPanel();
+ OpcodeResult scriptWalkToAndPickup();
+ OpcodeResult scriptSetPickupFrames();
+ OpcodeResult scriptSetupObject();
+ OpcodeResult scriptSetSkippable();
+ OpcodeResult scriptClearSkippable();
+ OpcodeResult scriptPlayAnimation();
+ OpcodeResult scriptTestPathfinding();
+ OpcodeResult scriptSetYOffset();
+ OpcodeResult scriptSetMotion();
+ OpcodeResult scriptSetOrientation();
+ OpcodeResult scriptMoveToPosition();
+ OpcodeResult scriptAddValues();
+ OpcodeResult scriptSubValues();
+ OpcodeResult scriptLoadSpecialAnim();
+ OpcodeResult scriptSetDirection();
+ OpcodeResult scriptStopAnimation();
+ OpcodeResult scriptOpenInventory();
+ OpcodeResult scriptLoadObjectAnim();
+ OpcodeResult scriptCheckObjectData();
+ OpcodeResult scriptCheckInventory();
+ OpcodeResult scriptSetSnapToTarget();
+ OpcodeResult scriptTestObjectAnimFrame();
+ OpcodeResult scriptPrintStringRight();
+ OpcodeResult scriptSetPaletteDarkness();
+ OpcodeResult scriptSetObjectShading();
+ OpcodeResult scriptSetObjectScaling();
+ OpcodeResult scriptSetHotspotOverride();
+ OpcodeResult scriptSetObjectBounds();
+ OpcodeResult scriptDismissAllPanels();
+ OpcodeResult scriptResetToSceneScript();
+ OpcodeResult scriptLoadOverlayFont();
+ OpcodeResult scriptAddOverlayTextEntry();
+ OpcodeResult scriptClearOverlayText();
+ OpcodeResult scriptFadeToBlack();
+ OpcodeResult scriptLoadPcmSound();
+ OpcodeResult scriptPlayPcmSound();
+ OpcodeResult scriptWaitForSound();
+ OpcodeResult scriptStopPcmSound();
+ OpcodeResult scriptLoadMusicSlot();
+ OpcodeResult scriptPlayMusicSlot();
+ OpcodeResult scriptStopMusicSlot();
+ OpcodeResult scriptWaitForMusic();
+ OpcodeResult scriptFreeMusicSlot();
+ OpcodeResult scriptGetObjectX();
+ OpcodeResult scriptGetObjectY();
+ OpcodeResult scriptGetObjectField8();
+ OpcodeResult scriptGetObjectOrientation();
+ OpcodeResult scriptClearActorInventory();
+ OpcodeResult scriptSetPathfindingRemap();
+ OpcodeResult scriptWaitForAdlib();
+ OpcodeResult scriptSkipUntil14();
+ OpcodeResult scriptChangeAnimation();
+ OpcodeResult scriptFrameWait();
+ OpcodeResult scriptSetPathfinding();
+ OpcodeResult scriptTestSceneAnimFrame();
+ OpcodeResult scriptEndOverlayText();
+ OpcodeResult scriptFadeFromBlack();
+ OpcodeResult scriptFreePcmSound();
inline void scriptUnimplementedOpcode(const char *source, uint16 opcode) {
debug("Unimplemented opcode (%s): %.2x.", source, opcode);
@@ -517,7 +429,7 @@ public:
Common::Point getCharPosition();
// executeOpcodes (1008:db56): the opcode dispatch loop for the current script.
- ExecutionResult executeOpcodes();
+ OpcodeResult executeOpcodes();
// Will execute the script and any object scripts until execution should be stopped
// TODO: Consider if we should let the executor also figure out where to get the
More information about the Scummvm-git-logs
mailing list