[Scummvm-git-logs] scummvm master -> 4f45ad0d22b6d8fdeb00bcb64ffe03c12d582f09
neuromancer
noreply at scummvm.org
Fri Apr 3 07:07:17 UTC 2026
This automated email contains information about 2 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
acc405f699 FREESCAPE: initial implementation to allow to play castle master 2 in zx
4f45ad0d22 FREESCAPE: reworked opl music for eclipse and make sure music is restarted properly
Commit: acc405f69906f71b146f0283d7db9daf3bd98437
https://github.com/scummvm/scummvm/commit/acc405f69906f71b146f0283d7db9daf3bd98437
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-03T09:06:59+02:00
Commit Message:
FREESCAPE: initial implementation to allow to play castle master 2 in zx
Changed paths:
engines/freescape/detection.cpp
engines/freescape/freescape.h
engines/freescape/games/castle/castle.cpp
engines/freescape/games/castle/castle.h
engines/freescape/games/castle/zx.cpp
engines/freescape/language/instruction.cpp
engines/freescape/loaders/8bitBinaryLoader.cpp
engines/freescape/metaengine.cpp
engines/freescape/movement.cpp
diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index e7ba5d6056b..8192c43fa8d 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -35,6 +35,7 @@ static const PlainGameDescriptor freescapeGames[] = {
{"totaleclipse", "Total Eclipse"},
{"totaleclipse2", "Total Eclipse 2"},
{"castlemaster", "Castle Master"},
+ {"castlemaster2", "Castle Master 2: The Crypt"},
{0, 0}
};
@@ -985,6 +986,16 @@ static const ADGameDescription gameDescriptions[] = {
ADGF_NO_FLAGS,
GUIO5(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_WASD_CONTROLS)
},
+ // Castle Master 2: The Crypt
+ {
+ "castlemaster2",
+ "",
+ AD_ENTRY1s("castlemaster2.zx.data", "a470acb51c7925e921b0de056cdb880f", 35328),
+ Common::EN_ANY,
+ Common::kPlatformZX,
+ ADGF_NO_FLAGS,
+ GUIO3(GUIO_NOMIDI, GUIO_RENDERZX, GAMEOPTION_WASD_CONTROLS)
+ },
// 3D Construction Kit games
{
"3dkit",
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index 3ea2f51b933..e51774ec742 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -205,6 +205,7 @@ public:
bool isEclipse() { return _targetName.hasPrefix("totaleclipse"); } // This will match Total Eclipse 1 and 2.
bool isEclipse2() { return _targetName.hasPrefix("totaleclipse2"); }
bool isCastle() { return _targetName.hasPrefix("castle"); }
+ bool isCastleMaster2() { return _targetName.hasPrefix("castlemaster2"); }
bool isAmiga() { return _gameDescription->platform == Common::kPlatformAmiga; }
bool isAtariST() { return _gameDescription->platform == Common::kPlatformAtariST; }
bool isDOS() { return _gameDescription->platform == Common::kPlatformDOS; }
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 286f76ac8ec..3454462399e 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -741,6 +741,10 @@ bool CastleEngine::triggerWinCondition() {
if (!_areaMap.contains(74))
return false;
gotoArea(74, 0);
+ } else if (isCastleMaster2()) {
+ // CM2: escape is script-driven (game boolean 3 bit 6).
+ // The escape animation plays from the current location.
+ return true;
} else {
_gameStateVars[31] = 10;
gotoArea(16, 136);
@@ -756,8 +760,11 @@ void CastleEngine::endGame() {
if (hasEscaped()) {
insertTemporaryMessage(_messagesList[5], INT_MIN);
- if (isDOS()) {
+ if (isDOS() && !isCastleMaster2()) {
drawFullscreenEndGameAndWait();
+ } else if (isCastleMaster2()) {
+ executeEscapeCameraSequence();
+ drawFullscreenGameOverAndWait();
} else
drawFullscreenGameOverAndWait();
} else {
@@ -768,6 +775,47 @@ void CastleEngine::endGame() {
_endGameKeyPressed = false;
}
+void CastleEngine::executeEscapeCameraSequence() {
+ // Escape camera animation from CM2 disassembly (L9fa1_escape_castle_sequence):
+ // 1. Rotate player to face yaw=180 (away from castle), pitch=0 (level)
+ // 2. Walk backward (increase Z) until leaving the map
+
+ // Step 1: Rotate toward yaw=180, pitch=0
+ float yawStep = (_yaw < 180.0f) ? 10.0f : -10.0f;
+ float pitchStep = (_pitch < 180.0f) ? -10.0f : 10.0f;
+
+ while (ABS(_yaw - 180.0f) > 5.0f || (ABS(_pitch) > 5.0f && ABS(_pitch - 360.0f) > 5.0f)) {
+ if (ABS(_yaw - 180.0f) > 5.0f)
+ _yaw += yawStep;
+ if (ABS(_pitch) > 5.0f && ABS(_pitch - 360.0f) > 5.0f)
+ _pitch += pitchStep;
+
+ if (_yaw < 0) _yaw += 360.0f;
+ if (_yaw >= 360.0f) _yaw -= 360.0f;
+ if (_pitch < 0) _pitch += 360.0f;
+ if (_pitch >= 360.0f) _pitch -= 360.0f;
+
+ _cameraFront = directionToVector(_yaw, _pitch, false);
+ drawFrame();
+ _gfx->flipBuffer();
+ g_system->updateScreen();
+ g_system->delayMillis(40);
+ }
+
+ _yaw = 180.0f;
+ _pitch = 0.0f;
+ _cameraFront = directionToVector(_yaw, _pitch, false);
+
+ // Step 2: Move backward (increase Z) until out of bounds
+ for (int i = 0; i < 40; i++) {
+ _position.setValue(2, _position.z() + 256.0f);
+ drawFrame();
+ _gfx->flipBuffer();
+ g_system->updateScreen();
+ g_system->delayMillis(40);
+ }
+}
+
bool CastleEngine::hasEscaped() {
if (isDOS())
return _currentArea && _currentArea->getAreaID() == 74;
@@ -1975,7 +2023,9 @@ void CastleEngine::borderScreen() {
surface->free();
delete surface;
}
- selectCharacterScreen();
+
+ if (!isCastleMaster2())
+ selectCharacterScreen();
}
void CastleEngine::drawOption() {
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index d5f734a45be..51e390bafea 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -59,6 +59,7 @@ public:
void initGameState() override;
bool triggerWinCondition() override;
void endGame() override;
+ void executeEscapeCameraSequence();
void drawInfoMenu() override;
void loadAssets() override;
diff --git a/engines/freescape/games/castle/zx.cpp b/engines/freescape/games/castle/zx.cpp
index 1aaaf37408a..d1979d13b36 100644
--- a/engines/freescape/games/castle/zx.cpp
+++ b/engines/freescape/games/castle/zx.cpp
@@ -46,69 +46,114 @@ void CastleEngine::loadAssetsZXFullGame() {
uint8 r, g, b;
Common::Array<Graphics::ManagedSurface *> chars;
- file.open("castlemaster.zx.title");
+ Common::Path titleFile(isCastleMaster2() ? "castlemaster2.zx.title" : "castlemaster.zx.title");
+ Common::Path borderFile(isCastleMaster2() ? "castlemaster2.zx.border" : "castlemaster.zx.border");
+ Common::Path dataFile(isCastleMaster2() ? "castlemaster2.zx.data" : "castlemaster.zx.data");
+
+ file.open(titleFile);
if (file.isOpen()) {
_title = loadAndConvertScrImage(&file);
} else
- error("Unable to find castlemaster.zx.title");
+ error("Unable to find %s", titleFile.toString().c_str());
file.close();
- file.open("castlemaster.zx.border");
+ file.open(borderFile);
if (file.isOpen()) {
_border = loadAndConvertScrImage(&file);
} else
- error("Unable to find castlemaster.zx.border");
+ error("Unable to find %s", borderFile.toString().c_str());
file.close();
- file.open("castlemaster.zx.data");
+ file.open(dataFile);
if (!file.isOpen())
- error("Failed to open castlemaster.zx.data");
-
- loadMessagesVariableSize(&file, 0x4bd, 71);
- switch (_language) {
- case Common::ES_ESP:
- loadRiddles(&file, 0x1458, 9);
- load8bitBinary(&file, 0x6aa9, 16);
- loadSpeakerFxZX(&file, 0xca0, 0xcdc);
-
- file.seek(0x1228);
- for (int i = 0; i < 90; i++) {
- Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
- surface->create(8, 8, Graphics::PixelFormat::createFormatCLUT8());
- chars.push_back(loadFrame(&file, surface, 1, 8, 1));
- }
- _font = Font(chars);
- _font.setCharWidth(9);
- _fontLoaded = true;
-
- break;
- case Common::EN_ANY:
- if (_variant & GF_ZX_RETAIL) {
- loadRiddles(&file, 0x1448, 9);
- load8bitBinary(&file, 0x6a3b, 16);
- loadSpeakerFxZX(&file, 0xc91, 0xccd);
- file.seek(0x1219);
- } else if (_variant & GF_ZX_DISC) {
- loadRiddles(&file, 0x1457, 9);
- load8bitBinary(&file, 0x6a9b, 16);
+ error("Failed to open %s", dataFile.toString().c_str());
+
+ if (isCastleMaster2()) {
+ // CM2 game text (L6cb9_game_text) and area names (L6f49_area_names)
+ // are both fixed-size: 16 bytes per entry (1-byte indent flag + 15
+ // chars of text). The indent flag is used by Ld01c_draw_string for
+ // display positioning but is not part of the text content.
+ // Game text: 41 entries at offset 0x02B9.
+ // Area names: 40 entries at offset 0x0549.
+ // Both are loaded into _messagesList (game text at indices 0-40,
+ // area names at indices 41-80).
+ file.seek(0x02b9);
+ for (int i = 0; i < 41; i++) {
+ file.readByte(); // skip indent flag
+ char buf[16];
+ file.read(buf, 15);
+ buf[15] = '\0';
+ _messagesList.push_back(Common::String(buf));
+ }
+
+ // Area names follow immediately (L6f49_area_names, 40 entries).
+ for (int i = 0; i < 40; i++) {
+ file.readByte(); // skip indent flag
+ char buf[16];
+ file.read(buf, 15);
+ buf[15] = '\0';
+ _messagesList.push_back(Common::String(buf));
+ }
+
+ load8bitBinary(&file, 0x6682, 16);
+ loadSpeakerFxZX(&file, 0x0bbf, 0x0bfb);
+
+ file.seek(0x1147);
+ for (int i = 0; i < 90; i++) {
+ Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+ surface->create(8, 8, Graphics::PixelFormat::createFormatCLUT8());
+ chars.push_back(loadFrame(&file, surface, 1, 8, 1));
+ }
+ _font = Font(chars);
+ _font.setCharWidth(9);
+ _fontLoaded = true;
+ } else {
+ loadMessagesVariableSize(&file, 0x4bd, 71);
+ switch (_language) {
+ case Common::ES_ESP:
+ loadRiddles(&file, 0x1458, 9);
+ load8bitBinary(&file, 0x6aa9, 16);
loadSpeakerFxZX(&file, 0xca0, 0xcdc);
+
file.seek(0x1228);
- } else {
- error("Unknown Castle Master ZX variant");
- }
- for (int i = 0; i < 90; i++) {
- Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
- surface->create(8, 8, Graphics::PixelFormat::createFormatCLUT8());
- chars.push_back(loadFrame(&file, surface, 1, 8, 1));
- }
- _font = Font(chars);
- _font.setCharWidth(9);
- _fontLoaded = true;
-
- break;
- default:
- error("Language not supported");
- break;
+ for (int i = 0; i < 90; i++) {
+ Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+ surface->create(8, 8, Graphics::PixelFormat::createFormatCLUT8());
+ chars.push_back(loadFrame(&file, surface, 1, 8, 1));
+ }
+ _font = Font(chars);
+ _font.setCharWidth(9);
+ _fontLoaded = true;
+
+ break;
+ case Common::EN_ANY:
+ if (_variant & GF_ZX_RETAIL) {
+ loadRiddles(&file, 0x1448, 9);
+ load8bitBinary(&file, 0x6a3b, 16);
+ loadSpeakerFxZX(&file, 0xc91, 0xccd);
+ file.seek(0x1219);
+ } else if (_variant & GF_ZX_DISC) {
+ loadRiddles(&file, 0x1457, 9);
+ load8bitBinary(&file, 0x6a9b, 16);
+ loadSpeakerFxZX(&file, 0xca0, 0xcdc);
+ file.seek(0x1228);
+ } else {
+ error("Unknown Castle Master ZX variant");
+ }
+ for (int i = 0; i < 90; i++) {
+ Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+ surface->create(8, 8, Graphics::PixelFormat::createFormatCLUT8());
+ chars.push_back(loadFrame(&file, surface, 1, 8, 1));
+ }
+ _font = Font(chars);
+ _font.setCharWidth(9);
+ _fontLoaded = true;
+
+ break;
+ default:
+ error("Language not supported");
+ break;
+ }
}
loadColorPalette();
@@ -118,10 +163,18 @@ void CastleEngine::loadAssetsZXFullGame() {
_gfx->readFromPalette(7, r, g, b);
uint32 white = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
- _keysBorderFrames.push_back(loadFrameWithHeader(&file, _variant & GF_ZX_DISC ? 0xe06 : 0xdf7, red, white));
+ if (isCastleMaster2()) {
+ _keysBorderFrames.push_back(loadFrameWithHeader(&file, 0x0d25, red, white));
+ } else {
+ _keysBorderFrames.push_back(loadFrameWithHeader(&file, _variant & GF_ZX_DISC ? 0xe06 : 0xdf7, red, white));
+ }
uint32 green = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0, 0xff, 0);
- _spiritsMeterIndicatorFrame = loadFrameWithHeader(&file, _variant & GF_ZX_DISC ? 0xe5e : 0xe4f, green, white);
+ if (isCastleMaster2()) {
+ _spiritsMeterIndicatorFrame = loadFrameWithHeader(&file, 0x0d7d, green, white);
+ } else {
+ _spiritsMeterIndicatorFrame = loadFrameWithHeader(&file, _variant & GF_ZX_DISC ? 0xe5e : 0xe4f, green, white);
+ }
_gfx->readFromPalette(4, r, g, b);
uint32 front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
@@ -132,17 +185,27 @@ void CastleEngine::loadAssetsZXFullGame() {
background->create(backgroundWidth * 8, backgroundHeight, _gfx->_texturePixelFormat);
background->fillRect(Common::Rect(0, 0, backgroundWidth * 8, backgroundHeight), 0);
- file.seek(_variant & GF_ZX_DISC ? 0xfd3 : 0xfc4);
+ if (isCastleMaster2()) {
+ file.seek(0x0ef2);
+ } else {
+ file.seek(_variant & GF_ZX_DISC ? 0xfd3 : 0xfc4);
+ }
_background = loadFrame(&file, background, backgroundWidth, backgroundHeight, front);
_gfx->readFromPalette(6, r, g, b);
uint32 yellow = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
uint32 black = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0, 0, 0);
- _strenghtBackgroundFrame = loadFrameWithHeader(&file, _variant & GF_ZX_DISC ? 0xee6 : 0xed7, yellow, black);
- _strenghtBarFrame = loadFrameWithHeader(&file, _variant & GF_ZX_DISC ? 0xf72 : 0xf63, yellow, black);
- _strenghtWeightsFrames = loadFramesWithHeader(&file, _variant & GF_ZX_DISC ? 0xf92 : 0xf83, 4, yellow, black);
-
- _flagFrames = loadFramesWithHeader(&file, (_variant & GF_ZX_DISC ? 0x10e4 + 15 : 0x10e4), 4, green, black);
+ if (isCastleMaster2()) {
+ _strenghtBackgroundFrame = loadFrameWithHeader(&file, 0x0e05, yellow, black);
+ _strenghtBarFrame = loadFrameWithHeader(&file, 0x0e91, yellow, black);
+ _strenghtWeightsFrames = loadFramesWithHeader(&file, 0x0eb1, 4, yellow, black);
+ _flagFrames = loadFramesWithHeader(&file, 0x1012, 4, green, black);
+ } else {
+ _strenghtBackgroundFrame = loadFrameWithHeader(&file, _variant & GF_ZX_DISC ? 0xee6 : 0xed7, yellow, black);
+ _strenghtBarFrame = loadFrameWithHeader(&file, _variant & GF_ZX_DISC ? 0xf72 : 0xf63, yellow, black);
+ _strenghtWeightsFrames = loadFramesWithHeader(&file, _variant & GF_ZX_DISC ? 0xf92 : 0xf83, 4, yellow, black);
+ _flagFrames = loadFramesWithHeader(&file, (_variant & GF_ZX_DISC ? 0x10e4 + 15 : 0x10e4), 4, green, black);
+ }
file.skip(24);
int thunderWidth = 4;
@@ -161,27 +224,29 @@ void CastleEngine::loadAssetsZXFullGame() {
_thunderFrames[0]->copyRectToSurface(*thunderFrame, 0, 0, Common::Rect(0, 0, thunderWidth * 8 / 2, thunderHeight));
_thunderFrames[1]->copyRectToSurface(*thunderFrame, 0, 0, Common::Rect(thunderWidth * 8 / 2, 0, thunderWidth * 8, thunderHeight));
- Graphics::Surface *tmp;
- tmp = loadBundledImage("castle_riddle_top_frame");
- _riddleTopFrame = new Graphics::ManagedSurface;
- _riddleTopFrame->copyFrom(*tmp);
- tmp->free();
- delete tmp;
- _riddleTopFrame->convertToInPlace(_gfx->_texturePixelFormat);
-
- tmp = loadBundledImage("castle_riddle_background_frame");
- _riddleBackgroundFrame = new Graphics::ManagedSurface();
- _riddleBackgroundFrame->copyFrom(*tmp);
- tmp->free();
- delete tmp;
- _riddleBackgroundFrame->convertToInPlace(_gfx->_texturePixelFormat);
-
- tmp = loadBundledImage("castle_riddle_bottom_frame");
- _riddleBottomFrame = new Graphics::ManagedSurface();
- _riddleBottomFrame->copyFrom(*tmp);
- tmp->free();
- delete tmp;
- _riddleBottomFrame->convertToInPlace(_gfx->_texturePixelFormat);
+ if (!isCastleMaster2()) {
+ Graphics::Surface *tmp;
+ tmp = loadBundledImage("castle_riddle_top_frame");
+ _riddleTopFrame = new Graphics::ManagedSurface;
+ _riddleTopFrame->copyFrom(*tmp);
+ tmp->free();
+ delete tmp;
+ _riddleTopFrame->convertToInPlace(_gfx->_texturePixelFormat);
+
+ tmp = loadBundledImage("castle_riddle_background_frame");
+ _riddleBackgroundFrame = new Graphics::ManagedSurface();
+ _riddleBackgroundFrame->copyFrom(*tmp);
+ tmp->free();
+ delete tmp;
+ _riddleBackgroundFrame->convertToInPlace(_gfx->_texturePixelFormat);
+
+ tmp = loadBundledImage("castle_riddle_bottom_frame");
+ _riddleBottomFrame = new Graphics::ManagedSurface();
+ _riddleBottomFrame->copyFrom(*tmp);
+ tmp->free();
+ delete tmp;
+ _riddleBottomFrame->convertToInPlace(_gfx->_texturePixelFormat);
+ }
}
void CastleEngine::drawZXUI(Graphics::Surface *surface) {
diff --git a/engines/freescape/language/instruction.cpp b/engines/freescape/language/instruction.cpp
index a0457b18707..3309ad2a57a 100644
--- a/engines/freescape/language/instruction.cpp
+++ b/engines/freescape/language/instruction.cpp
@@ -670,6 +670,12 @@ void FreescapeEngine::executeMakeVisible(FCLInstruction &instruction) {
if (!obj) {
obj = _areaMap[255]->objectWithID(objectID);
if (!obj) {
+ if (isCastleMaster2()) {
+ // CM2 Z80 code (Lb286_find_object_by_id) returns silently
+ // when object is not found â the caller skips the rule.
+ debugC(1, kFreescapeDebugCode, "obj %d not found in area %d nor in global area, skipping", objectID, areaID);
+ return;
+ }
if (!isCastle() || !isDemo())
error("obj %d does not exists in area %d nor in the global one!", objectID, areaID);
return;
diff --git a/engines/freescape/loaders/8bitBinaryLoader.cpp b/engines/freescape/loaders/8bitBinaryLoader.cpp
index 870a90bf01a..a69e1804c72 100644
--- a/engines/freescape/loaders/8bitBinaryLoader.cpp
+++ b/engines/freescape/loaders/8bitBinaryLoader.cpp
@@ -308,7 +308,9 @@ Object *FreescapeEngine::load8bitObject(Common::SeekableReadStream *file) {
byte rawFlagsAndType = readField(file, 8);
debugC(1, kFreescapeDebugParser, "Raw object data flags and type: %d", rawFlagsAndType);
- ObjectType objectType = (ObjectType)(rawFlagsAndType & 0x1F);
+ // Castle Master uses a 4-bit type field (0x0F mask); other Freescape games use 5 bits (0x1F).
+ byte typeMask = isCastle() ? 0x0F : 0x1F;
+ ObjectType objectType = (ObjectType)(rawFlagsAndType & typeMask);
if (objectType == ObjectType::kGroupType)
return load8bitGroup(file, rawFlagsAndType);
@@ -643,7 +645,9 @@ Area *FreescapeEngine::load8bitArea(Common::SeekableReadStream *file, uint16 nco
uint8 skyColor = areaFlags & 15;
uint8 groundColor = areaFlags >> 4;
- if (groundColor == 0)
+ // In Castle Master, areaFlags holds sky/floor texture IDs, not colors.
+ // A value of 0 means no sky/floor (indoor area), not "missing color".
+ if (groundColor == 0 && !isCastle())
groundColor = 255;
uint8 usualBackgroundColor = 0;
@@ -744,7 +748,7 @@ Area *FreescapeEngine::load8bitArea(Common::SeekableReadStream *file, uint16 nco
if (isAmiga())
name = _messagesList[idx + 51];
else if (isSpectrum() || isCPC() || isC64())
- name = areaNumber == 255 ? "GLOBAL" : _messagesList[idx + 16];
+ name = areaNumber == 255 ? "GLOBAL" : _messagesList[idx + (isCastleMaster2() ? 41 : 16)];
else
name = _messagesList[idx + 41];
diff --git a/engines/freescape/metaengine.cpp b/engines/freescape/metaengine.cpp
index 77b1feb7baa..5970768aad1 100644
--- a/engines/freescape/metaengine.cpp
+++ b/engines/freescape/metaengine.cpp
@@ -207,7 +207,7 @@ Common::Error FreescapeMetaEngine::createInstance(OSystem *syst, Engine **engine
*engine = (Engine *)new Freescape::DarkEngine(syst, gd);
} else if (Common::String(gd->gameId) == "totaleclipse" || Common::String(gd->gameId) == "totaleclipse2") {
*engine = (Engine *)new Freescape::EclipseEngine(syst, gd);
- } else if (Common::String(gd->gameId) == "castlemaster") {
+ } else if (Common::String(gd->gameId) == "castlemaster" || Common::String(gd->gameId) == "castlemaster2") {
*engine = (Engine *)new Freescape::CastleEngine(syst, gd);
} else
*engine = new Freescape::FreescapeEngine(syst, gd);
diff --git a/engines/freescape/movement.cpp b/engines/freescape/movement.cpp
index 1773634a6a5..1a64593c57b 100644
--- a/engines/freescape/movement.cpp
+++ b/engines/freescape/movement.cpp
@@ -199,38 +199,102 @@ void FreescapeEngine::traverseEntrance(uint16 entranceID) {
}
void FreescapeEngine::activate() {
+ // Castle Master interaction following the original Z80 code (Lb464 in
+ // castlemaster2-annotated.asm). activate() is only called by Castle Master.
+ //
+ // 1. Find object under pointer via ray cast (equivalent to Lb607)
+ // 2. Reject objects where any dimension > room_scale * 5 (line 10216-10223)
+ // 3. Compute manhattan distance to object center (lines 10227-10266)
+ // 4. Normalize by room_scale if scale >= 2 (lines 10269-10277)
+ // 5. If distance >= 300, show "OUT OF REACH" (lines 10279-10283)
+ // 6. Otherwise execute the object's rules
+
Common::Point center(_viewArea.left + _viewArea.width() / 2, _viewArea.top + _viewArea.height() / 2);
- // Convert to normalized coordinates [-1, 1]
float ndcX = (2.0f * (_crossairPosition.x - _viewArea.left) / _viewArea.width()) - 1.0f;
float ndcY = 1.0f - (2.0f * (_crossairPosition.y - _viewArea.top) / _viewArea.height());
- // Calculate angular offsets using perspective projection
float fovHorizontalRad = (float)(75.0f * M_PI / 180.0f);
- float aspectRatio = isCastle() ? 1.6 : 2.18;
+ float aspectRatio = 1.6;
float fovVerticalRad = 2.0f * atan(tan(fovHorizontalRad / 2.0f) / aspectRatio);
- // Convert NDC to angle offset
float angleOffsetX = atan(ndcX * tan(fovHorizontalRad / 2.0f)) * 180.0f / M_PI;
float angleOffsetY = atan(ndcY * tan(fovVerticalRad / 2.0f)) * 180.0f / M_PI;
Math::Vector3d direction = directionToVector(_pitch + angleOffsetY, _yaw - angleOffsetX, false);
Math::Ray ray(_position, direction);
- Object *interacted = _currentArea->checkCollisionRay(ray, 1250.0 / _currentArea->getScale());
+ // Use a wide ray to find the object under the pointer. The original code
+ // (Lb607_find_object_under_pointer) works in screen-space projected
+ // coordinates, so it finds objects regardless of their 3D thickness.
+ // The manhattan distance check below handles reachability.
+ Object *interacted = _currentArea->checkCollisionRay(ray, 8000.0 / _currentArea->getScale());
+
if (interacted) {
GeometricObject *gobj = (GeometricObject *)interacted;
- debugC(1, kFreescapeDebugMove, "Interact with object %d with flags %x", gobj->getObjectID(), gobj->getObjectFlags());
+ int scale = _currentArea->getScale();
+ // Z80 (line 10216-10222): rejects objects with any raw dimension > scale * 5.
+ // ScummVM sizes are in raw * 32 / scale units, so the equivalent
+ // threshold is: scale * 5 * 32 / scale = 160.
+ int maxSize = 160;
+ Math::Vector3d objOrigin = gobj->getOrigin();
+ Math::Vector3d objSize = gobj->getSize();
+
+ debugC(1, kFreescapeDebugMove, "Activate: found object %d (type %d) at (%.0f,%.0f,%.0f) size (%.0f,%.0f,%.0f) scale %d",
+ gobj->getObjectID(), (int)gobj->getType(),
+ objOrigin.x(), objOrigin.y(), objOrigin.z(),
+ objSize.x(), objSize.y(), objSize.z(), scale);
+
+ // Reject objects where any dimension > room_scale * 5
+ bool tooLarge = false;
+ for (int i = 0; i < 3; i++) {
+ if ((int)objSize.getValue(i) > maxSize) {
+ tooLarge = true;
+ break;
+ }
+ }
- if (!gobj->_conditionSource.empty())
- debugC(1, kFreescapeDebugMove, "Must use interact = true when executing: %s", gobj->_conditionSource.c_str());
+ if (tooLarge) {
+ debugC(1, kFreescapeDebugMove, "Activate: object %d too large (maxSize %d), skipping", gobj->getObjectID(), maxSize);
+ } else {
+ // Manhattan distance to object center, matching the Z80 code at
+ // Lb486 (castlemaster2-annotated.asm lines 10215-10281).
+ //
+ // Z80 works in *64 space: center = raw_pos*64 + raw_size*32,
+ // player = raw*64+32.
+ // ScummVM stores positions as raw*32/scale (after load8bitObject
+ // multiplies by 32 and load8bitArea divides by scale).
+ // So ScummVM center = origin + size/2, and the Z80 distance
+ // (after its /scale normalization) maps to:
+ // dist_z80_normalized = dist_scummvm * 2 * scale / scale
+ // = dist_scummvm * 2
+ // Threshold in Z80 (post-normalization) = 300, so in ScummVM = 150.
+ int manhattanDist = 0;
+ for (int i = 0; i < 3; i++) {
+ float objCenter = objOrigin.getValue(i) + objSize.getValue(i) / 2.0f;
+ float playerPos = _position.getValue(i);
+ int diff = (int)ABS(objCenter - playerPos);
+ debugC(1, kFreescapeDebugMove, "Activate: axis %d: objCenter=%.1f playerPos=%.1f |diff|=%d",
+ i, objCenter, playerPos, diff);
+ manhattanDist += diff;
+ }
- executeObjectConditions(gobj, false, false, true);
+ debugC(1, kFreescapeDebugMove, "Activate: object %d manhattanDist=%d (threshold 150)",
+ gobj->getObjectID(), manhattanDist);
+
+ if (manhattanDist >= 150) {
+ clearTemporalMessages();
+ insertTemporaryMessage(_outOfReachMessage, _countdown - 2);
+ } else {
+ debugC(1, kFreescapeDebugMove, "Activate: interacting with object %d", gobj->getObjectID());
+ executeObjectConditions(gobj, false, false, true);
+ }
+ }
} else {
+ debugC(1, kFreescapeDebugMove, "Activate: no object found under pointer");
if (!_outOfReachMessage.empty()) {
clearTemporalMessages();
insertTemporaryMessage(_outOfReachMessage, _countdown - 2);
}
}
- //executeLocalGlobalConditions(true, false, false); // Only execute "on shot" room/global conditions
}
Commit: 4f45ad0d22b6d8fdeb00bcb64ffe03c12d582f09
https://github.com/scummvm/scummvm/commit/4f45ad0d22b6d8fdeb00bcb64ffe03c12d582f09
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-03T09:06:59+02:00
Commit Message:
FREESCAPE: reworked opl music for eclipse and make sure music is restarted properly
Changed paths:
engines/freescape/games/eclipse/ay.music.cpp
engines/freescape/games/eclipse/eclipse.cpp
engines/freescape/games/eclipse/eclipse.h
engines/freescape/games/eclipse/eclipse.musicdata.h
engines/freescape/games/eclipse/opl.music.cpp
engines/freescape/games/eclipse/opl.music.h
diff --git a/engines/freescape/games/eclipse/ay.music.cpp b/engines/freescape/games/eclipse/ay.music.cpp
index 56f7c7e9946..c30c09523d6 100644
--- a/engines/freescape/games/eclipse/ay.music.cpp
+++ b/engines/freescape/games/eclipse/ay.music.cpp
@@ -143,6 +143,7 @@ EclipseAYMusicPlayer::~EclipseAYMusicPlayer() {
// ============================================================================
void EclipseAYMusicPlayer::startMusic() {
+ stopMusic();
_mixer->playStream(Audio::Mixer::kMusicSoundType, &_handle, toAudioStream(),
-1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO);
setupSong();
diff --git a/engines/freescape/games/eclipse/eclipse.cpp b/engines/freescape/games/eclipse/eclipse.cpp
index 77ab804c326..9f2056f4c45 100644
--- a/engines/freescape/games/eclipse/eclipse.cpp
+++ b/engines/freescape/games/eclipse/eclipse.cpp
@@ -109,7 +109,40 @@ EclipseEngine::EclipseEngine(OSystem *syst, const ADGameDescription *gd) : Frees
_flashlightOn = false;
}
+void EclipseEngine::stopBackgroundMusic() {
+ if (_playerOPLMusic)
+ _playerOPLMusic->stopMusic();
+ if (_playerAYMusic)
+ _playerAYMusic->stopMusic();
+ if (_playerC64Music)
+ _playerC64Music->stopMusic();
+ if (_mixer)
+ _mixer->stopHandle(_musicHandle);
+}
+
+void EclipseEngine::restartBackgroundMusic() {
+ if (isC64() && _playerC64Music) {
+ _playerC64Music->startMusic();
+ } else if ((isCPC() || isSpectrum()) && _playerAYMusic) {
+ _playerAYMusic->startMusic();
+ } else if (isDOS() && _playerOPLMusic) {
+ _playerOPLMusic->startMusic();
+ } else if (isAtariST() && !_musicData.empty()) {
+ if (_mixer)
+ _mixer->stopHandle(_musicHandle);
+ Audio::AudioStream *musicStream = makeEclipseAtariMusicStream(
+ _musicData.data(), _musicData.size(), 1);
+ if (musicStream) {
+ _mixer->playStream(Audio::Mixer::kMusicSoundType,
+ &_musicHandle, musicStream);
+ }
+ } else {
+ playMusic("Total Eclipse Theme");
+ }
+}
+
EclipseEngine::~EclipseEngine() {
+ stopBackgroundMusic();
delete _playerOPLMusic;
delete _playerAYMusic;
delete _playerC64Music;
@@ -132,15 +165,7 @@ void EclipseEngine::initGameState() {
_lastHeartIndicatorFrame = 1;
_resting = false;
_flashlightOn = false;
-
- if (isC64() && _playerC64Music)
- _playerC64Music->startMusic();
- else if ((isCPC() || isSpectrum()) && _playerAYMusic)
- _playerAYMusic->startMusic();
- else if (isDOS() && _playerOPLMusic)
- _playerOPLMusic->startMusic();
- else
- playMusic("Total Eclipse Theme");
+ restartBackgroundMusic();
}
void EclipseEngine::loadAssets() {
@@ -237,6 +262,10 @@ bool EclipseEngine::triggerWinCondition() {
}
void EclipseEngine::endGame() {
+ bool enteringEndArea = (_gameStateControl == kFreescapeGameStateEnd && !_endGamePlayerEndArea);
+ if (enteringEndArea)
+ restartBackgroundMusic();
+
FreescapeEngine::endGame();
if (!_endGamePlayerEndArea)
diff --git a/engines/freescape/games/eclipse/eclipse.h b/engines/freescape/games/eclipse/eclipse.h
index b78a709517c..9ba254de665 100644
--- a/engines/freescape/games/eclipse/eclipse.h
+++ b/engines/freescape/games/eclipse/eclipse.h
@@ -125,6 +125,8 @@ public:
EclipseAYMusicPlayer *_playerAYMusic;
EclipseOPLMusicPlayer *_playerOPLMusic;
+ void restartBackgroundMusic();
+ void stopBackgroundMusic();
// Atari ST UI sprites (extracted from binary, pre-converted to target format)
Font _fontScore; // Font B (10 score digit glyphs, 4-plane at $249BE)
diff --git a/engines/freescape/games/eclipse/eclipse.musicdata.h b/engines/freescape/games/eclipse/eclipse.musicdata.h
index 466b222f56e..788f99a1cba 100644
--- a/engines/freescape/games/eclipse/eclipse.musicdata.h
+++ b/engines/freescape/games/eclipse/eclipse.musicdata.h
@@ -26,7 +26,8 @@
* Shared music data for Total Eclipse backported music players.
*
* Song data extracted from the C64 version's Wally Beben music engine.
- * SID-only fields (pulse width, PWM) have been stripped from instruments.
+ * The shared instrument table strips SID-only pulse-width fields, but the
+ * original pulse-width values are kept below for ports that can use them.
* This header is included by both the AY and OPL player implementations.
*/
@@ -52,6 +53,16 @@ static const byte kInstruments[] = {
static const byte kInstrumentSize = 6;
static const byte kInstrumentCount = 12;
+// SID pulse-width data kept alongside the shared instruments so the OPL port
+// can retain some of the original timbre motion.
+static const byte kPulseWidthInit[] = {
+ 0x00, 0x02, 0x00, 0x64, 0x20, 0x20, 0x22, 0x43, 0x44, 0x30, 0x80, 0x41
+};
+
+static const byte kPulseWidthMod[] = {
+ 0x00, 0x0F, 0x0B, 0x2F, 0x08, 0x01, 0x00, 0x00, 0x22, 0x00, 0x22, 0x40
+};
+
static const byte kOrderList0[] = {
0xE0, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x0B, 0x0B, 0x0B, 0x0B, 0x01, 0x01,
0x0B, 0x10, 0x0B, 0x0B, 0x01, 0x10, 0x01, 0x01, 0x13, 0x13, 0x13, 0x13, 0x0B, 0x0B, 0x13, 0x13,
diff --git a/engines/freescape/games/eclipse/opl.music.cpp b/engines/freescape/games/eclipse/opl.music.cpp
index bb77368206c..77b9d0e5297 100644
--- a/engines/freescape/games/eclipse/opl.music.cpp
+++ b/engines/freescape/games/eclipse/opl.music.cpp
@@ -30,6 +30,18 @@ using namespace Freescape::EclipseMusicData;
namespace Freescape {
+namespace {
+
+struct OPLBasePatch {
+ byte modChar;
+ byte carChar;
+ byte modLevel;
+ byte carLevel;
+ byte modWave;
+ byte carWave;
+ byte feedbackConnection;
+};
+
// ============================================================================
// Embedded music data (shared with AY player, extracted from C64)
// ============================================================================
@@ -37,7 +49,7 @@ namespace Freescape {
// OPL2 F-number/block table (95 entries)
// Format: fnum (bits 0-9) | block (bits 10-12)
// Derived from SID frequency table
-static const uint16 kOPLFreqs[] = {
+const uint16 kOPLFreqs[] = {
0x0000, 0x0168, 0x017D, 0x0194, 0x01AD, 0x01C5,
0x01E1, 0x01FD, 0x021B, 0x023B, 0x025E, 0x0282,
0x02A8, 0x02D0, 0x02FB, 0x0328, 0x0358, 0x038B,
@@ -63,61 +75,62 @@ static const uint16 kOPLFreqs[] = {
// (pattern data removed - now in eclipse.musicdata.h)
//
// ============================================================================
-// OPL2 FM instrument patches (our own creation, not from C64)
+// OPL2 FM base patches (our own creation, but driven by the original SID data)
// ============================================================================
// OPL operator register offsets for channels 0-2
// Each OPL channel has 2 operators (modulator + carrier)
// Modulator offsets: 0x00, 0x01, 0x02 for channels 0-2
// Carrier offsets: 0x03, 0x04, 0x05 for channels 0-2
-static const byte kOPLModOffset[] = { 0x00, 0x01, 0x02 };
-static const byte kOPLCarOffset[] = { 0x03, 0x04, 0x05 };
-
-// FM patch definitions, 11 bytes each:
-// [0] mod: AM/VIB/EG/KSR/MULT (reg 0x20+mod)
-// [1] car: AM/VIB/EG/KSR/MULT (reg 0x20+car)
-// [2] mod: KSL/output level (reg 0x40+mod)
-// [3] car: KSL/output level (reg 0x40+car)
-// [4] mod: attack/decay (reg 0x60+mod)
-// [5] car: attack/decay (reg 0x60+car)
-// [6] mod: sustain/release (reg 0x80+mod)
-// [7] car: sustain/release (reg 0x80+car)
-// [8] mod: wave select (reg 0xE0+mod)
-// [9] car: wave select (reg 0xE0+car)
-// [10] feedback/connection (reg 0xC0+ch)
-// Carrier ADSR derived from SID AD/SR bytes using timing-matched conversion.
-// Modulator settings tuned per SID waveform type:
-// triangle â additive sine (soft, warm)
-// pulse â FM with slight harmonic content (brighter)
-// sawtooth â FM with feedback (rich harmonics)
-// noise â inharmonic FM (metallic percussion)
-static const byte kOPLPatches[][11] = {
- // 0: rest â silent
- { 0x00, 0x00, 0x3F, 0x3F, 0xFF, 0xFF, 0x0F, 0x0F, 0x00, 0x00, 0x00 },
- // 1: bass pulse (AD=0x42 SR=0x24) â punchy FM bass
- { 0x02, 0x01, 0x1E, 0x00, 0xF1, 0x8A, 0x07, 0xD9, 0x00, 0x01, 0x06 },
- // 2: triangle lead (AD=0x8A SR=0xAC) â warm sine lead
- { 0x01, 0x01, 0x2A, 0x00, 0xF1, 0x75, 0x07, 0x54, 0x00, 0x00, 0x01 },
- // 3: triangle pad (AD=0x6C SR=0x4F) â soft sustained pad
- { 0x01, 0x01, 0x2A, 0x00, 0xF1, 0x84, 0x07, 0xB1, 0x00, 0x00, 0x01 },
- // 4: pulse arpeggio (AD=0x3A SR=0xAC) â bright arpeggio
- { 0x02, 0x01, 0x1E, 0x00, 0xF1, 0x95, 0x07, 0x54, 0x00, 0x01, 0x06 },
- // 5: noise percussion (AD=0x42 SR=0x00) â metallic hit
- { 0x01, 0x0C, 0x00, 0x00, 0xFA, 0x8A, 0x07, 0xFD, 0x00, 0x00, 0x01 },
- // 6: triangle melody (AD=0x3D SR=0x1C) â flute-like
- { 0x01, 0x01, 0x2A, 0x00, 0xF1, 0x92, 0x07, 0xE4, 0x00, 0x00, 0x01 },
- // 7: pulse melody (AD=0x4C SR=0x2C) â vibrato lead
- { 0x02, 0x01, 0x1E, 0x00, 0xF1, 0x84, 0x07, 0xD4, 0x00, 0x01, 0x06 },
- // 8: triangle sustain (AD=0x5D SR=0xBC) â organ-like
- { 0x01, 0x01, 0x2A, 0x00, 0xF1, 0x82, 0x07, 0x44, 0x00, 0x00, 0x01 },
- // 9: pulse sustain (AD=0x4C SR=0xAF) â electric piano
- { 0x02, 0x01, 0x1E, 0x00, 0xF1, 0x84, 0x07, 0x51, 0x00, 0x01, 0x06 },
- // 10: sawtooth lead (AD=0x4A SR=0x2A) â brass-like
- { 0x21, 0x21, 0x1A, 0x00, 0xF1, 0x85, 0x07, 0xD5, 0x02, 0x00, 0x0E },
- // 11: pulse w/ arpeggio (AD=0x6A SR=0x6B) â harpsichord-like
- { 0x02, 0x01, 0x1E, 0x00, 0xF1, 0x85, 0x07, 0x94, 0x00, 0x01, 0x06 },
+const byte kOPLModOffset[] = { 0x00, 0x01, 0x02 };
+const byte kOPLCarOffset[] = { 0x03, 0x04, 0x05 };
+
+const OPLBasePatch kOPLBasePatches[] = {
+ // 0: silent
+ { 0x00, 0x00, 0x3F, 0x3F, 0x00, 0x00, 0x00 },
+ // 1: triangle - soft additive sine
+ { 0x01, 0x01, 0x28, 0x00, 0x00, 0x00, 0x01 },
+ // 2: sawtooth - brighter feedback voice
+ { 0x21, 0x21, 0x18, 0x00, 0x02, 0x00, 0x0C },
+ // 3: pulse - compact square-like FM voice
+ { 0x02, 0x01, 0x18, 0x00, 0x00, 0x01, 0x06 },
+ // 4: noise - metallic inharmonic approximation
+ { 0x11, 0x0C, 0x08, 0x00, 0x00, 0x00, 0x05 },
};
+// Software ADSR rate tables (8.8 fixed point, per-frame at 50 Hz)
+const uint16 kAttackRate[16] = {
+ 0x0F00, 0x0F00, 0x0F00, 0x0C80,
+ 0x07E5, 0x055B, 0x0469, 0x03C0,
+ 0x0300, 0x0133, 0x009A, 0x0060,
+ 0x004D, 0x001A, 0x000F, 0x000A
+};
+
+const uint16 kDecayReleaseRate[16] = {
+ 0x0F00, 0x0C80, 0x0640, 0x042B,
+ 0x02A2, 0x01C9, 0x0178, 0x0140,
+ 0x0100, 0x0066, 0x0033, 0x0020,
+ 0x001A, 0x0009, 0x0005, 0x0003
+};
+
+byte getWaveformFamily(byte ctrl) {
+ if ((ctrl & 0x80) != 0)
+ return 4;
+ if ((ctrl & 0x40) != 0)
+ return 3;
+ if ((ctrl & 0x20) != 0)
+ return 2;
+ if ((ctrl & 0x10) != 0)
+ return 1;
+ return 0;
+}
+
+uint16 decodePulseWidth(byte packed) {
+ return ((packed & 0x0F) << 8) | (packed & 0xF0);
+}
+
+} // namespace
+
// ============================================================================
// ChannelState
// ============================================================================
@@ -130,6 +143,8 @@ void EclipseOPLMusicPlayer::ChannelState::reset() {
instrumentOffset = 0;
currentNote = 0;
transpose = 0;
+ frequencyFnum = 0;
+ frequencyBlock = 0;
durationReload = 0;
durationCounter = 0;
effectMode = 0;
@@ -149,6 +164,19 @@ void EclipseOPLMusicPlayer::ChannelState::reset() {
instrumentFlags = 0;
gateOffDisabled = false;
keyOn = false;
+ pulseWidth = 0;
+ pulseWidthMod = 0;
+ pulseWidthDirection = 0;
+ modBaseLevel = 0x3F;
+ carBaseLevel = 0x3F;
+ modLevel = 0x3F;
+ carLevel = 0x3F;
+ adsrPhase = kPhaseOff;
+ adsrVolume = 0;
+ attackRate = 0;
+ decayRate = 0;
+ sustainLevel = 0;
+ releaseRate = 0;
}
// ============================================================================
@@ -182,6 +210,7 @@ EclipseOPLMusicPlayer::~EclipseOPLMusicPlayer() {
void EclipseOPLMusicPlayer::startMusic() {
if (!_opl)
return;
+ stopMusic();
_opl->start(new Common::Functor0Mem<void, EclipseOPLMusicPlayer>(
this, &EclipseOPLMusicPlayer::onTimer), 50);
setupSong();
@@ -212,6 +241,12 @@ void EclipseOPLMusicPlayer::noteToFnumBlock(byte note, uint16 &fnum, byte &block
}
void EclipseOPLMusicPlayer::setFrequency(int channel, uint16 fnum, byte block) {
+ _channels[channel].frequencyFnum = fnum;
+ _channels[channel].frequencyBlock = block;
+ writeFrequency(channel, fnum, block);
+}
+
+void EclipseOPLMusicPlayer::writeFrequency(int channel, uint16 fnum, byte block) {
if (!_opl)
return;
_opl->writeReg(0xA0 + channel, fnum & 0xFF);
@@ -226,44 +261,51 @@ void EclipseOPLMusicPlayer::setOPLInstrument(int channel, byte instrumentOffset)
if (!_opl)
return;
byte patchIdx = instrumentOffset / kInstrumentSize;
- if (patchIdx >= ARRAYSIZE(kOPLPatches))
+ if (patchIdx >= kInstrumentCount)
patchIdx = 0;
- const byte *patch = kOPLPatches[patchIdx];
+ byte ctrl = kInstruments[instrumentOffset + 0];
+ const OPLBasePatch &patch = kOPLBasePatches[getWaveformFamily(ctrl)];
byte mod = kOPLModOffset[channel];
byte car = kOPLCarOffset[channel];
- _opl->writeReg(0x20 + mod, patch[0]);
- _opl->writeReg(0x20 + car, patch[1]);
- _opl->writeReg(0x40 + mod, patch[2]);
- _opl->writeReg(0x40 + car, patch[3]);
- _opl->writeReg(0x60 + mod, patch[4]);
- _opl->writeReg(0x60 + car, patch[5]);
- _opl->writeReg(0x80 + mod, patch[6]);
- _opl->writeReg(0x80 + car, patch[7]);
- _opl->writeReg(0xE0 + mod, patch[8]);
- _opl->writeReg(0xE0 + car, patch[9]);
- _opl->writeReg(0xC0 + channel, patch[10]);
+ _channels[channel].pulseWidth = decodePulseWidth(kPulseWidthInit[patchIdx]);
+ _channels[channel].pulseWidthMod = kPulseWidthMod[patchIdx];
+ _channels[channel].pulseWidthDirection = 0;
+ _channels[channel].modBaseLevel = patch.modLevel;
+ _channels[channel].carBaseLevel = patch.carLevel;
+ _channels[channel].modLevel = patch.modLevel;
+ _channels[channel].carLevel = patch.carLevel;
+
+ _opl->writeReg(0x20 + mod, patch.modChar);
+ _opl->writeReg(0x20 + car, patch.carChar);
+ _opl->writeReg(0x60 + mod, 0xF0);
+ _opl->writeReg(0x60 + car, 0xF0);
+ _opl->writeReg(0x80 + mod, 0x00);
+ _opl->writeReg(0x80 + car, 0x00);
+ _opl->writeReg(0xE0 + mod, patch.modWave);
+ _opl->writeReg(0xE0 + car, patch.carWave);
+ _opl->writeReg(0xC0 + channel, patch.feedbackConnection);
+
+ updatePulseWidth(channel, false);
+ applyOperatorLevels(channel);
}
-void EclipseOPLMusicPlayer::noteOn(int channel, byte note) {
+void EclipseOPLMusicPlayer::noteOn(int channel) {
if (!_opl)
return;
- uint16 fnum;
- byte block;
- noteToFnumBlock(note, fnum, block);
-
_channels[channel].keyOn = true;
- _opl->writeReg(0xA0 + channel, fnum & 0xFF);
- _opl->writeReg(0xB0 + channel, 0x20 | (block << 2) | ((fnum >> 8) & 0x03));
+ _opl->writeReg(0xA0 + channel, _channels[channel].frequencyFnum & 0xFF);
+ _opl->writeReg(0xB0 + channel, 0x20 | (_channels[channel].frequencyBlock << 2) |
+ ((_channels[channel].frequencyFnum >> 8) & 0x03));
}
void EclipseOPLMusicPlayer::noteOff(int channel) {
if (!_opl)
return;
_channels[channel].keyOn = false;
- // Clear key-on bit, preserve frequency
- byte b0 = _opl ? 0x00 : 0x00; // just clear everything
+ byte b0 = ((_channels[channel].frequencyFnum >> 8) & 0x03) |
+ (_channels[channel].frequencyBlock << 2);
_opl->writeReg(0xB0 + channel, b0);
}
@@ -325,12 +367,14 @@ void EclipseOPLMusicPlayer::processChannel(int channel, bool newBeat) {
}
void EclipseOPLMusicPlayer::finalizeChannel(int channel) {
- // Gate off at half duration: trigger release via key-off
if (_channels[channel].durationReload != 0 &&
!_channels[channel].gateOffDisabled &&
((_channels[channel].durationReload >> 1) == _channels[channel].durationCounter)) {
- noteOff(channel);
+ releaseADSR(channel);
}
+
+ updatePulseWidth(channel, true);
+ updateADSR(channel);
}
// ============================================================================
@@ -361,10 +405,11 @@ void EclipseOPLMusicPlayer::setupSong() {
void EclipseOPLMusicPlayer::silenceAll() {
if (!_opl)
return;
- for (int ch = 0; ch < 9; ch++) {
+ for (int ch = 0; ch < kChannelCount; ch++) {
+ _channels[ch].keyOn = false;
_opl->writeReg(0xB0 + ch, 0x00); // key off
- _opl->writeReg(0x40 + kOPLModOffset[ch < 3 ? ch : 0], 0x3F); // silence mod
- _opl->writeReg(0x40 + kOPLCarOffset[ch < 3 ? ch : 0], 0x3F); // silence car
+ _opl->writeReg(0x40 + kOPLModOffset[ch], 0x3F); // silence mod
+ _opl->writeReg(0x40 + kOPLCarOffset[ch], 0x3F); // silence car
}
}
@@ -511,10 +556,12 @@ void EclipseOPLMusicPlayer::parseCommands(int channel) {
void EclipseOPLMusicPlayer::applyNote(int channel, byte note) {
byte instrumentOffset = _channels[channel].instrumentOffset;
byte ctrl = kInstruments[instrumentOffset + 0];
+ byte attackDecay = kInstruments[instrumentOffset + 1];
+ byte sustainRelease = kInstruments[instrumentOffset + 2];
byte autoEffect = kInstruments[instrumentOffset + 4];
byte flags = kInstruments[instrumentOffset + 5];
- byte sustainRelease = kInstruments[instrumentOffset + 2];
byte actualNote = note;
+ bool gateEnabled = (ctrl & 0x01) != 0;
if (actualNote != 0)
actualNote = clampNote(actualNote + _channels[channel].transpose);
@@ -539,12 +586,19 @@ void EclipseOPLMusicPlayer::applyNote(int channel, byte note) {
_channels[channel].gateOffDisabled = (sustainRelease & 0x0F) == 0x0F;
- if (actualNote == 0) {
+ if (actualNote != 0)
+ loadCurrentFrequency(channel);
+
+ if (actualNote == 0 || !gateEnabled) {
+ _channels[channel].adsrPhase = kPhaseOff;
+ _channels[channel].adsrVolume = 0;
+ applyOperatorLevels(channel);
noteOff(channel);
} else {
- // Key off then on to retrigger envelope
+ triggerADSR(channel, attackDecay, sustainRelease);
+ applyOperatorLevels(channel);
noteOff(channel);
- noteOn(channel, actualNote);
+ noteOn(channel);
}
_channels[channel].durationCounter = _channels[channel].durationReload;
@@ -630,7 +684,7 @@ bool EclipseOPLMusicPlayer::applyInstrumentVibrato(int channel) {
return false;
}
- int32 freq = noteFreq;
+ int32 freq = (int32)_channels[channel].frequencyFnum << _channels[channel].frequencyBlock;
for (byte i = 0; i < (span >> 1); i++)
freq -= delta;
for (byte i = 0; i < _channels[channel].vibratoCounter; i++)
@@ -644,7 +698,7 @@ bool EclipseOPLMusicPlayer::applyInstrumentVibrato(int channel) {
freq >>= 1;
block++;
}
- setFrequency(channel, freq & 0x3FF, block);
+ writeFrequency(channel, freq & 0x3FF, block);
return true;
}
@@ -662,7 +716,7 @@ void EclipseOPLMusicPlayer::applyEffectArpeggio(int channel) {
uint16 fnum;
byte block;
noteToFnumBlock(note, fnum, block);
- setFrequency(channel, fnum, block);
+ writeFrequency(channel, fnum, block);
}
void EclipseOPLMusicPlayer::applyTimedSlide(int channel) {
@@ -699,11 +753,7 @@ void EclipseOPLMusicPlayer::applyTimedSlide(int channel) {
if (delta == 0)
return;
- // Read current frequency from stored fnum/block
- uint16 curFnum;
- byte curBlock;
- noteToFnumBlock(currentNote, curFnum, curBlock);
- int32 curFreq = curFnum << curBlock;
+ int32 curFreq = (int32)_channels[channel].frequencyFnum << _channels[channel].frequencyBlock;
if (tgtFreq > srcFreq)
curFreq += delta;
@@ -720,4 +770,111 @@ void EclipseOPLMusicPlayer::applyTimedSlide(int channel) {
setFrequency(channel, curFreq & 0x3FF, block);
}
+void EclipseOPLMusicPlayer::triggerADSR(int channel, byte ad, byte sr) {
+ _channels[channel].adsrPhase = kPhaseAttack;
+ // Match the SID re-gate behavior: keep the current level when a new note
+ // starts so ornaments stay smooth instead of re-attacking from silence.
+ _channels[channel].attackRate = kAttackRate[ad >> 4];
+ _channels[channel].decayRate = kDecayReleaseRate[ad & 0x0F];
+ _channels[channel].sustainLevel = sr >> 4;
+ _channels[channel].releaseRate = kDecayReleaseRate[sr & 0x0F];
+}
+
+void EclipseOPLMusicPlayer::releaseADSR(int channel) {
+ if (_channels[channel].adsrPhase != kPhaseRelease &&
+ _channels[channel].adsrPhase != kPhaseOff) {
+ _channels[channel].adsrPhase = kPhaseRelease;
+ }
+}
+
+void EclipseOPLMusicPlayer::updateADSR(int channel) {
+ switch (_channels[channel].adsrPhase) {
+ case kPhaseAttack:
+ _channels[channel].adsrVolume += _channels[channel].attackRate;
+ if (_channels[channel].adsrVolume >= 0x0F00) {
+ _channels[channel].adsrVolume = 0x0F00;
+ _channels[channel].adsrPhase = kPhaseDecay;
+ }
+ break;
+
+ case kPhaseDecay: {
+ uint16 sustainTarget = (uint16)_channels[channel].sustainLevel << 8;
+ if (_channels[channel].adsrVolume > _channels[channel].decayRate + sustainTarget) {
+ _channels[channel].adsrVolume -= _channels[channel].decayRate;
+ } else {
+ _channels[channel].adsrVolume = sustainTarget;
+ _channels[channel].adsrPhase = kPhaseSustain;
+ }
+ break;
+ }
+
+ case kPhaseSustain:
+ break;
+
+ case kPhaseRelease:
+ if (_channels[channel].adsrVolume > _channels[channel].releaseRate) {
+ _channels[channel].adsrVolume -= _channels[channel].releaseRate;
+ } else {
+ _channels[channel].adsrVolume = 0;
+ _channels[channel].adsrPhase = kPhaseOff;
+ }
+ break;
+
+ case kPhaseOff:
+ _channels[channel].adsrVolume = 0;
+ break;
+ }
+
+ applyOperatorLevels(channel);
+}
+
+void EclipseOPLMusicPlayer::updatePulseWidth(int channel, bool advance) {
+ if ((_channels[channel].waveform & 0x40) == 0) {
+ _channels[channel].modLevel = _channels[channel].modBaseLevel;
+ _channels[channel].carLevel = _channels[channel].carBaseLevel;
+ return;
+ }
+
+ if (advance && _channels[channel].pulseWidthMod != 0) {
+ if ((_channels[channel].instrumentFlags & 0x04) != 0) {
+ uint16 pulseWidth = _channels[channel].pulseWidth;
+ pulseWidth = (pulseWidth & 0x0F00) |
+ (((pulseWidth & 0x00FF) + _channels[channel].pulseWidthMod) & 0x00FF);
+ _channels[channel].pulseWidth = pulseWidth;
+ } else if (_channels[channel].pulseWidthDirection == 0) {
+ _channels[channel].pulseWidth += _channels[channel].pulseWidthMod;
+ if ((_channels[channel].pulseWidth >> 8) >= 0x0F)
+ _channels[channel].pulseWidthDirection = 1;
+ } else {
+ _channels[channel].pulseWidth -= _channels[channel].pulseWidthMod;
+ if ((_channels[channel].pulseWidth >> 8) < 0x08)
+ _channels[channel].pulseWidthDirection = 0;
+ }
+ }
+
+ uint16 pulseWidth = MIN<uint16>(_channels[channel].pulseWidth & 0x0FFF, 0x0800);
+ byte brightnessBoost = pulseWidth < 0x0800 ? (0x0800 - pulseWidth) >> 7 : 0;
+ if (brightnessBoost > 12)
+ brightnessBoost = 12;
+
+ _channels[channel].modLevel = (_channels[channel].modBaseLevel > brightnessBoost) ?
+ (_channels[channel].modBaseLevel - brightnessBoost) : 0;
+ _channels[channel].carLevel = _channels[channel].carBaseLevel;
+}
+
+void EclipseOPLMusicPlayer::applyOperatorLevels(int channel) {
+ if (!_opl)
+ return;
+
+ byte mod = kOPLModOffset[channel];
+ byte car = kOPLCarOffset[channel];
+ uint16 inverseVolume = 0x0F00 - _channels[channel].adsrVolume;
+ byte attenuation = (inverseVolume * 63 + 0x0780) / 0x0F00;
+ byte modLevel = MIN<byte>(_channels[channel].modLevel + attenuation, 0x3F);
+ byte carLevel = MIN<byte>(_channels[channel].carLevel + attenuation, 0x3F);
+
+ _opl->writeReg(0x40 + mod, modLevel);
+ _opl->writeReg(0x40 + car, carLevel);
+}
+
} // namespace Freescape
diff --git a/engines/freescape/games/eclipse/opl.music.h b/engines/freescape/games/eclipse/opl.music.h
index 0f49e9c6ffd..466a898bf7d 100644
--- a/engines/freescape/games/eclipse/opl.music.h
+++ b/engines/freescape/games/eclipse/opl.music.h
@@ -33,7 +33,7 @@ namespace Freescape {
* - Reusing the same sequencer (order lists, patterns, instruments)
* - Converting SID note numbers to OPL F-number/block pairs
* - Mapping SID waveforms to OPL FM instrument patches
- * - Using OPL's built-in ADSR envelopes
+ * - Rebuilding the SID envelope and pulse-width motion on top of AdLib timbres
*/
class EclipseOPLMusicPlayer {
public:
@@ -45,8 +45,18 @@ public:
bool isPlaying() const;
private:
- static const byte kChannelCount = 3;
- static const byte kMaxNote = 94;
+ enum {
+ kChannelCount = 3,
+ kMaxNote = 94
+ };
+
+ enum ADSRPhase {
+ kPhaseOff,
+ kPhaseAttack,
+ kPhaseDecay,
+ kPhaseSustain,
+ kPhaseRelease
+ };
struct ChannelState {
const byte *orderList;
@@ -58,6 +68,8 @@ private:
byte instrumentOffset;
byte currentNote;
byte transpose;
+ uint16 frequencyFnum;
+ byte frequencyBlock;
byte durationReload;
byte durationCounter;
@@ -83,6 +95,19 @@ private:
byte instrumentFlags;
bool gateOffDisabled;
bool keyOn;
+ uint16 pulseWidth;
+ byte pulseWidthMod;
+ byte pulseWidthDirection;
+ byte modBaseLevel;
+ byte carBaseLevel;
+ byte modLevel;
+ byte carLevel;
+ ADSRPhase adsrPhase;
+ uint16 adsrVolume;
+ uint16 attackRate;
+ uint16 decayRate;
+ byte sustainLevel;
+ uint16 releaseRate;
void reset();
};
@@ -108,11 +133,17 @@ private:
bool applyInstrumentVibrato(int channel);
void applyEffectArpeggio(int channel);
void applyTimedSlide(int channel);
+ void triggerADSR(int channel, byte ad, byte sr);
+ void releaseADSR(int channel);
+ void updateADSR(int channel);
+ void updatePulseWidth(int channel, bool advance);
+ void applyOperatorLevels(int channel);
void setOPLInstrument(int channel, byte instrumentOffset);
- void noteOn(int channel, byte note);
+ void noteOn(int channel);
void noteOff(int channel);
void setFrequency(int channel, uint16 fnum, byte block);
+ void writeFrequency(int channel, uint16 fnum, byte block);
void noteToFnumBlock(byte note, uint16 &fnum, byte &block) const;
byte readPatternByte(int channel);
More information about the Scummvm-git-logs
mailing list