[Scummvm-git-logs] scummvm master -> b4868a69f73229fc260bc819b960fe6106b82bae
neuromancer
noreply at scummvm.org
Sun Feb 8 20:33:30 UTC 2026
This automated email contains information about 11 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
192382fcf3 FREESCAPE: fixed colors reading CPC image in mode0
7ae92cf1d1 FREESCAPE: loading several images from castle amiga (demo)
b5aab81d02 FREESCAPE: music for castle amiga (demo)
f361b7c91d FREESCAPE: wb sound driver for dark amiga
c184ea0bb0 FREESCAPE: load additional images from castle cpc
6f27dba36d FREESCAPE: load additional images from castle amiga
d92aaeb1cd FREESCAPE: add sound for castle cpc
4f7d766b4a FREESCAPE: fixed a few bugs in the driller c64 music emulation
ba72f4991f FREESCAPE: add gate bitmap in castle cpc
49f2dc9b34 FREESCAPE: properly parse and show dark amiga title
b4868a69f7 FREESCAPE: EMSCRIPTEN -> USE_FORCED_GLES
Commit: 192382fcf35b7b02239a591e6f8d2f9e236d8890
https://github.com/scummvm/scummvm/commit/192382fcf35b7b02239a591e6f8d2f9e236d8890
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-08T21:30:54+01:00
Commit Message:
FREESCAPE: fixed colors reading CPC image in mode0
Changed paths:
engines/freescape/games/driller/cpc.cpp
diff --git a/engines/freescape/games/driller/cpc.cpp b/engines/freescape/games/driller/cpc.cpp
index db9b895c1a3..404fc20da4f 100644
--- a/engines/freescape/games/driller/cpc.cpp
+++ b/engines/freescape/games/driller/cpc.cpp
@@ -80,9 +80,9 @@ byte getCPCPixelMode1(byte cpc_byte, int index) {
byte getCPCPixelMode0(byte cpc_byte, int index) {
if (index == 0) {
// Extract Pixel 0 from the byte
- return ((cpc_byte & 0x02) >> 1) | // Bit 1 -> Bit 3 (MSB)
- ((cpc_byte & 0x20) >> 4) | // Bit 5 -> Bit 2
- ((cpc_byte & 0x08) >> 1) | // Bit 3 -> Bit 1
+ return ((cpc_byte & 0x02) << 2) | // Bit 1 -> Bit 3 (MSB)
+ ((cpc_byte & 0x20) >> 3) | // Bit 5 -> Bit 2
+ ((cpc_byte & 0x08) >> 2) | // Bit 3 -> Bit 1
((cpc_byte & 0x80) >> 7); // Bit 7 -> Bit 0 (LSB)
}
else if (index == 2) {
Commit: 7ae92cf1d12dd112ad92347e032eadbb24ebb26f
https://github.com/scummvm/scummvm/commit/7ae92cf1d12dd112ad92347e032eadbb24ebb26f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-08T21:30:54+01:00
Commit Message:
FREESCAPE: loading several images from castle amiga (demo)
Changed paths:
engines/freescape/games/castle/amiga.cpp
engines/freescape/games/castle/castle.cpp
engines/freescape/games/castle/castle.h
engines/freescape/gfx.h
engines/freescape/gfx_opengl.cpp
engines/freescape/gfx_opengl_shaders.cpp
engines/freescape/gfx_tinygl.cpp
diff --git a/engines/freescape/games/castle/amiga.cpp b/engines/freescape/games/castle/amiga.cpp
index 7b941850e3e..61be4aeffbf 100644
--- a/engines/freescape/games/castle/amiga.cpp
+++ b/engines/freescape/games/castle/amiga.cpp
@@ -47,6 +47,25 @@ byte kAmigaCastlePalette[16][3] = {
{0xee, 0xee, 0xee},
};
+byte kAmigaCastleRiddlePalette[16][3] = {
+ {0x00, 0x00, 0x00},
+ {0x44, 0x44, 0x44},
+ {0x66, 0x66, 0x66},
+ {0x88, 0x88, 0x88},
+ {0xaa, 0xaa, 0xaa},
+ {0xcc, 0x44, 0x00},
+ {0xee, 0xaa, 0x00},
+ {0x66, 0x22, 0x00},
+ {0x66, 0x22, 0x00},
+ {0x66, 0x22, 0x00},
+ {0x66, 0x22, 0x00},
+ {0x66, 0x22, 0x00},
+ {0xaa, 0x88, 0x00},
+ {0xaa, 0x66, 0x00},
+ {0x88, 0x44, 0x00},
+ {0xee, 0xcc, 0x66},
+};
+
Graphics::ManagedSurface *CastleEngine::loadFrameFromPlanesVertical(Common::SeekableReadStream *file, int widthInBytes, int height) {
Graphics::ManagedSurface *surface;
surface = new Graphics::ManagedSurface();
@@ -79,6 +98,32 @@ Graphics::ManagedSurface *CastleEngine::loadFrameFromPlanesInternalVertical(Comm
return surface;
}
+Graphics::ManagedSurface *CastleEngine::loadFrameFromPlanesInterleaved(Common::SeekableReadStream *file, int widthInWords, int height) {
+ int widthInPixels = widthInWords * 16;
+ Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+ surface->create(widthInPixels, height, Graphics::PixelFormat::createFormatCLUT8());
+ surface->fillRect(Common::Rect(0, 0, widthInPixels, height), 0);
+
+ for (int y = 0; y < height; y++) {
+ for (int col = 0; col < widthInWords; col++) {
+ uint16 planes[4];
+ for (int p = 0; p < 4; p++)
+ planes[p] = file->readUint16BE();
+
+ for (int bit = 0; bit < 16; bit++) {
+ int x = col * 16 + (15 - bit);
+ byte color = 0;
+ for (int p = 0; p < 4; p++) {
+ if (planes[p] & (1 << bit))
+ color |= (1 << p);
+ }
+ surface->setPixel(x, y, color);
+ }
+ }
+ }
+ return surface;
+}
+
void CastleEngine::loadAssetsAmigaDemo() {
Common::File file;
file.open("x");
@@ -129,6 +174,47 @@ void CastleEngine::loadAssetsAmigaDemo() {
file.seek(0x2cf28 + 0x28 - 0x2 + 0x28);
_border = loadFrameFromPlanesVertical(&file, 160, 200);
_border->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+
+ // Menu background: 224x54 interleaved 4-plane (memory 0x36B9A, file 0x36BB6)
+ file.seek(0x36bb6);
+ _menu = loadFrameFromPlanesInterleaved(&file, 14, 54);
+ _menu->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+
+ file.seek(0x38952); // Spirit meter indicator background (memory 0x38936)
+ _spiritsMeterIndicatorBackgroundFrame = loadFrameFromPlanesInterleaved(&file, 5, 10);
+ _spiritsMeterIndicatorBackgroundFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+
+ file.seek(0x38ae2); // Spirit meter indicator (memory 0x38AC6)
+ _spiritsMeterIndicatorFrame = loadFrameFromPlanesInterleaved(&file, 1, 10);
+ _spiritsMeterIndicatorFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+
+ // Key sprites (memory 0x3C096, 12 frames, 16x7 each, interleaved 4-plane)
+ file.seek(0x3c0b2);
+ for (int i = 0; i < 12; i++) {
+ Graphics::ManagedSurface *frame = loadFrameFromPlanesInterleaved(&file, 1, 7);
+ frame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+ _keysBorderFrames.push_back(frame);
+ }
+
+ // Flag animation (memory 0x3C340, 5 frames, 32x11 each, interleaved 4-plane)
+ file.seek(0x3c35c);
+ for (int i = 0; i < 5; i++) {
+ Graphics::ManagedSurface *frame = loadFrameFromPlanesInterleaved(&file, 2, 11);
+ frame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+ _flagFrames.push_back(frame);
+ }
+
+ // Riddle frames (memory 0x3C6FA: top 20 rows + bg 1 row + bottom 8 rows, 256px wide)
+ file.seek(0x3c716);
+ _riddleTopFrame = loadFrameFromPlanesInterleaved(&file, 16, 20);
+ _riddleTopFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
+
+ _riddleBackgroundFrame = loadFrameFromPlanesInterleaved(&file, 16, 1);
+ _riddleBackgroundFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
+
+ _riddleBottomFrame = loadFrameFromPlanesInterleaved(&file, 16, 8);
+ _riddleBottomFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
+
file.close();
_areaMap[2]->_groundColor = 1;
@@ -138,6 +224,32 @@ void CastleEngine::loadAssetsAmigaDemo() {
void CastleEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
drawStringInSurface(_currentArea->_name, 97, 182, 0, 0, surface);
+ uint32 black = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0x00, 0x00);
+
+ // Draw last collected key at (224, 164)
+ if (!_keysCollected.empty() && !_keysBorderFrames.empty()) {
+ int k = int(_keysCollected.size()) - 1;
+ if (k < int(_keysBorderFrames.size()))
+ surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_keysBorderFrames[k], 224, 164,
+ Common::Rect(0, 0, _keysBorderFrames[k]->w, _keysBorderFrames[k]->h), black);
+ }
+
+ // Draw flag animation at (288, 5)
+ if (!_flagFrames.empty()) {
+ int flagFrameIndex = (_ticks / 10) % _flagFrames.size();
+ surface->copyRectToSurface(*_flagFrames[flagFrameIndex], 288, 5,
+ Common::Rect(0, 0, _flagFrames[flagFrameIndex]->w, _flagFrames[flagFrameIndex]->h));
+ }
+
+ // Draw spirit meter
+ if (_spiritsMeterIndicatorBackgroundFrame)
+ surface->copyRectToSurface((const Graphics::Surface)*_spiritsMeterIndicatorBackgroundFrame, 128, 160,
+ Common::Rect(0, 0, _spiritsMeterIndicatorBackgroundFrame->w, _spiritsMeterIndicatorBackgroundFrame->h));
+
+ if (_spiritsMeterIndicatorFrame) {
+ surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_spiritsMeterIndicatorFrame, 128 + _spiritsMeterPosition, 160,
+ Common::Rect(0, 0, _spiritsMeterIndicatorFrame->w, _spiritsMeterIndicatorFrame->h), black);
+ }
}
} // End of namespace Freescape
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index deb2c661cc6..54b611a9c65 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -590,6 +590,33 @@ void CastleEngine::drawInfoMenu() {
for (int i = 0; i < int(_keysCollected.size()) ; i++) {
int y = 58 + (i / 2) * 18;
+ if (i % 2 == 0) {
+ surface->copyRectToSurfaceWithKey(*_keysBorderFrames[i], 58, y, Common::Rect(0, 0, _keysBorderFrames[i]->w, _keysBorderFrames[i]->h), black);
+ keyRects.push_back(Common::Rect(58, y, 58 + _keysBorderFrames[i]->w / 2, y + _keysBorderFrames[i]->h));
+ } else {
+ surface->copyRectToSurfaceWithKey(*_keysBorderFrames[i], 80, y, Common::Rect(0, 0, _keysBorderFrames[i]->w, _keysBorderFrames[i]->h), black);
+ keyRects.push_back(Common::Rect(80, y, 80 + _keysBorderFrames[i]->w / 2, y + _keysBorderFrames[i]->h));
+ }
+ }
+ } else if (isAmiga() || isAtariST()) {
+ if (_menu)
+ surface->copyRectToSurface(*_menu, 47, 35, Common::Rect(0, 0, MIN<int>(_menu->w, surface->w - 47), MIN<int>(_menu->h, surface->h - 35)));
+
+ _gfx->readFromPalette(15, r, g, b);
+ front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
+ drawStringInSurface(Common::String::format("%07d", score), 166, 71, front, black, surface);
+ drawStringInSurface(centerAndPadString(Common::String::format("%s", _messagesList[135 + shield / 6].c_str()), 10), 151, 102, front, black, surface);
+
+ Common::String keysCollected = _messagesList[141];
+ Common::replace(keysCollected, "X", Common::String::format("%d", _keysCollected.size()));
+ drawStringInSurface(keysCollected, 103, 41, front, black, surface);
+
+ Common::String spiritsDestroyedString = _messagesList[133];
+ Common::replace(spiritsDestroyedString, "X", Common::String::format("%d", spiritsDestroyed));
+ drawStringInSurface(spiritsDestroyedString, 145, 132, front, black, surface);
+
+ for (int i = 0; i < int(_keysCollected.size()); i++) {
+ int y = 58 + (i / 2) * 18;
if (i % 2 == 0) {
surface->copyRectToSurfaceWithKey(*_keysBorderFrames[i], 58, y, Common::Rect(0, 0, _keysBorderFrames[i]->w, _keysBorderFrames[i]->h), black);
keyRects.push_back(Common::Rect(58, y, 58 + _keysBorderFrames[i]->w / 2, y + _keysBorderFrames[i]->h));
@@ -1122,6 +1149,8 @@ void CastleEngine::drawFullscreenRiddleAndWait(uint16 riddle) {
uint8 r, g, b;
_gfx->readFromPalette(frontColor, r, g, b);
uint32 front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
+ if (isAmiga())
+ front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0xEE, 0xAA, 0x00);
uint32 transparent = _gfx->_texturePixelFormat.ARGBToColor(0x00, 0x00, 0x00, 0x00);
Graphics::Surface *surface = new Graphics::Surface();
@@ -1180,6 +1209,10 @@ void CastleEngine::drawRiddle(uint16 riddle, uint32 front, uint32 back, Graphics
} else if (isSpectrum() || isCPC()) {
x = 64;
y = 37;
+ } else if (isAmiga()) {
+ x = 32;
+ y = 33;
+ maxWidth = 139;
}
surface->copyRectToSurface((const Graphics::Surface)*_riddleTopFrame, x, y, Common::Rect(0, 0, _riddleTopFrame->w, _riddleTopFrame->h));
for (y += _riddleTopFrame->h; y < maxWidth;) {
@@ -1198,6 +1231,9 @@ void CastleEngine::drawRiddle(uint16 riddle, uint32 front, uint32 back, Graphics
} else if (isSpectrum() || isCPC()) {
x = 64;
y = 36;
+ } else if (isAmiga()) {
+ x = 40;
+ y = 32;
}
for (int i = 0; i < int(riddleMessages.size()); i++) {
@@ -1678,7 +1714,7 @@ void CastleEngine::drawBackground() {
}
void CastleEngine::updateThunder() {
- if (!_thunderFrames[0])
+ if (_thunderFrames.empty() || !_thunderFrames[0])
return;
if (_thunderFrameDuration > 0) {
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index b106860e3ad..23952f23c36 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -108,6 +108,7 @@ public:
Graphics::ManagedSurface *loadFrameFromPlanesVertical(Common::SeekableReadStream *file, int widthInBytes, int height);
Graphics::ManagedSurface *loadFrameFromPlanesInternalVertical(Common::SeekableReadStream *file, Graphics::ManagedSurface *surface, int width, int height, int plane);
+ Graphics::ManagedSurface *loadFrameFromPlanesInterleaved(Common::SeekableReadStream *file, int widthInWords, int height);
Common::Array<Graphics::ManagedSurface *>_keysBorderFrames;
Common::Array<Graphics::ManagedSurface *>_keysMenuFrames;
diff --git a/engines/freescape/gfx.h b/engines/freescape/gfx.h
index 77895cb0258..a375eac9b27 100644
--- a/engines/freescape/gfx.h
+++ b/engines/freescape/gfx.h
@@ -193,6 +193,28 @@ public:
{ 0.4f, 2.0f }, //4
};
+ float _skyUvs672[16][2] = {
+ { 0.0f, 0.0f }, //1
+ { 0.0f, 2.0f }, //2
+ { 0.6f, 2.0f }, //3
+ { 0.6f, 0.0f }, //front //4
+
+ { 0.0f, 2.0f }, //back //1
+ { 0.6f, 2.0f }, //2
+ { 0.6f, 0.0f }, //3
+ { 0.0f, 0.0f }, //4
+
+ { 0.0f, 0.0f }, //left //1
+ { 0.6f, 0.0f }, //2
+ { 0.6f, 2.0f }, //3
+ { 0.0f, 2.0f }, //4
+
+ { 0.6f, 0.0f }, //right //1
+ { 0.0f, 0.0f }, //2
+ { 0.0f, 2.0f }, //3
+ { 0.6f, 2.0f }, //4
+ };
+
float _skyUvs128[16][2] = {
{ 0.0f, 0.0f }, //1
{ 0.0f, 2.0f }, //2
diff --git a/engines/freescape/gfx_opengl.cpp b/engines/freescape/gfx_opengl.cpp
index a73b4420ffc..e7c199a6172 100644
--- a/engines/freescape/gfx_opengl.cpp
+++ b/engines/freescape/gfx_opengl.cpp
@@ -171,6 +171,8 @@ void OpenGLRenderer::drawSkybox(Texture *texture, Math::Vector3d camera) {
glNormalPointer(GL_FLOAT, 0, _skyNormals);
if (texture->_width == 1008)
glTexCoordPointer(2, GL_FLOAT, 0, _skyUvs1008);
+ else if (texture->_width == 672)
+ glTexCoordPointer(2, GL_FLOAT, 0, _skyUvs672);
else if (texture->_width == 128)
glTexCoordPointer(2, GL_FLOAT, 0, _skyUvs128);
else
diff --git a/engines/freescape/gfx_opengl_shaders.cpp b/engines/freescape/gfx_opengl_shaders.cpp
index e4ea1a10b04..06ba3db09f1 100644
--- a/engines/freescape/gfx_opengl_shaders.cpp
+++ b/engines/freescape/gfx_opengl_shaders.cpp
@@ -182,6 +182,8 @@ void OpenGLShaderRenderer::drawSkybox(Texture *texture, Math::Vector3d camera) {
glBindBuffer(GL_ARRAY_BUFFER, _cubemapTexCoordVBO);
if (texture->_width == 1008)
glBufferData(GL_ARRAY_BUFFER, sizeof(_skyUvs1008), _skyUvs1008, GL_DYNAMIC_DRAW);
+ else if (texture->_width == 672)
+ glBufferData(GL_ARRAY_BUFFER, sizeof(_skyUvs672), _skyUvs672, GL_DYNAMIC_DRAW);
else if (texture->_width == 128)
glBufferData(GL_ARRAY_BUFFER, sizeof(_skyUvs128), _skyUvs128, GL_DYNAMIC_DRAW);
else
diff --git a/engines/freescape/gfx_tinygl.cpp b/engines/freescape/gfx_tinygl.cpp
index e5796397f68..b646b73c84a 100644
--- a/engines/freescape/gfx_tinygl.cpp
+++ b/engines/freescape/gfx_tinygl.cpp
@@ -115,6 +115,8 @@ void TinyGLRenderer::drawSkybox(Texture *texture, Math::Vector3d camera) {
tglNormalPointer(TGL_FLOAT, 0, _skyNormals);
if (texture->_width == 1008)
tglTexCoordPointer(2, TGL_FLOAT, 0, _skyUvs1008);
+ else if (texture->_width == 672)
+ tglTexCoordPointer(2, TGL_FLOAT, 0, _skyUvs672);
else if (texture->_width == 128)
tglTexCoordPointer(2, TGL_FLOAT, 0, _skyUvs128);
else
Commit: b5aab81d025981bc53a8903287e8adf67ce89607
https://github.com/scummvm/scummvm/commit/b5aab81d025981bc53a8903287e8adf67ce89607
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-08T21:30:54+01:00
Commit Message:
FREESCAPE: music for castle amiga (demo)
Changed paths:
engines/freescape/games/castle/amiga.cpp
engines/freescape/games/castle/castle.cpp
engines/freescape/games/castle/castle.h
diff --git a/engines/freescape/games/castle/amiga.cpp b/engines/freescape/games/castle/amiga.cpp
index 61be4aeffbf..7588a98c7f1 100644
--- a/engines/freescape/games/castle/amiga.cpp
+++ b/engines/freescape/games/castle/amiga.cpp
@@ -22,6 +22,8 @@
#include "common/file.h"
#include "common/memstream.h"
+#include "audio/mods/protracker.h"
+
#include "freescape/freescape.h"
#include "freescape/games/castle/castle.h"
#include "freescape/language/8bitDetokeniser.h"
@@ -215,6 +217,18 @@ void CastleEngine::loadAssetsAmigaDemo() {
_riddleBottomFrame = loadFrameFromPlanesInterleaved(&file, 16, 8);
_riddleBottomFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
+ // Load embedded ProTracker module for background music
+ // Module is at file offset 0x3D5A6 (memory 0x3D58A), ~86260 bytes
+ static const int kModOffset = 0x3D5A6;
+ file.seek(0, SEEK_END);
+ int fileSize = file.pos();
+ int modSize = fileSize - kModOffset;
+ if (modSize > 0) {
+ file.seek(kModOffset);
+ _modData.resize(modSize);
+ file.read(_modData.data(), modSize);
+ }
+
file.close();
_areaMap[2]->_groundColor = 1;
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 54b611a9c65..8fee1961a76 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -30,6 +30,8 @@
#include "backends/keymapper/standard-actions.h"
#include "common/translation.h"
+#include "audio/mods/protracker.h"
+
#include "freescape/freescape.h"
#include "freescape/games/castle/castle.h"
#include "freescape/language/8bitDetokeniser.h"
@@ -361,6 +363,14 @@ void CastleEngine::gotoArea(uint16 areaID, int entranceID) {
playSound(13, true, _soundFxHandle);
else
playSound(_soundIndexStart, false, _soundFxHandle);
+
+ // Start ProTracker background music for Amiga demo
+ if (isAmiga() && !_modData.empty() && !_mixer->isSoundHandleActive(_musicHandle)) {
+ Common::MemoryReadStream modStream(_modData.data(), _modData.size());
+ Audio::AudioStream *musicStream = Audio::makeProtrackerStream(&modStream);
+ if (musicStream)
+ _mixer->playStream(Audio::Mixer::kMusicSoundType, &_musicHandle, musicStream);
+ }
} else if (areaID == _endArea && entranceID == _endEntrance) {
_pitch = -85;
} else {
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index 23952f23c36..815f605e5da 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -129,6 +129,7 @@ public:
Graphics::ManagedSurface *_endGameBackgroundFrame;
Graphics::ManagedSurface *_gameOverBackgroundFrame;
+ Common::Array<byte> _modData; // Embedded ProTracker module (Amiga demo)
Common::Array<int> _keysCollected;
bool _useRockTravel;
int _spiritsMeter;
Commit: f361b7c91dd93cccd07edb33dfa1536fd59bcfeb
https://github.com/scummvm/scummvm/commit/f361b7c91dd93cccd07edb33dfa1536fd59bcfeb
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-08T21:30:54+01:00
Commit Message:
FREESCAPE: wb sound driver for dark amiga
Changed paths:
A engines/freescape/wb.cpp
A engines/freescape/wb.h
engines/freescape/games/dark/amiga.cpp
engines/freescape/games/dark/dark.cpp
engines/freescape/games/dark/dark.h
engines/freescape/module.mk
diff --git a/engines/freescape/games/dark/amiga.cpp b/engines/freescape/games/dark/amiga.cpp
index 53468acdd33..9308d0c502c 100644
--- a/engines/freescape/games/dark/amiga.cpp
+++ b/engines/freescape/games/dark/amiga.cpp
@@ -42,6 +42,16 @@ void DarkEngine::loadAssetsAmigaFullGame() {
loadMessagesVariableSize(stream, 0x3d37, 66);
loadSoundsFx(stream, 0x34738 + 2, 11);
+ // Load HDSMUSIC.AM music data (Wally Beben custom engine)
+ // HDSMUSIC.AM is an embedded GEMDOS executable at stream offset $BA64
+ static const uint32 kHdsMusicOffset = 0xBA64;
+ static const uint32 kGemdosHeaderSize = 0x1C;
+ static const uint32 kHdsMusicTextSize = 0xF4BC;
+
+ stream->seek(kHdsMusicOffset + kGemdosHeaderSize);
+ _musicData.resize(kHdsMusicTextSize);
+ stream->read(_musicData.data(), kHdsMusicTextSize);
+
Common::Array<Graphics::ManagedSurface *> chars;
chars = getCharsAmigaAtariInternal(8, 8, - 7 - 8, 16, 16, stream, 0x1b0bc, 85);
_fontBig = Font(chars);
diff --git a/engines/freescape/games/dark/dark.cpp b/engines/freescape/games/dark/dark.cpp
index c4a71d2a0ce..42060f38873 100644
--- a/engines/freescape/games/dark/dark.cpp
+++ b/engines/freescape/games/dark/dark.cpp
@@ -28,6 +28,7 @@
#include "freescape/games/dark/dark.h"
#include "freescape/language/8bitDetokeniser.h"
#include "freescape/objects/global.h"
+#include "freescape/wb.h"
#include "freescape/objects/connections.h"
namespace Freescape {
@@ -293,8 +294,16 @@ void DarkEngine::initGameState() {
_angleRotationIndex = 0;
_playerStepIndex = 6;
- // Start playing music, if any, in any supported format
- playMusic("Dark Side Theme");
+ // Start background music
+ if (isAmiga() && !_musicData.empty()) {
+ Audio::AudioStream *musicStream = makeWallyBebenStream(
+ _musicData.data(), _musicData.size(), 1);
+ if (musicStream) {
+ _mixer->stopHandle(_musicHandle);
+ _mixer->playStream(Audio::Mixer::kMusicSoundType,
+ &_musicHandle, musicStream);
+ }
+ }
}
void DarkEngine::loadAssets() {
diff --git a/engines/freescape/games/dark/dark.h b/engines/freescape/games/dark/dark.h
index c813fab4aa6..b2af7cd7923 100644
--- a/engines/freescape/games/dark/dark.h
+++ b/engines/freescape/games/dark/dark.h
@@ -20,6 +20,7 @@
*/
#include "audio/mixer.h"
+#include "common/array.h"
namespace Freescape {
@@ -104,6 +105,8 @@ public:
int _soundIndexDestroyECD;
Audio::SoundHandle _soundFxHandleJetpack;
+ Common::Array<byte> _musicData; // HDSMUSIC.AM TEXT segment (Amiga)
+
void drawString(const DarkFontSize size, const Common::String &str, int x, int y, uint32 primaryColor, uint32 secondaryColor, uint32 backColor, Graphics::Surface *surface);
void drawInfoMenu() override;
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index 1f607cf9e57..8108c47c156 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -50,7 +50,8 @@ MODULE_OBJS := \
sweepAABB.o \
sound.o \
ui.o \
- unpack.o
+ unpack.o \
+ wb.o
ifdef USE_TINYGL
MODULE_OBJS += \
diff --git a/engines/freescape/wb.cpp b/engines/freescape/wb.cpp
new file mode 100644
index 00000000000..e64e90cc922
--- /dev/null
+++ b/engines/freescape/wb.cpp
@@ -0,0 +1,934 @@
+/* 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/>.
+ *
+ */
+
+/**
+ * Wally Beben custom music engine player.
+ *
+ * Used in the Amiga version of Dark Side (Incentive Software, 1988).
+ * HDSMUSIC.AM is an embedded GEMDOS executable containing a 4-channel
+ * music engine with its own byte-stream pattern format, ~57KB of PCM
+ * samples, and ADSR volume envelopes.
+ *
+ * Assembly reference addresses (TEXT-relative, HDSMUSIC.AM):
+ * $0004 Tick entry point (called at 50 Hz from VBI)
+ * $0012 Command mailbox: 0=stop, 1/2=play song, $FF=playing
+ * $010C Main sequencer body (readPatternCommands equivalent)
+ * $0268 Pattern command dispatcher ($E0=inst, $C0=env, $80=dur, etc.)
+ * $030C Note-on handler (triggerNote equivalent)
+ * $068C Envelope processing (processEnvelope equivalent)
+ * $0AAE Period table (48 x uint16 BE)
+ * $0C42 Sample pointer table (16 x uint32 BE, TEXT-relative)
+ * $0C82 Instrument table (16 x 8 bytes)
+ * $0D02 Arpeggio interval lookup (8 bytes)
+ * $0D0A Envelope table (10 x 8 bytes)
+ * $0DBA Song table (2 songs x 4 channel pointers)
+ * $0DCA Pattern pointer table (up to ~71 x uint32 BE)
+ * $14B3 PCM sample data start (~57KB through $F4BB)
+ *
+ */
+
+#include "freescape/wb.h"
+#include "freescape/freescape.h"
+#include "audio/mods/paula.h"
+#include "common/endian.h"
+#include "common/debug.h"
+#include "common/util.h"
+
+namespace Freescape {
+
+// TEXT-relative offsets for data tables within HDSMUSIC.AM
+// All addresses verified against disassembly of the 68000 code.
+static const uint32 kPeriodTableOffset = 0x0AAE; // 48 x uint16 BE (note 0=silence, 1-47=C-1..B-3)
+static const uint32 kSamplePtrTableOffset = 0x0C42; // 16 x uint32 BE (TEXT-relative PCM offsets)
+static const uint32 kInstrumentTableOffset = 0x0C82; // 16 x 8 bytes (sample#, loopFlag, len, loopOff, loopLen)
+static const uint32 kArpeggioIntervalsOffset = 0x0D02; // 8 bytes (semitone offsets for arpeggio bitmask)
+static const uint32 kEnvelopeTableOffset = 0x0D0A; // 10 x 8 bytes (atk, dec, fadeRate, rel, mod, vib, arp, flags)
+static const uint32 kSongTableOffset = 0x0DBA; // 2 songs x 4 channels x uint32 BE order-list pointers
+static const uint32 kPatternPtrTableOffset = 0x0DCA; // up to 128 x uint32 BE (overlaps Song 2 order ptrs)
+static const uint32 kMaxPatternEntries = 128;
+
+class WallyBebenStream : public Audio::Paula {
+public:
+ WallyBebenStream(const byte *data, uint32 dataSize, int songNum, int rate, bool stereo);
+ ~WallyBebenStream() override;
+
+private:
+ void interrupt() override;
+
+ // --- Data tables (parsed from HDSMUSIC.AM TEXT segment) ---
+
+ const byte *_data;
+ uint32 _dataSize;
+
+ uint16 _periods[48];
+
+ struct InstrumentDesc {
+ byte sampleIndex;
+ byte loopFlag;
+ uint16 totalLength;
+ uint16 loopOffset;
+ uint16 loopLength;
+ };
+ InstrumentDesc _instruments[16];
+
+ struct EnvelopeDesc {
+ byte attackLevel;
+ byte decayTarget;
+ byte sustainLevel;
+ byte releaseRate;
+ byte modDepth;
+ byte vibratoWave;
+ byte arpeggioMask;
+ byte flags;
+ };
+ EnvelopeDesc _envelopes[10];
+
+ // Sample offsets within the data buffer
+ uint32 _sampleOffsets[16];
+
+ // Song order list pointers (TEXT-relative)
+ uint32 _songOrderPtrs[2][4];
+
+ // Pattern pointer table (TEXT+$DCA, up to 128 entries)
+ uint32 _patternPtrs[kMaxPatternEntries];
+ uint32 _numPatterns; // actual number of valid entries loaded
+ uint32 _firstSampleOffset; // lowest sample data offset â upper bound for pattern pointers
+
+ // Arpeggio interval lookup
+ byte _arpeggioIntervals[8];
+
+ // --- Per-channel state ---
+
+ struct ChannelState {
+ // Order list
+ uint32 orderListOffset; // TEXT-relative offset to order list
+ int orderListPos; // Current byte position in order list
+ int8 transpose; // Semitone transpose from order list
+
+ // Current pattern
+ uint32 patternOffset; // TEXT-relative offset to current pattern
+ int patternPos; // Current byte position in pattern
+
+ // Note state
+ byte note; // Current note (0=rest, 1-47)
+ byte prevNote; // Previous note (for portamento)
+ byte duration; // Note duration in sequencer steps
+ int durationCounter; // Remaining steps for current note
+
+ // Instrument / envelope selection
+ byte instrumentIdx; // 0-15
+ byte envelopeIdx; // 0-9
+
+ // Volume envelope
+ byte volume; // Current output volume (0-64)
+ byte attackLevel;
+ byte decayTarget;
+ byte sustainLevel;
+ byte releaseRate;
+ byte envelopePhase; // 0=attack, 1=decay, 2=sustain, 3=release
+ byte modDepth;
+
+ // Effects
+ byte effectMode; // 0=none, 1=porta/arpeggio, 2=envelope vibrato
+ bool portaUp;
+ bool portaDown;
+ int16 portaStep;
+ int16 portaTarget;
+ byte arpeggioMask;
+ byte arpeggioPos;
+ byte vibratoPos;
+
+ // Period
+ int16 basePeriod; // From note lookup
+ int16 outputPeriod; // After effects
+
+ // Delay
+ byte delay;
+ byte delayCounter;
+
+ bool active;
+ };
+ ChannelState _channels[4];
+
+ // --- Global state ---
+
+ bool _musicActive;
+ byte _tickSpeed; // Ticks between sequencer steps
+ byte _tickCounter; // Current tick count (counts up to _tickSpeed)
+
+ // Arpeggio working table (built by buildArpeggioTable)
+ byte _arpeggioTable[16];
+ int _arpeggioTableLen;
+
+ // --- Methods ---
+
+ void loadTables();
+ void startSong(int songNum);
+ void initChannel(int ch);
+ void readOrderList(int ch);
+ void readPatternCommands(int ch);
+ void triggerNote(int ch);
+ void processEffects(int ch);
+ void processEnvelope(int ch);
+ void buildArpeggioTable(byte mask);
+ uint16 getPeriod(int note) const;
+
+ byte readDataByte(uint32 offset) const {
+ if (offset < _dataSize)
+ return _data[offset];
+ return 0;
+ }
+
+ uint16 readDataWord(uint32 offset) const {
+ if (offset + 1 < _dataSize)
+ return READ_BE_UINT16(_data + offset);
+ return 0;
+ }
+
+ uint32 readDataLong(uint32 offset) const {
+ if (offset + 3 < _dataSize)
+ return READ_BE_UINT32(_data + offset);
+ return 0;
+ }
+};
+
+// ---------------------------------------------------------------------------
+// Construction / data loading
+// ---------------------------------------------------------------------------
+
+WallyBebenStream::WallyBebenStream(const byte *data, uint32 dataSize,
+ int songNum, int rate, bool stereo)
+ : Paula(stereo, rate, rate / 50), // 50 Hz PAL VBI interrupt rate
+ _data(data), _dataSize(dataSize),
+ _musicActive(false), _tickSpeed(6), _tickCounter(0),
+ _arpeggioTableLen(0), _numPatterns(0), _firstSampleOffset(0) {
+
+ memset(_periods, 0, sizeof(_periods));
+ memset(_instruments, 0, sizeof(_instruments));
+ memset(_envelopes, 0, sizeof(_envelopes));
+ memset(_sampleOffsets, 0, sizeof(_sampleOffsets));
+ memset(_songOrderPtrs, 0, sizeof(_songOrderPtrs));
+ memset(_patternPtrs, 0, sizeof(_patternPtrs));
+ memset(_arpeggioIntervals, 0, sizeof(_arpeggioIntervals));
+ memset(_channels, 0, sizeof(_channels));
+ memset(_arpeggioTable, 0, sizeof(_arpeggioTable));
+
+ // Standard Amiga panning: channels 0,3 left â channels 1,2 right
+ setChannelPanning(0, PANNING_LEFT);
+ setChannelPanning(1, PANNING_RIGHT);
+ setChannelPanning(2, PANNING_RIGHT);
+ setChannelPanning(3, PANNING_LEFT);
+
+ loadTables();
+ startSong(songNum);
+}
+
+WallyBebenStream::~WallyBebenStream() {
+}
+
+void WallyBebenStream::loadTables() {
+ // Period table: 48 x uint16 BE at TEXT+$AAE
+ for (int i = 0; i < 48; i++) {
+ _periods[i] = readDataWord(kPeriodTableOffset + i * 2);
+ }
+
+ // Sample pointer table: 16 x uint32 BE at TEXT+$C42
+ // These are TEXT-relative offsets to PCM sample data
+ for (int i = 0; i < 16; i++) {
+ _sampleOffsets[i] = readDataLong(kSamplePtrTableOffset + i * 4);
+ }
+
+ // Instrument table: 16 x 8 bytes at TEXT+$C82
+ for (int i = 0; i < 16; i++) {
+ uint32 off = kInstrumentTableOffset + i * 8;
+ _instruments[i].sampleIndex = readDataByte(off + 0);
+ _instruments[i].loopFlag = readDataByte(off + 1);
+ _instruments[i].totalLength = readDataWord(off + 2);
+ _instruments[i].loopOffset = readDataWord(off + 4);
+ _instruments[i].loopLength = readDataWord(off + 6);
+ }
+
+ // Arpeggio interval table: 8 bytes at TEXT+$D02
+ for (int i = 0; i < 8; i++) {
+ _arpeggioIntervals[i] = readDataByte(kArpeggioIntervalsOffset + i);
+ }
+
+ // Envelope table: 10 x 8 bytes at TEXT+$D0A
+ for (int i = 0; i < 10; i++) {
+ uint32 off = kEnvelopeTableOffset + i * 8;
+ _envelopes[i].attackLevel = readDataByte(off + 0);
+ _envelopes[i].decayTarget = readDataByte(off + 1);
+ _envelopes[i].sustainLevel = readDataByte(off + 2);
+ _envelopes[i].releaseRate = readDataByte(off + 3);
+ _envelopes[i].modDepth = readDataByte(off + 4);
+ _envelopes[i].vibratoWave = readDataByte(off + 5);
+ _envelopes[i].arpeggioMask = readDataByte(off + 6);
+ _envelopes[i].flags = readDataByte(off + 7);
+ }
+
+ // Song table: 2 songs x 4 channels x uint32 BE at TEXT+$DBA
+ for (int s = 0; s < 2; s++) {
+ for (int ch = 0; ch < 4; ch++) {
+ _songOrderPtrs[s][ch] = readDataLong(kSongTableOffset + s * 16 + ch * 4);
+ }
+ }
+
+ // Compute the first sample offset â this is the upper bound for valid
+ // pattern/order-list data. Anything at or above this is PCM sample data.
+ _firstSampleOffset = _dataSize;
+ for (int i = 0; i < 16; i++) {
+ if (_sampleOffsets[i] > 0 && _sampleOffsets[i] < _firstSampleOffset)
+ _firstSampleOffset = _sampleOffsets[i];
+ }
+
+ // Pattern pointer table at TEXT+$DCA.
+ // Valid pattern pointers must be > 0 and < _firstSampleOffset (i.e., they
+ // point into the song data area between the tables and the PCM samples).
+ // Entries at or beyond _firstSampleOffset are stale data, not real patterns.
+ _numPatterns = 0;
+ for (uint32 i = 0; i < kMaxPatternEntries; i++) {
+ uint32 ptr = readDataLong(kPatternPtrTableOffset + i * 4);
+ _patternPtrs[i] = ptr;
+ if (ptr > 0 && ptr < _firstSampleOffset)
+ _numPatterns = i + 1;
+ }
+
+ // Debug: dump loaded data tables for verification
+ debug(3, "WB: Data loaded from HDSMUSIC.AM TEXT segment (%u bytes)", _dataSize);
+
+ debug(3, "WB: Period table (first 12): %d %d %d %d %d %d %d %d %d %d %d %d",
+ _periods[0], _periods[1], _periods[2], _periods[3],
+ _periods[4], _periods[5], _periods[6], _periods[7],
+ _periods[8], _periods[9], _periods[10], _periods[11]);
+
+ for (int i = 0; i < 16; i++) {
+ if (_sampleOffsets[i])
+ debug(3, "WB: Sample %d: offset=$%X", i, _sampleOffsets[i]);
+ }
+
+ for (int i = 0; i < 16; i++) {
+ const InstrumentDesc &inst = _instruments[i];
+ if (inst.totalLength > 0)
+ debug(3, "WB: Inst %d: sample=%d loop=%d len=%d loopOff=%d loopLen=%d",
+ i, inst.sampleIndex, inst.loopFlag, inst.totalLength,
+ inst.loopOffset, inst.loopLength);
+ }
+
+ for (int i = 0; i < 10; i++) {
+ const EnvelopeDesc &env = _envelopes[i];
+ debug(3, "WB: Env %d: atk=%d dec=%d sus=%d rel=%d mod=%d arp=$%02X",
+ i, env.attackLevel, env.decayTarget, env.sustainLevel,
+ env.releaseRate, env.modDepth, env.arpeggioMask);
+ }
+
+ debug(3, "WB: Song 1 order ptrs: $%X $%X $%X $%X",
+ _songOrderPtrs[0][0], _songOrderPtrs[0][1],
+ _songOrderPtrs[0][2], _songOrderPtrs[0][3]);
+ debug(3, "WB: Song 2 order ptrs: $%X $%X $%X $%X",
+ _songOrderPtrs[1][0], _songOrderPtrs[1][1],
+ _songOrderPtrs[1][2], _songOrderPtrs[1][3]);
+
+ debug(3, "WB: %d valid patterns (firstSampleOffset=$%X)", _numPatterns, _firstSampleOffset);
+ for (uint32 i = 0; i < _numPatterns; i++) {
+ if (_patternPtrs[i])
+ debug(3, "WB: Pattern %d: offset=$%X", i, _patternPtrs[i]);
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Song init
+// ---------------------------------------------------------------------------
+
+void WallyBebenStream::startSong(int songNum) {
+ _musicActive = false;
+
+ if (songNum < 1 || songNum > 2)
+ return;
+
+ int songIdx = songNum - 1;
+ _tickSpeed = 6;
+ _tickCounter = 0;
+ _arpeggioTableLen = 0;
+
+ // Silence all Paula channels
+ for (int ch = 0; ch < NUM_VOICES; ch++) {
+ clearVoice(ch);
+ }
+
+ // Initialize each channel from the song table
+ for (int ch = 0; ch < 4; ch++) {
+ initChannel(ch);
+ _channels[ch].orderListOffset = _songOrderPtrs[songIdx][ch];
+ _channels[ch].orderListPos = 0;
+ _channels[ch].active = true;
+
+ // Read first order list entry to get the initial pattern
+ readOrderList(ch);
+ }
+
+ _musicActive = true;
+ startPaula();
+
+ debug(3, "WB: Song %d started, tickSpeed=%d", songNum, _tickSpeed);
+ for (int ch = 0; ch < 4; ch++) {
+ debug(3, "WB: ch%d orderList=$%X pattern=$%X",
+ ch, _channels[ch].orderListOffset, _channels[ch].patternOffset);
+ }
+}
+
+void WallyBebenStream::initChannel(int ch) {
+ ChannelState &c = _channels[ch];
+ memset(&c, 0, sizeof(ChannelState));
+ c.duration = 1;
+ c.durationCounter = 0; // Will trigger readPatternCommands on first tick
+ c.envelopePhase = 3; // Start in release (silent) until note-on
+
+ // Default envelope params: full volume sustain, so notes before any
+ // $C0 envelope command still produce sound (Env 0 has all zeros = silence)
+ c.attackLevel = 64;
+ c.decayTarget = 64;
+ c.sustainLevel = 0; // Instant transition (no fade)
+ c.releaseRate = 0;
+}
+
+// ---------------------------------------------------------------------------
+// Order list reader â advances to the next pattern for a channel
+// Asm ref: TEXT+$01A0 (order list processing)
+// Order list format: $00-$C0=pattern#, $C1-$FE=transpose, $FF=loop
+// ---------------------------------------------------------------------------
+
+void WallyBebenStream::readOrderList(int ch) {
+ ChannelState &c = _channels[ch];
+
+ for (int safety = 0; safety < 256; safety++) {
+ if (c.orderListOffset + c.orderListPos >= _dataSize)
+ break;
+
+ byte cmd = readDataByte(c.orderListOffset + c.orderListPos);
+ c.orderListPos++;
+
+ if (cmd == 0xFF) {
+ // Loop song: reset order list to beginning
+ c.orderListPos = 0;
+ continue;
+ }
+
+ if (cmd > 0xC0) {
+ // Transpose command: transpose = (cmd + 0x20) & 0xFF
+ // Stored as signed offset
+ c.transpose = (int8)((cmd + 0x20) & 0xFF);
+ continue;
+ }
+
+ // Pattern index (0 to $C0) â look up in pattern pointer table
+ // Validate: must be within table AND point to real pattern data (< sample area)
+ if (cmd < kMaxPatternEntries && _patternPtrs[cmd] > 0 && _patternPtrs[cmd] < _firstSampleOffset) {
+ c.patternOffset = _patternPtrs[cmd];
+ c.patternPos = 0;
+ debugC(3, kFreescapeDebugParser, "WB: ch%d order -> pattern %d (offset $%04X)", ch, cmd, c.patternOffset);
+ } else {
+ warning("WallyBeben: ch%d pattern index %d invalid (ptr=$%X, sampleStart=$%X)",
+ ch, cmd, (cmd < kMaxPatternEntries) ? _patternPtrs[cmd] : 0, _firstSampleOffset);
+ c.patternOffset = _patternPtrs[0];
+ c.patternPos = 0;
+ }
+ return;
+ }
+
+ warning("WallyBeben: ch%d order list safety limit hit", ch);
+}
+
+// ---------------------------------------------------------------------------
+// Pattern command reader â reads byte stream until a note is found
+// Asm ref: TEXT+$0268 (command dispatcher)
+// Pattern format: $FF=end-pattern, $FE=end-song, $F0-$FD=speed,
+// $E0-$EF=instrument, $C0-$DF=envelope, $80-$BF=duration,
+// $7F/$7E=portamento, $7D/$7C=vibrato, $7B=arpeggio,
+// $7A=delay, $00-$79=note (0=rest, 1-47=C-1..B-3)
+// ---------------------------------------------------------------------------
+
+void WallyBebenStream::readPatternCommands(int ch) {
+ ChannelState &c = _channels[ch];
+
+ for (int safety = 0; safety < 256; safety++) {
+ if (c.patternOffset + c.patternPos >= _dataSize)
+ break;
+
+ byte cmd = readDataByte(c.patternOffset + c.patternPos);
+ c.patternPos++;
+
+ if (cmd == 0xFF) {
+ // End of pattern â advance order list
+ readOrderList(ch);
+ continue;
+ }
+
+ if (cmd == 0xFE) {
+ // End of song
+ _musicActive = false;
+ return;
+ }
+
+ if (cmd == 0xDD) {
+ // Song change â read parameter byte, ignore for now
+ c.patternPos++;
+ continue;
+ }
+
+ if (cmd >= 0xF0) {
+ // Set speed: low nibble
+ _tickSpeed = cmd & 0x0F;
+ if (_tickSpeed == 0)
+ _tickSpeed = 1;
+ continue;
+ }
+
+ if (cmd >= 0xE0) {
+ // Set instrument: low nibble (0-15)
+ c.instrumentIdx = cmd & 0x0F;
+ continue;
+ }
+
+ if (cmd >= 0xC0) {
+ // Set envelope: low 5 bits (0-31, but only 1-9 valid)
+ // Asm ref: TEXT+$2C8 â envelope command handler
+ // $C0 (index 0) is a no-op: Env 0 is all zeros (sentinel entry).
+ // The original engine treats index 0 as "no envelope change".
+ byte envIdx = cmd & 0x1F;
+ if (envIdx == 0 || envIdx > 9) {
+ // Index 0 or out-of-range: skip, keep current envelope params
+ continue;
+ }
+
+ c.envelopeIdx = envIdx;
+
+ // Copy envelope parameters into channel state immediately
+ // (original engine loads params on $C0 command, not on note-on)
+ const EnvelopeDesc &env = _envelopes[c.envelopeIdx];
+ c.attackLevel = MIN(env.attackLevel, (byte)64);
+ c.decayTarget = MIN(env.decayTarget, (byte)64);
+ c.sustainLevel = MIN(env.sustainLevel, (byte)64);
+ c.releaseRate = env.releaseRate;
+ c.modDepth = env.modDepth;
+
+ // Envelope-triggered arpeggio/vibrato from envelope table
+ if (env.arpeggioMask != 0) {
+ c.effectMode = 2;
+ c.arpeggioMask = env.arpeggioMask;
+ buildArpeggioTable(env.arpeggioMask);
+ }
+ continue;
+ }
+
+ if (cmd >= 0x80) {
+ // Set duration: low 6 bits (0-63)
+ c.duration = cmd & 0x3F;
+ if (c.duration == 0)
+ c.duration = 1;
+ continue;
+ }
+
+ if (cmd == 0x7F) {
+ // Portamento up
+ c.portaUp = true;
+ c.portaDown = false;
+ c.effectMode = 1;
+ continue;
+ }
+
+ if (cmd == 0x7E) {
+ // Portamento down
+ c.portaDown = true;
+ c.portaUp = false;
+ c.effectMode = 1;
+ continue;
+ }
+
+ if (cmd == 0x7D) {
+ // Vibrato type 1
+ byte param = readDataByte(c.patternOffset + c.patternPos);
+ c.patternPos++;
+ c.effectMode = 1;
+ c.arpeggioMask = param;
+ buildArpeggioTable(param);
+ continue;
+ }
+
+ if (cmd == 0x7C) {
+ // Vibrato type 2
+ byte param = readDataByte(c.patternOffset + c.patternPos);
+ c.patternPos++;
+ c.effectMode = 2;
+ c.arpeggioMask = param;
+ buildArpeggioTable(param);
+ continue;
+ }
+
+ if (cmd == 0x7B) {
+ // Arpeggio
+ byte base = readDataByte(c.patternOffset + c.patternPos);
+ c.patternPos++;
+ byte rate = readDataByte(c.patternOffset + c.patternPos);
+ c.patternPos++;
+ c.effectMode = 1;
+ c.arpeggioMask = 0;
+ c.arpeggioPos = 0;
+ // Store arpeggio base note with transpose applied
+ _arpeggioTable[0] = base;
+ _arpeggioTable[1] = rate;
+ _arpeggioTableLen = 2;
+ continue;
+ }
+
+ if (cmd == 0x7A) {
+ // Set delay
+ byte param = readDataByte(c.patternOffset + c.patternPos);
+ c.patternPos++;
+ c.delay = param;
+ continue;
+ }
+
+ // Note value (0x00-0x79)
+ c.prevNote = c.note;
+ c.note = cmd;
+ c.durationCounter = c.duration;
+ triggerNote(ch);
+ return; // One note per sequencer step
+ }
+
+ warning("WallyBeben: ch%d pattern read safety limit hit", ch);
+}
+
+// ---------------------------------------------------------------------------
+// Note trigger â load instrument, set up Paula channel, reset envelope
+// Asm ref: TEXT+$030C (note-on handler)
+// ---------------------------------------------------------------------------
+
+void WallyBebenStream::triggerNote(int ch) {
+ ChannelState &c = _channels[ch];
+
+ if (c.note == 0) {
+ // Rest â silence channel
+ c.outputPeriod = 0;
+ c.volume = 0;
+ c.envelopePhase = 3;
+ setChannelVolume(ch, 0);
+ return;
+ }
+
+ // Apply transpose and clamp
+ int note = c.note + c.transpose;
+ if (note < 1) note = 1;
+ if (note > 47) note = 47;
+
+ // Look up period
+ c.basePeriod = getPeriod(note);
+ c.outputPeriod = c.basePeriod;
+
+ if (c.basePeriod == 0) {
+ warning("WallyBeben: ch%d note %d (raw %d + transpose %d) has period 0",
+ ch, note, c.note, c.transpose);
+ return;
+ }
+
+ // Load instrument
+ const InstrumentDesc &inst = _instruments[c.instrumentIdx];
+ byte sampleIdx = inst.sampleIndex;
+ if (sampleIdx >= 16)
+ sampleIdx = 0;
+
+ uint32 sampleOffset = _sampleOffsets[sampleIdx];
+ if (sampleOffset >= _dataSize) {
+ warning("WallyBeben: ch%d sample %d offset $%X out of range (dataSize=$%X)",
+ ch, sampleIdx, sampleOffset, _dataSize);
+ return;
+ }
+
+ const int8 *sampleData = (const int8 *)(_data + sampleOffset);
+ uint32 maxLen = _dataSize - sampleOffset;
+
+ // Instrument lengths are in bytes
+ uint32 totalLen = MIN((uint32)inst.totalLength, maxLen);
+ uint32 loopOff = MIN((uint32)inst.loopOffset, totalLen);
+ uint32 loopLen = inst.loopLength;
+
+ debug(5, "WB: ch%d note=%d period=%d inst=%d sample=%d offset=$%X len=%d loop=%d/%d",
+ ch, note, c.basePeriod, c.instrumentIdx, sampleIdx, sampleOffset,
+ totalLen, loopOff, loopLen);
+
+ if (totalLen < 4) {
+ warning("WallyBeben: ch%d sample too short (%d bytes)", ch, totalLen);
+ return;
+ }
+
+ // setChannelData calls enableChannel internally â do NOT call enableChannel again!
+ if (inst.loopFlag && loopLen > 2 && loopOff + loopLen <= totalLen) {
+ // Looped sample: initial play of full sample, then loop region
+ setChannelData(ch,
+ sampleData, // initial data pointer
+ sampleData + loopOff, // loop start pointer
+ totalLen, // initial length in bytes
+ loopLen); // loop length in bytes
+ } else {
+ // One-shot: play once, then 1-byte silence loop
+ setChannelData(ch,
+ sampleData,
+ sampleData + totalLen - 2,
+ totalLen, // length in bytes
+ 1);
+ }
+
+ setChannelPeriod(ch, c.outputPeriod);
+
+ // Reset envelope phase â params were already loaded by $C0 command
+ // (or default to full volume from initChannel)
+ // Asm ref: TEXT+$30C â note-on envelope reset
+ c.envelopePhase = 0; // Start at attack
+ c.volume = c.attackLevel;
+
+ setChannelVolume(ch, c.volume);
+
+ debugC(3, kFreescapeDebugParser, "WB: ch%d NOTE note=%d(+%d) period=%d inst=%d env=%d vol=%d",
+ ch, c.note, c.transpose, c.basePeriod, c.instrumentIdx, c.envelopeIdx, c.volume);
+
+ // Set up portamento if active
+ if (c.portaUp || c.portaDown) {
+ int16 prevPeriod = (c.prevNote > 0 && c.prevNote <= 47) ? getPeriod(c.prevNote + c.transpose) : c.basePeriod;
+ int16 delta = ABS(c.basePeriod - prevPeriod);
+ int steps = (_tickSpeed > 0) ? _tickSpeed : 1;
+ c.portaStep = delta / steps;
+ if (c.portaStep == 0)
+ c.portaStep = 1;
+ c.portaTarget = c.basePeriod;
+ c.basePeriod = prevPeriod; // Start from previous note
+ c.outputPeriod = prevPeriod;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Effects processing â runs every frame (50Hz)
+// Asm ref: TEXT+$05A0 (portamento), TEXT+$0620 (arpeggio/vibrato)
+// ---------------------------------------------------------------------------
+
+void WallyBebenStream::processEffects(int ch) {
+ ChannelState &c = _channels[ch];
+
+ if (c.effectMode == 0) {
+ c.outputPeriod = c.basePeriod;
+ return;
+ }
+
+ // Portamento
+ if (c.portaUp) {
+ // Sliding toward lower period (higher pitch)
+ c.basePeriod -= c.portaStep;
+ if (c.basePeriod <= c.portaTarget) {
+ c.basePeriod = c.portaTarget;
+ c.portaUp = false;
+ c.effectMode = 0;
+ }
+ c.outputPeriod = c.basePeriod;
+ return;
+ }
+
+ if (c.portaDown) {
+ // Sliding toward higher period (lower pitch)
+ c.basePeriod += c.portaStep;
+ if (c.basePeriod >= c.portaTarget) {
+ c.basePeriod = c.portaTarget;
+ c.portaDown = false;
+ c.effectMode = 0;
+ }
+ c.outputPeriod = c.basePeriod;
+ return;
+ }
+
+ // Arpeggio effect (effectMode 1 or 2)
+ if (_arpeggioTableLen > 0) {
+ int note = c.note + c.transpose;
+ int offset = _arpeggioTable[c.arpeggioPos % _arpeggioTableLen];
+ note += offset;
+ if (note < 1) note = 1;
+ if (note > 47) note = 47;
+ c.outputPeriod = getPeriod(note);
+ c.arpeggioPos++;
+ if (c.arpeggioPos >= _arpeggioTableLen)
+ c.arpeggioPos = 0;
+ } else {
+ c.outputPeriod = c.basePeriod;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Volume envelope â runs every frame (50Hz)
+// Asm ref: TEXT+$068C (envelope processing)
+//
+// Envelope table bytes (per entry, 8 bytes at TEXT+$D0A):
+// byte 0 (attackLevel): initial volume on note-on
+// byte 1 (decayTarget): target volume to fade toward and hold
+// byte 2 (sustainLevel): fade rate (0 = instant jump to target, >0 = per-tick)
+// byte 3 (releaseRate): volume decrease per tick on note-off
+// byte 4 (modDepth): modulation depth
+// byte 5 (vibratoWave): vibrato waveform selector
+// byte 6 (arpeggioMask): bitmask into arpeggio interval table at TEXT+$D02
+// byte 7 (flags): misc flags
+// ---------------------------------------------------------------------------
+
+void WallyBebenStream::processEnvelope(int ch) {
+ ChannelState &c = _channels[ch];
+
+ switch (c.envelopePhase) {
+ case 0: // Attack â volume already set to attackLevel in triggerNote
+ if (c.sustainLevel == 0) {
+ // Instant transition to hold level
+ c.volume = c.decayTarget;
+ c.envelopePhase = 2; // Skip fade, go straight to hold
+ } else {
+ // Begin gradual fade toward target
+ c.envelopePhase = 1;
+ }
+ break;
+
+ case 1: // Fade â move volume toward hold level (decayTarget)
+ if (c.volume < c.decayTarget) {
+ c.volume++;
+ } else if (c.volume > c.decayTarget) {
+ c.volume--;
+ } else {
+ c.envelopePhase = 2; // Reached target
+ }
+ break;
+
+ case 2: // Hold â maintain volume until note-off
+ break;
+
+ case 3: // Release â decrease volume on note-off
+ if (c.releaseRate > 0) {
+ if (c.volume > c.releaseRate) {
+ c.volume -= c.releaseRate;
+ } else {
+ c.volume = 0;
+ }
+ } else {
+ c.volume = 0; // Rate 0 = instant off
+ }
+ break;
+ }
+
+ // Clamp to Paula range
+ if (c.volume > 64)
+ c.volume = 64;
+}
+
+// ---------------------------------------------------------------------------
+// Arpeggio table builder â unpacks a bitmask into interval offsets
+// ---------------------------------------------------------------------------
+
+void WallyBebenStream::buildArpeggioTable(byte mask) {
+ _arpeggioTableLen = 0;
+
+ // Always include 0 (base note) as first entry
+ _arpeggioTable[_arpeggioTableLen++] = 0;
+
+ for (int i = 0; i < 8 && _arpeggioTableLen < 16; i++) {
+ if (mask & (1 << i)) {
+ _arpeggioTable[_arpeggioTableLen++] = _arpeggioIntervals[i];
+ }
+ }
+
+ if (_arpeggioTableLen <= 1)
+ _arpeggioTableLen = 0; // No arpeggio if only base note
+}
+
+// ---------------------------------------------------------------------------
+// Period lookup
+// ---------------------------------------------------------------------------
+
+uint16 WallyBebenStream::getPeriod(int note) const {
+ if (note < 0 || note > 47)
+ return 0;
+ return _periods[note];
+}
+
+// ---------------------------------------------------------------------------
+// Main interrupt â called at 50Hz by Paula
+// Asm ref: TEXT+$0004 (tick entry), TEXT+$010C (sequencer body)
+// ---------------------------------------------------------------------------
+
+void WallyBebenStream::interrupt() {
+ if (!_musicActive)
+ return;
+
+ // Sequencer step: when tick counter reaches 0
+ if (_tickCounter == 0) {
+ for (int ch = 0; ch < 4; ch++) {
+ if (!_channels[ch].active)
+ continue;
+
+ if (_channels[ch].durationCounter > 0) {
+ _channels[ch].durationCounter--;
+ }
+
+ if (_channels[ch].durationCounter == 0) {
+ // Note-off: enter release phase
+ if (_channels[ch].envelopePhase < 3)
+ _channels[ch].envelopePhase = 3;
+
+ // Read next commands
+ readPatternCommands(ch);
+ }
+ }
+ }
+
+ // Every frame: process effects and envelope, update Paula
+ for (int ch = 0; ch < 4; ch++) {
+ if (!_channels[ch].active)
+ continue;
+
+ processEffects(ch);
+ processEnvelope(ch);
+
+ setChannelPeriod(ch, _channels[ch].outputPeriod);
+ setChannelVolume(ch, _channels[ch].volume);
+ }
+
+ // Advance tick counter
+ _tickCounter++;
+ if (_tickCounter >= _tickSpeed) {
+ _tickCounter = 0;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Factory function
+// ---------------------------------------------------------------------------
+
+Audio::AudioStream *makeWallyBebenStream(const byte *data, uint32 dataSize,
+ int songNum, int rate, bool stereo) {
+ if (!data || dataSize < 0xF000) {
+ warning("WallyBeben: invalid data (size %u)", dataSize);
+ return nullptr;
+ }
+
+ return new WallyBebenStream(data, dataSize, songNum, rate, stereo);
+}
+
+} // End of namespace Freescape
diff --git a/engines/freescape/wb.h b/engines/freescape/wb.h
new file mode 100644
index 00000000000..1dcc22be052
--- /dev/null
+++ b/engines/freescape/wb.h
@@ -0,0 +1,47 @@
+/* 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 FREESCAPE_WB_H
+#define FREESCAPE_WB_H
+
+#include "audio/audiostream.h"
+#include "common/types.h"
+
+namespace Freescape {
+
+/**
+ * Create a music stream for the Wally Beben custom music engine
+ * used in the Amiga version of Dark Side.
+ *
+ * @param data Raw TEXT segment data from HDSMUSIC.AM (after 0x1C GEMDOS header)
+ * @param dataSize Size of the TEXT segment (0xF4BC for Dark Side)
+ * @param songNum Song number to play (1 or 2)
+ * @param rate Output sample rate
+ * @param stereo Whether to produce stereo output
+ * @return A new AudioStream, or nullptr on error
+ */
+Audio::AudioStream *makeWallyBebenStream(const byte *data, uint32 dataSize,
+ int songNum = 1, int rate = 44100,
+ bool stereo = true);
+
+} // End of namespace Freescape
+
+#endif
Commit: c184ea0bb0b6da3d2cda308c33c91716fecb2db5
https://github.com/scummvm/scummvm/commit/c184ea0bb0b6da3d2cda308c33c91716fecb2db5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-08T21:30:54+01:00
Commit Message:
FREESCAPE: load additional images from castle cpc
Changed paths:
engines/freescape/freescape.cpp
engines/freescape/freescape.h
engines/freescape/games/castle/castle.cpp
engines/freescape/games/castle/castle.h
engines/freescape/games/castle/cpc.cpp
engines/freescape/games/castle/zx.cpp
engines/freescape/games/dark/cpc.cpp
engines/freescape/games/driller/cpc.cpp
engines/freescape/games/eclipse/cpc.cpp
engines/freescape/gfx.cpp
engines/freescape/gfx.h
diff --git a/engines/freescape/freescape.cpp b/engines/freescape/freescape.cpp
index 3a08393d147..610d4f98cdb 100644
--- a/engines/freescape/freescape.cpp
+++ b/engines/freescape/freescape.cpp
@@ -39,6 +39,44 @@ namespace Freescape {
FreescapeEngine *g_freescape;
+byte getCPCPixelMode1(byte cpc_byte, int index) {
+ if (index == 0)
+ return ((cpc_byte & 0x08) >> 2) | ((cpc_byte & 0x80) >> 7);
+ else if (index == 1)
+ return ((cpc_byte & 0x04) >> 1) | ((cpc_byte & 0x40) >> 6);
+ else if (index == 2)
+ return (cpc_byte & 0x02) | ((cpc_byte & 0x20) >> 5);
+ else if (index == 3)
+ return ((cpc_byte & 0x01) << 1) | ((cpc_byte & 0x10) >> 4);
+ else
+ error("Invalid index %d requested", index);
+}
+
+byte getCPCPixelMode0(byte cpc_byte, int index) {
+ if (index == 0) {
+ // Extract Pixel 0 from the byte
+ return ((cpc_byte & 0x02) << 2) | // Bit 1 -> Bit 3 (MSB)
+ ((cpc_byte & 0x20) >> 3) | // Bit 5 -> Bit 2
+ ((cpc_byte & 0x08) >> 2) | // Bit 3 -> Bit 1
+ ((cpc_byte & 0x80) >> 7); // Bit 7 -> Bit 0 (LSB)
+ } else if (index == 2) {
+ // Extract Pixel 1 from the byte
+ return ((cpc_byte & 0x01) << 3) | // Bit 0 -> Bit 3 (MSB)
+ ((cpc_byte & 0x10) >> 2) | // Bit 4 -> Bit 2
+ ((cpc_byte & 0x04) >> 1) | // Bit 2 -> Bit 1
+ ((cpc_byte & 0x40) >> 6); // Bit 6 -> Bit 0 (LSB)
+ } else {
+ error("Invalid index %d requested", index);
+ }
+}
+
+byte getCPCPixel(byte cpc_byte, int index, bool mode1) {
+ if (mode1)
+ return getCPCPixelMode1(cpc_byte, index);
+ else
+ return getCPCPixelMode0(cpc_byte, index);
+}
+
FreescapeEngine::FreescapeEngine(OSystem *syst, const ADGameDescription *gd)
: Engine(syst), _gameDescription(gd), _gfx(nullptr) {
if (!ConfMan.hasKey("render_mode") || ConfMan.get("render_mode").empty())
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index be13fd65f01..4e44f39eabd 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -143,6 +143,7 @@ struct CGAPaletteEntry {
extern Common::String shiftStr(const Common::String &str, int shift);
extern Common::String centerAndPadString(const Common::String &str, int size);
+extern Graphics::ManagedSurface *readCPCImage(Common::SeekableReadStream *file, bool mode1);
class EventManagerWrapper {
public:
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 8fee1961a76..76b536d0d91 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -33,6 +33,7 @@
#include "audio/mods/protracker.h"
#include "freescape/freescape.h"
+#include "freescape/gfx.h"
#include "freescape/games/castle/castle.h"
#include "freescape/language/8bitDetokeniser.h"
@@ -245,6 +246,101 @@ CastleEngine::~CastleEngine() {
}
}
+Graphics::ManagedSurface *CastleEngine::loadFrameWithHeader(Common::SeekableReadStream *file, int pos, uint32 front, uint32 back) {
+ Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+ file->seek(pos);
+ int16 width = file->readByte();
+ int16 height = file->readByte();
+ debugC(kFreescapeDebugParser, "Frame size: %d x %d", width, height);
+ surface->create(width * 8, height, _gfx->_texturePixelFormat);
+
+ /*byte mask =*/ file->readByte();
+
+ surface->fillRect(Common::Rect(0, 0, width * 8, height), back);
+ /*int frameSize =*/ file->readUint16LE();
+ return loadFrame(file, surface, width, height, front);
+}
+
+Common::Array<Graphics::ManagedSurface *> CastleEngine::loadFramesWithHeader(Common::SeekableReadStream *file, int pos, int numFrames, uint32 front, uint32 back) {
+ Graphics::ManagedSurface *surface = nullptr;
+ file->seek(pos);
+ int16 width = file->readByte();
+ int16 height = file->readByte();
+ /*byte mask =*/ file->readByte();
+
+ /*int frameSize =*/ file->readUint16LE();
+ Common::Array<Graphics::ManagedSurface *> frames;
+ for (int i = 0; i < numFrames; i++) {
+ surface = new Graphics::ManagedSurface();
+ surface->create(width * 8, height, _gfx->_texturePixelFormat);
+ surface->fillRect(Common::Rect(0, 0, width * 8, height), back);
+ frames.push_back(loadFrame(file, surface, width, height, front));
+ }
+
+ return frames;
+}
+
+Graphics::ManagedSurface *CastleEngine::loadFrame(Common::SeekableReadStream *file, Graphics::ManagedSurface *surface, int width, int height, uint32 front) {
+ for (int i = 0; i < width * height; i++) {
+ byte color = file->readByte();
+ for (int n = 0; n < 8; n++) {
+ int y = i / width;
+ int x = (i % width) * 8 + (7 - n);
+ if ((color & (1 << n)))
+ surface->setPixel(x, y, front);
+ }
+ }
+ return surface;
+}
+
+Graphics::ManagedSurface *CastleEngine::loadFrameWithHeaderCPC(Common::SeekableReadStream *file, int pos, const uint32 *cpcPalette) {
+ Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+ file->seek(pos);
+ int16 width = file->readByte();
+ int16 height = file->readByte();
+ debugC(kFreescapeDebugParser, "CPC Frame size: %d x %d", width, height);
+ surface->create(width * 4, height, _gfx->_texturePixelFormat);
+
+ /*byte mask =*/ file->readByte();
+
+ surface->fillRect(Common::Rect(0, 0, width * 4, height), cpcPalette[0]);
+ /*int frameSize =*/ file->readUint16LE();
+ return loadFrameCPC(file, surface, width, height, cpcPalette);
+}
+
+Common::Array<Graphics::ManagedSurface *> CastleEngine::loadFramesWithHeaderCPC(Common::SeekableReadStream *file, int pos, int numFrames, const uint32 *cpcPalette) {
+ Graphics::ManagedSurface *surface = nullptr;
+ file->seek(pos);
+ int16 width = file->readByte();
+ int16 height = file->readByte();
+ /*byte mask =*/ file->readByte();
+
+ /*int frameSize =*/ file->readUint16LE();
+ Common::Array<Graphics::ManagedSurface *> frames;
+ for (int i = 0; i < numFrames; i++) {
+ surface = new Graphics::ManagedSurface();
+ surface->create(width * 4, height, _gfx->_texturePixelFormat);
+ surface->fillRect(Common::Rect(0, 0, width * 4, height), cpcPalette[0]);
+ frames.push_back(loadFrameCPC(file, surface, width, height, cpcPalette));
+ }
+
+ return frames;
+}
+
+Graphics::ManagedSurface *CastleEngine::loadFrameCPC(Common::SeekableReadStream *file, Graphics::ManagedSurface *surface, int width, int height, const uint32 *cpcPalette) {
+ for (int y = 0; y < height; y++) {
+ for (int col = 0; col < width; col++) {
+ byte cpc_byte = file->readByte();
+ for (int i = 0; i < 4; i++) {
+ int pixel = getCPCPixel(cpc_byte, i, true);
+ if (pixel != 0)
+ surface->setPixel(col * 4 + i, y, cpcPalette[pixel]);
+ }
+ }
+ }
+ return surface;
+}
+
void CastleEngine::initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *infoScreenKeyMap, const char *target) {
FreescapeEngine::initKeymaps(engineKeyMap, infoScreenKeyMap, target);
Common::Action *act;
@@ -1224,12 +1320,20 @@ void CastleEngine::drawRiddle(uint16 riddle, uint32 front, uint32 back, Graphics
y = 33;
maxWidth = 139;
}
- surface->copyRectToSurface((const Graphics::Surface)*_riddleTopFrame, x, y, Common::Rect(0, 0, _riddleTopFrame->w, _riddleTopFrame->h));
- for (y += _riddleTopFrame->h; y < maxWidth;) {
- surface->copyRectToSurface((const Graphics::Surface)*_riddleBackgroundFrame, x, y, Common::Rect(0, 0, _riddleBackgroundFrame->w, _riddleBackgroundFrame->h));
- y += _riddleBackgroundFrame->h;
+ // Draw riddle frame borders (if available)
+ if (_riddleTopFrame) {
+ surface->copyRectToSurface((const Graphics::Surface)*_riddleTopFrame, x, y, Common::Rect(0, 0, _riddleTopFrame->w, _riddleTopFrame->h));
+ y += _riddleTopFrame->h;
+ }
+ if (_riddleBackgroundFrame) {
+ for (; y < maxWidth;) {
+ surface->copyRectToSurface((const Graphics::Surface)*_riddleBackgroundFrame, x, y, Common::Rect(0, 0, _riddleBackgroundFrame->w, _riddleBackgroundFrame->h));
+ y += _riddleBackgroundFrame->h;
+ }
+ }
+ if (_riddleBottomFrame) {
+ surface->copyRectToSurface((const Graphics::Surface)*_riddleBottomFrame, x, maxWidth, Common::Rect(0, 0, _riddleBottomFrame->w, _riddleBottomFrame->h - 1));
}
- surface->copyRectToSurface((const Graphics::Surface)*_riddleBottomFrame, x, maxWidth, Common::Rect(0, 0, _riddleBottomFrame->w, _riddleBottomFrame->h - 1));
Common::Array<RiddleText> riddleMessages = _riddleList[riddle]._lines;
x = _riddleList[riddle]._origin.x;
@@ -1286,41 +1390,48 @@ void CastleEngine::drawEnergyMeter(Graphics::Surface *surface, Common::Point ori
barFrameOrigin += Common::Point(5, 6);
else if (isSpectrum())
barFrameOrigin += Common::Point(0, 6);
+ else if (isCPC())
+ barFrameOrigin += Common::Point(0, 6);
surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_strenghtBarFrame, barFrameOrigin.x, barFrameOrigin.y, Common::Rect(0, 0, _strenghtBarFrame->w, _strenghtBarFrame->h), black);
Common::Point weightPoint;
int frameIdx = -1;
- weightPoint = Common::Point(origin.x + 10, origin.y);
- frameIdx = _gameStateVars[k8bitVariableShield] % 4;
-
if (_strenghtWeightsFrames.empty())
return;
+ // Use actual weight sprite width for positioning
+ int weightWidth = _strenghtWeightsFrames[0]->w;
+ int weightOffset = isCPC() ? 9 : 10;
+ int rightWeightPos = isCPC() ? 59 : 62;
+
+ weightPoint = Common::Point(origin.x + weightOffset, origin.y);
+ frameIdx = _gameStateVars[k8bitVariableShield] % 4;
+
if (frameIdx != 0) {
frameIdx = 4 - frameIdx;
- surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_strenghtWeightsFrames[frameIdx], weightPoint.x, weightPoint.y, Common::Rect(0, 0, 3, _strenghtWeightsFrames[frameIdx]->h), back);
- weightPoint += Common::Point(3, 0);
+ surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_strenghtWeightsFrames[frameIdx], weightPoint.x, weightPoint.y, Common::Rect(0, 0, weightWidth, _strenghtWeightsFrames[frameIdx]->h), back);
+ weightPoint += Common::Point(weightWidth, 0);
}
for (int i = 0; i < _gameStateVars[k8bitVariableShield] / 4; i++) {
- surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_strenghtWeightsFrames[0], weightPoint.x, weightPoint.y, Common::Rect(0, 0, 3, _strenghtWeightsFrames[0]->h), back);
- weightPoint += Common::Point(3, 0);
+ surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_strenghtWeightsFrames[0], weightPoint.x, weightPoint.y, Common::Rect(0, 0, weightWidth, _strenghtWeightsFrames[0]->h), back);
+ weightPoint += Common::Point(weightWidth, 0);
}
- weightPoint = Common::Point(origin.x + 62, origin.y);
+ weightPoint = Common::Point(origin.x + rightWeightPos, origin.y);
frameIdx = _gameStateVars[k8bitVariableShield] % 4;
if (frameIdx != 0) {
frameIdx = 4 - frameIdx;
- surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_strenghtWeightsFrames[frameIdx], weightPoint.x, weightPoint.y, Common::Rect(0, 0, 3, _strenghtWeightsFrames[frameIdx]->h), back);
- weightPoint += Common::Point(-3, 0);
+ surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_strenghtWeightsFrames[frameIdx], weightPoint.x, weightPoint.y, Common::Rect(0, 0, weightWidth, _strenghtWeightsFrames[frameIdx]->h), back);
+ weightPoint += Common::Point(-weightWidth, 0);
}
for (int i = 0; i < _gameStateVars[k8bitVariableShield] / 4; i++) {
- surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_strenghtWeightsFrames[0], weightPoint.x, weightPoint.y, Common::Rect(0, 0, 3, _strenghtWeightsFrames[0]->h), back);
- weightPoint += Common::Point(-3, 0);
+ surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_strenghtWeightsFrames[0], weightPoint.x, weightPoint.y, Common::Rect(0, 0, weightWidth, _strenghtWeightsFrames[0]->h), back);
+ weightPoint += Common::Point(-weightWidth, 0);
}
}
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index 815f605e5da..2f54595a9f3 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -103,6 +103,13 @@ public:
Common::Array<Graphics::ManagedSurface *> loadFramesWithHeader(Common::SeekableReadStream *file, int pos, int numFrames, uint32 front, uint32 back);
Graphics::ManagedSurface *loadFrameWithHeader(Common::SeekableReadStream *file, int pos, uint32 front, uint32 back);
Graphics::ManagedSurface *loadFrame(Common::SeekableReadStream *file, Graphics::ManagedSurface *surface, int width, int height, uint32 back);
+
+ // CPC-specific frame loading (Mode 1: 4 pixels per byte)
+ // cpcPalette is a 4-entry array mapping CPC ink numbers (0-3) to ARGB colors
+ Common::Array<Graphics::ManagedSurface *> loadFramesWithHeaderCPC(Common::SeekableReadStream *file, int pos, int numFrames, const uint32 *cpcPalette);
+ Graphics::ManagedSurface *loadFrameWithHeaderCPC(Common::SeekableReadStream *file, int pos, const uint32 *cpcPalette);
+ Graphics::ManagedSurface *loadFrameCPC(Common::SeekableReadStream *file, Graphics::ManagedSurface *surface, int width, int height, const uint32 *cpcPalette);
+
Graphics::ManagedSurface *loadFrameFromPlanes(Common::SeekableReadStream *file, int widthInBytes, int height);
Graphics::ManagedSurface *loadFrameFromPlanesInternal(Common::SeekableReadStream *file, Graphics::ManagedSurface *surface, int width, int height);
diff --git a/engines/freescape/games/castle/cpc.cpp b/engines/freescape/games/castle/cpc.cpp
index bd149ea3ca9..a1d1bb1374e 100644
--- a/engines/freescape/games/castle/cpc.cpp
+++ b/engines/freescape/games/castle/cpc.cpp
@@ -96,7 +96,7 @@ byte mountainsData[288] {
0xaa, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
};
-extern Graphics::ManagedSurface *readCPCImage(Common::SeekableReadStream *file, bool mode0);
+
void CastleEngine::loadAssetsCPCFullGame() {
Common::File file;
@@ -178,74 +178,43 @@ void CastleEngine::loadAssetsCPCFullGame() {
uint32 front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
_background = loadFrame(&mountainsStream, background, backgroundWidth, backgroundHeight, front);
- /*_gfx->readFromPalette(2, r, g, b);
- uint32 red = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
- _gfx->readFromPalette(7, r, g, b);
- uint32 white = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
+ // CPC UI Sprites - located at different offsets than ZX Spectrum!
+ // CPC uses Mode 1 format (4 pixels per byte, 2 bits per pixel).
+ // Sprite pixel values 0-3 are CPC ink numbers that map to the border palette.
+ uint32 cpcPalette[4];
+ for (int i = 0; i < 4; i++) {
+ cpcPalette[i] = _gfx->_texturePixelFormat.ARGBToColor(0xFF,
+ kCPCPaletteCastleBorderData[i][0],
+ kCPCPaletteCastleBorderData[i][1],
+ kCPCPaletteCastleBorderData[i][2]);
+ }
- _keysBorderFrames.push_back(loadFrameWithHeader(&file, _language == Common::ES_ESP ? 0xe06 : 0xdf7, red, white));
+ // Keys Border: CPC offset 0x2362 (8x14 px, 1 frame - matches ZX key_sprite)
+ _keysBorderFrames.push_back(loadFrameWithHeaderCPC(&file, 0x2362, cpcPalette));
- uint32 green = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0, 0xff, 0);
- _spiritsMeterIndicatorFrame = loadFrameWithHeader(&file, _language == Common::ES_ESP ? 0xe5e : 0xe4f, green, white);
+ // Spirit Meter Background: CPC offset 0x2383 (64x8 px - matches ZX spirit_meter_bg)
+ _spiritsMeterIndicatorBackgroundFrame = loadFrameWithHeaderCPC(&file, 0x2383, cpcPalette);
- _gfx->readFromPalette(4, r, g, b);
- uint32 front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
+ // Spirit Meter Indicator: CPC offset 0x2408 (16x8 px - matches ZX spirit_meter_indicator)
+ _spiritsMeterIndicatorFrame = loadFrameWithHeaderCPC(&file, 0x2408, cpcPalette);
- int backgroundWidth = 16;
- int backgroundHeight = 18;
- Graphics::ManagedSurface *background = new Graphics::ManagedSurface();
- background->create(backgroundWidth * 8, backgroundHeight, _gfx->_texturePixelFormat);
- background->fillRect(Common::Rect(0, 0, backgroundWidth * 8, backgroundHeight), 0);
+ // Strength Background: CPC offset 0x242D (68x15 px - matches ZX strength_bg)
+ _strenghtBackgroundFrame = loadFrameWithHeaderCPC(&file, 0x242D, cpcPalette);
- file.seek(_language == Common::ES_ESP ? 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, _language == Common::ES_ESP ? 0xee6 : 0xed7, yellow, black);
- _strenghtBarFrame = loadFrameWithHeader(&file, _language == Common::ES_ESP ? 0xf72 : 0xf63, yellow, black);
-
- Graphics::ManagedSurface *bar = new Graphics::ManagedSurface();
- bar->create(_strenghtBarFrame->w - 4, _strenghtBarFrame->h, _gfx->_texturePixelFormat);
- _strenghtBarFrame->copyRectToSurface(*bar, 4, 0, Common::Rect(4, 0, _strenghtBarFrame->w - 4, _strenghtBarFrame->h));
- _strenghtBarFrame->free();
- delete _strenghtBarFrame;
- _strenghtBarFrame = bar;
-
- _strenghtWeightsFrames = loadFramesWithHeader(&file, _language == Common::ES_ESP ? 0xf92 : 0xf83, 4, yellow, black);
-
- _flagFrames = loadFramesWithHeader(&file, (_language == Common::ES_ESP ? 0x10e4 + 15 : 0x10e4), 4, green, black);
-
- int thunderWidth = 4;
- int thunderHeight = 43;
- _thunderFrame = new Graphics::ManagedSurface();
- _thunderFrame->create(thunderWidth * 8, thunderHeight, _gfx->_texturePixelFormat);
- _thunderFrame->fillRect(Common::Rect(0, 0, thunderWidth * 8, thunderHeight), 0);
- _thunderFrame = loadFrame(&file, _thunderFrame, thunderWidth, thunderHeight, front);
-
- 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);*/
+ // Strength Bar: CPC offset 0x2531 (68x3 px - matches ZX strength_bar)
+ _strenghtBarFrame = loadFrameWithHeaderCPC(&file, 0x2531, cpcPalette);
+
+ // Strength Weights: CPC offset 0x2569 (4x15 px, 4 frames - matches ZX weight_sprite w=1,h=15)
+ _strenghtWeightsFrames = loadFramesWithHeaderCPC(&file, 0x2569, 4, cpcPalette);
+
+ // Flag Animation: CPC offset 0x2654 (16x9 px, 4 frames)
+ _flagFrames = loadFramesWithHeaderCPC(&file, 0x2654, 4, cpcPalette);
+
+ // Note: Riddle frames are not loaded from external files for CPC.
+ // The _riddleTopFrame, _riddleBackgroundFrame, and _riddleBottomFrame
+ // will remain nullptr (initialized in constructor).
+ // The drawRiddle function handles nullptr gracefully.
for (auto &it : _areaMap) {
it._value->addStructure(_areaMap[255]);
@@ -273,7 +242,6 @@ void CastleEngine::loadAssetsCPCFullGame() {
void CastleEngine::drawCPCUI(Graphics::Surface *surface) {
uint32 color = _gfx->_paperColor;
- //uint32 black = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0x00, 0x00);
uint8 r, g, b;
_gfx->readFromPalette(color, r, g, b);
@@ -296,50 +264,42 @@ void CastleEngine::drawCPCUI(Graphics::Surface *surface) {
_temporaryMessageDeadlines.push_back(deadline);
} else {
if (_gameStateControl == kFreescapeGameStatePlaying) {
- drawStringInSurface(_currentArea->_name, 97, 182, front, back, surface);
+ drawStringInSurface(_currentArea->_name, 97, 182, front, back, surface);
}
}
- /*uint32 color = 5;
- uint8 r, g, b;
-
- _gfx->readFromPalette(color, r, g, b);
- uint32 front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
-
- color = 0;
- _gfx->readFromPalette(color, r, g, b);
- uint32 black = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
+ // Draw collected keys
+ if (!_keysBorderFrames.empty()) {
+ for (int k = 0; k < int(_keysCollected.size()); k++) {
+ surface->copyRectToSurface((const Graphics::Surface)*_keysBorderFrames[0], 76 - k * 4, 179, Common::Rect(0, 0, _keysBorderFrames[0]->w, _keysBorderFrames[0]->h));
+ }
+ }
- Common::Rect backRect(123, 179, 242 + 5, 188);
- surface->fillRect(backRect, black);
+ // Draw energy meter (strength)
+ drawEnergyMeter(surface, Common::Point(38, 158));
- Common::String message;
- int deadline = -1;
- getLatestMessages(message, deadline);
- if (deadline > 0 && deadline <= _countdown) {
- //debug("deadline: %d countdown: %d", deadline, _countdown);
- drawStringInSurface(message, 120, 179, front, black, surface);
- _temporaryMessages.push_back(message);
- _temporaryMessageDeadlines.push_back(deadline);
+ // Draw spirit meter
+ uint32 blackColor = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0, 0, 0);
+ if (_spiritsMeterIndicatorBackgroundFrame) {
+ surface->copyRectToSurface((const Graphics::Surface)*_spiritsMeterIndicatorBackgroundFrame, 136, 162, Common::Rect(0, 0, _spiritsMeterIndicatorBackgroundFrame->w, _spiritsMeterIndicatorBackgroundFrame->h));
+ if (_spiritsMeterIndicatorFrame) {
+ surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_spiritsMeterIndicatorFrame, 131 + _spiritsMeterPosition, 161, Common::Rect(0, 0, _spiritsMeterIndicatorFrame->w, _spiritsMeterIndicatorFrame->h), blackColor);
+ }
} else {
- if (_gameStateControl == kFreescapeGameStatePlaying) {
- drawStringInSurface(_currentArea->_name, 120, 179, front, black, surface);
+ uint32 green = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0xff, 0x00);
+ surface->fillRect(Common::Rect(152, 162, 216, 170), green);
+ if (_spiritsMeterIndicatorFrame) {
+ surface->copyRectToSurface((const Graphics::Surface)*_spiritsMeterIndicatorFrame, 131 + _spiritsMeterPosition, 161, Common::Rect(0, 0, _spiritsMeterIndicatorFrame->w, _spiritsMeterIndicatorFrame->h));
}
}
- for (int k = 0; k < int(_keysCollected.size()); k++) {
- surface->copyRectToSurface((const Graphics::Surface)*_keysBorderFrames[0], 99 - k * 4, 177, Common::Rect(0, 0, 6, 11));
+ // Draw animated flag
+ if (!_flagFrames.empty()) {
+ int ticks = g_system->getMillis() / 20;
+ int flagFrameIndex = (ticks / 10) % 4;
+ surface->copyRectToSurface(*_flagFrames[flagFrameIndex], 285, 5, Common::Rect(0, 0, _flagFrames[flagFrameIndex]->w, _flagFrames[flagFrameIndex]->h));
}
- uint32 green = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0, 0xff, 0);
-
- surface->fillRect(Common::Rect(152, 156, 216, 164), green);
- surface->copyRectToSurface((const Graphics::Surface)*_spiritsMeterIndicatorFrame, 140 + _spiritsMeterPosition, 156, Common::Rect(0, 0, 15, 8));
- drawEnergyMeter(surface, Common::Point(63, 154));
-
- int ticks = g_system->getMillis() / 20;
- int flagFrameIndex = (ticks / 10) % 4;
- surface->copyRectToSurface(*_flagFrames[flagFrameIndex], 264, 9, Common::Rect(0, 0, _flagFrames[flagFrameIndex]->w, _flagFrames[flagFrameIndex]->h));*/
}
} // End of namespace Freescape
diff --git a/engines/freescape/games/castle/zx.cpp b/engines/freescape/games/castle/zx.cpp
index 6eb5354714b..1aaaf37408a 100644
--- a/engines/freescape/games/castle/zx.cpp
+++ b/engines/freescape/games/castle/zx.cpp
@@ -41,54 +41,6 @@ void CastleEngine::initZX() {
_soundIndexAreaChange = 7;
}
-Graphics::ManagedSurface *CastleEngine::loadFrameWithHeader(Common::SeekableReadStream *file, int pos, uint32 front, uint32 back) {
- Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
- file->seek(pos);
- int16 width = file->readByte();
- int16 height = file->readByte();
- debugC(kFreescapeDebugParser, "Frame size: %d x %d", width, height);
- surface->create(width * 8, height, _gfx->_texturePixelFormat);
-
- /*byte mask =*/ file->readByte();
-
- surface->fillRect(Common::Rect(0, 0, width * 8, height), back);
- /*int frameSize =*/ file->readUint16LE();
- return loadFrame(file, surface, width, height, front);
-}
-
-Common::Array<Graphics::ManagedSurface *> CastleEngine::loadFramesWithHeader(Common::SeekableReadStream *file, int pos, int numFrames, uint32 front, uint32 back) {
- Graphics::ManagedSurface *surface = nullptr;
- file->seek(pos);
- int16 width = file->readByte();
- int16 height = file->readByte();
- /*byte mask =*/ file->readByte();
-
- /*int frameSize =*/ file->readUint16LE();
- Common::Array<Graphics::ManagedSurface *> frames;
- for (int i = 0; i < numFrames; i++) {
- surface = new Graphics::ManagedSurface();
- surface->create(width * 8, height, _gfx->_texturePixelFormat);
- surface->fillRect(Common::Rect(0, 0, width * 8, height), back);
- frames.push_back(loadFrame(file, surface, width, height, front));
- }
-
- return frames;
-}
-
-
-Graphics::ManagedSurface *CastleEngine::loadFrame(Common::SeekableReadStream *file, Graphics::ManagedSurface *surface, int width, int height, uint32 front) {
- for (int i = 0; i < width * height; i++) {
- byte color = file->readByte();
- for (int n = 0; n < 8; n++) {
- int y = i / width;
- int x = (i % width) * 8 + (7 - n);
- if ((color & (1 << n)))
- surface->setPixel(x, y, front);
- }
- }
- return surface;
-}
-
void CastleEngine::loadAssetsZXFullGame() {
Common::File file;
uint8 r, g, b;
diff --git a/engines/freescape/games/dark/cpc.cpp b/engines/freescape/games/dark/cpc.cpp
index 5df3839a54e..c29748f5178 100644
--- a/engines/freescape/games/dark/cpc.cpp
+++ b/engines/freescape/games/dark/cpc.cpp
@@ -60,7 +60,6 @@ byte kCPCPaletteDarkTitle[16][3] = {
{0x00, 0x80, 0x00}, // 15: X
};
-extern Graphics::ManagedSurface *readCPCImage(Common::SeekableReadStream *file, bool mode0);
void DarkEngine::loadAssetsCPCFullGame() {
Common::File file;
diff --git a/engines/freescape/games/driller/cpc.cpp b/engines/freescape/games/driller/cpc.cpp
index 404fc20da4f..a0cda34d34f 100644
--- a/engines/freescape/games/driller/cpc.cpp
+++ b/engines/freescape/games/driller/cpc.cpp
@@ -64,45 +64,7 @@ byte kCPCPaletteBorderData[4][3] = {
{0x00, 0x80, 0x00},
};
-byte getCPCPixelMode1(byte cpc_byte, int index) {
- if (index == 0)
- return ((cpc_byte & 0x08) >> 2) | ((cpc_byte & 0x80) >> 7);
- else if (index == 1)
- return ((cpc_byte & 0x04) >> 1) | ((cpc_byte & 0x40) >> 6);
- else if (index == 2)
- return (cpc_byte & 0x02) | ((cpc_byte & 0x20) >> 5);
- else if (index == 3)
- return ((cpc_byte & 0x01) << 1) | ((cpc_byte & 0x10) >> 4);
- else
- error("Invalid index %d requested", index);
-}
-
-byte getCPCPixelMode0(byte cpc_byte, int index) {
- if (index == 0) {
- // Extract Pixel 0 from the byte
- return ((cpc_byte & 0x02) << 2) | // Bit 1 -> Bit 3 (MSB)
- ((cpc_byte & 0x20) >> 3) | // Bit 5 -> Bit 2
- ((cpc_byte & 0x08) >> 2) | // Bit 3 -> Bit 1
- ((cpc_byte & 0x80) >> 7); // Bit 7 -> Bit 0 (LSB)
- }
- else if (index == 2) {
- // Extract Pixel 1 from the byte
- return ((cpc_byte & 0x01) << 3) | // Bit 0 -> Bit 3 (MSB)
- ((cpc_byte & 0x10) >> 2) | // Bit 4 -> Bit 2
- ((cpc_byte & 0x04) >> 1) | // Bit 2 -> Bit 1
- ((cpc_byte & 0x40) >> 6); // Bit 6 -> Bit 0 (LSB)
- }
- else {
- error("Invalid index %d requested", index);
- }
-}
-
-byte getCPCPixel(byte cpc_byte, int index, bool mode1) {
- if (mode1)
- return getCPCPixelMode1(cpc_byte, index);
- else
- return getCPCPixelMode0(cpc_byte, index);
-}
+// getCPCPixelMode1, getCPCPixelMode0, and getCPCPixel moved to freescape.cpp
Graphics::ManagedSurface *readCPCImage(Common::SeekableReadStream *file, bool mode1) {
Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
diff --git a/engines/freescape/games/eclipse/cpc.cpp b/engines/freescape/games/eclipse/cpc.cpp
index c7683addc7c..0f4dd244126 100644
--- a/engines/freescape/games/eclipse/cpc.cpp
+++ b/engines/freescape/games/eclipse/cpc.cpp
@@ -62,7 +62,6 @@ byte kCPCPaletteEclipseBorderData[4][3] = {
};
-extern Graphics::ManagedSurface *readCPCImage(Common::SeekableReadStream *file, bool mode0);
void EclipseEngine::loadAssetsCPCFullGame() {
Common::File file;
diff --git a/engines/freescape/gfx.cpp b/engines/freescape/gfx.cpp
index c777b01b514..83410420e9a 100644
--- a/engines/freescape/gfx.cpp
+++ b/engines/freescape/gfx.cpp
@@ -69,8 +69,6 @@ Renderer::Renderer(int screenW, int screenH, Common::RenderMode renderMode, bool
Renderer::~Renderer() {}
-extern byte getCPCPixel(byte cpc_byte, int index, bool mode0);
-
byte getCPCStipple(byte cpc_byte, int back, int fore) {
int c0 = getCPCPixel(cpc_byte, 0, true);
assert(c0 == back || c0 == fore);
diff --git a/engines/freescape/gfx.h b/engines/freescape/gfx.h
index a375eac9b27..8f9ae18fb78 100644
--- a/engines/freescape/gfx.h
+++ b/engines/freescape/gfx.h
@@ -43,6 +43,9 @@ typedef Common::HashMap<int, int> ColorReMap;
class Renderer;
const Graphics::PixelFormat getRGBAPixelFormat();
+byte getCPCPixelMode1(byte cpc_byte, int index);
+byte getCPCPixelMode0(byte cpc_byte, int index);
+byte getCPCPixel(byte cpc_byte, int index, bool mode1);
class Texture {
public:
Commit: 6f27dba36dd68f6ddb004aea8b20e66a0e37c81c
https://github.com/scummvm/scummvm/commit/6f27dba36dd68f6ddb004aea8b20e66a0e37c81c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-08T21:30:55+01:00
Commit Message:
FREESCAPE: load additional images from castle amiga
Changed paths:
engines/freescape/games/castle/amiga.cpp
engines/freescape/games/castle/castle.cpp
diff --git a/engines/freescape/games/castle/amiga.cpp b/engines/freescape/games/castle/amiga.cpp
index 7588a98c7f1..f0af67820e7 100644
--- a/engines/freescape/games/castle/amiga.cpp
+++ b/engines/freescape/games/castle/amiga.cpp
@@ -217,6 +217,99 @@ void CastleEngine::loadAssetsAmigaDemo() {
_riddleBottomFrame = loadFrameFromPlanesInterleaved(&file, 16, 8);
_riddleBottomFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
+ // Castle gate (game over background frame)
+ // Pixel data: 43 rows à 96 bytes (16 columns à 3 words) at file 0x39AE2
+ // Mask data: 43 rows à 32 bytes (16 words) at file 0x3AB02
+ // FUN_2CCA tiles 24 top rows + 19 bottom rows into a 256Ã120 gate image
+ {
+ static const int kTopRows = 24;
+ static const int kBottomRows = 19;
+ static const int kTotalSrcRows = kTopRows + kBottomRows;
+ static const int kColumnsPerRow = 16;
+ static const int kPixelBytesPerRow = kColumnsPerRow * 6; // 3 words à 2 bytes
+ static const int kMaskBytesPerRow = kColumnsPerRow * 2; // 1 word à 2 bytes
+ static const int kGateWidth = 256;
+ static const int kGateHeight = 120;
+
+ byte pixelData[kTotalSrcRows * kPixelBytesPerRow];
+ byte maskData[kTotalSrcRows * kMaskBytesPerRow];
+
+ file.seek(0x39AE2);
+ file.read(pixelData, sizeof(pixelData));
+ file.seek(0x3AB02);
+ file.read(maskData, sizeof(maskData));
+
+ uint32 keyColor = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0x24, 0xA5);
+ uint32 paletteColors[8];
+ for (int i = 0; i < 8; i++)
+ paletteColors[i] = _gfx->_texturePixelFormat.ARGBToColor(0xFF,
+ kAmigaCastlePalette[i][0], kAmigaCastlePalette[i][1], kAmigaCastlePalette[i][2]);
+
+ _gameOverBackgroundFrame = new Graphics::ManagedSurface();
+ _gameOverBackgroundFrame->create(kGateWidth, kGateHeight, _gfx->_texturePixelFormat);
+ _gameOverBackgroundFrame->fillRect(Common::Rect(0, 0, kGateWidth, kGateHeight), keyColor);
+
+ // Build row mapping: FUN_2CCA tiling for N=120
+ // 5 tail rows from top portion (rows 19-23), then 4Ã24 full top blocks, then 19 bottom rows
+ int destRow = 0;
+ // Tail of top portion
+ for (int r = kTopRows - 5; r < kTopRows; r++) {
+ int srcRow = r;
+ for (int col = 0; col < kColumnsPerRow; col++) {
+ uint16 mask = READ_BE_UINT16(&maskData[srcRow * kMaskBytesPerRow + col * 2]);
+ int pOff = srcRow * kPixelBytesPerRow + col * 6;
+ uint16 p0 = READ_BE_UINT16(&pixelData[pOff]);
+ uint16 p1 = READ_BE_UINT16(&pixelData[pOff + 2]);
+ uint16 p2 = READ_BE_UINT16(&pixelData[pOff + 4]);
+ for (int bit = 15; bit >= 0; bit--) {
+ if (!(mask & (1 << bit))) {
+ int color = ((p0 >> bit) & 1) | (((p1 >> bit) & 1) << 1) | (((p2 >> bit) & 1) << 2);
+ _gameOverBackgroundFrame->setPixel(col * 16 + (15 - bit), destRow, paletteColors[color]);
+ }
+ }
+ }
+ destRow++;
+ }
+ // 4 full repetitions of top portion (24 rows each)
+ for (int block = 0; block < 4; block++) {
+ for (int r = 0; r < kTopRows; r++) {
+ int srcRow = r;
+ for (int col = 0; col < kColumnsPerRow; col++) {
+ uint16 mask = READ_BE_UINT16(&maskData[srcRow * kMaskBytesPerRow + col * 2]);
+ int pOff = srcRow * kPixelBytesPerRow + col * 6;
+ uint16 p0 = READ_BE_UINT16(&pixelData[pOff]);
+ uint16 p1 = READ_BE_UINT16(&pixelData[pOff + 2]);
+ uint16 p2 = READ_BE_UINT16(&pixelData[pOff + 4]);
+ for (int bit = 15; bit >= 0; bit--) {
+ if (!(mask & (1 << bit))) {
+ int color = ((p0 >> bit) & 1) | (((p1 >> bit) & 1) << 1) | (((p2 >> bit) & 1) << 2);
+ _gameOverBackgroundFrame->setPixel(col * 16 + (15 - bit), destRow, paletteColors[color]);
+ }
+ }
+ }
+ destRow++;
+ }
+ }
+ // Bottom portion (19 rows)
+ for (int r = 0; r < kBottomRows; r++) {
+ int srcRow = kTopRows + r;
+ for (int col = 0; col < kColumnsPerRow; col++) {
+ uint16 mask = READ_BE_UINT16(&maskData[srcRow * kMaskBytesPerRow + col * 2]);
+ int pOff = srcRow * kPixelBytesPerRow + col * 6;
+ uint16 p0 = READ_BE_UINT16(&pixelData[pOff]);
+ uint16 p1 = READ_BE_UINT16(&pixelData[pOff + 2]);
+ uint16 p2 = READ_BE_UINT16(&pixelData[pOff + 4]);
+ for (int bit = 15; bit >= 0; bit--) {
+ if (!(mask & (1 << bit))) {
+ int color = ((p0 >> bit) & 1) | (((p1 >> bit) & 1) << 1) | (((p2 >> bit) & 1) << 2);
+ _gameOverBackgroundFrame->setPixel(col * 16 + (15 - bit), destRow, paletteColors[color]);
+ }
+ }
+ }
+ destRow++;
+ }
+ }
+
// Load embedded ProTracker module for background music
// Module is at file offset 0x3D5A6 (memory 0x3D58A), ~86260 bytes
static const int kModOffset = 0x3D5A6;
@@ -237,6 +330,9 @@ void CastleEngine::loadAssetsAmigaDemo() {
}
void CastleEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
+ drawLiftingGate(surface);
+ drawDroppingGate(surface);
+
drawStringInSurface(_currentArea->_name, 97, 182, 0, 0, surface);
uint32 black = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0x00, 0x00);
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 76b536d0d91..53b150ba284 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -421,6 +421,8 @@ void CastleEngine::beforeStarting() {
waitInLoop(250);
else if (isSpectrum())
waitInLoop(100);
+ else if (isAmiga() || isAtariST())
+ waitInLoop(250);
}
void CastleEngine::gotoArea(uint16 areaID, int entranceID) {
@@ -1696,6 +1698,8 @@ void CastleEngine::drawLiftingGate(Graphics::Surface *surface) {
duration = 250;
else if (isSpectrum())
duration = 100;
+ else if (isAmiga() || isAtariST())
+ duration = 250;
if ((_gameStateControl == kFreescapeGameStateStart || _gameStateControl == kFreescapeGameStateRestart) && _ticks <= duration) { // Draw the _gameOverBackgroundFrame gate lifting up slowly
int gate_w = _gameOverBackgroundFrame->w;
Commit: d92aaeb1cd7a8f69c593fe35451f2ca7f6496e4a
https://github.com/scummvm/scummvm/commit/d92aaeb1cd7a8f69c593fe35451f2ca7f6496e4a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-08T21:30:55+01:00
Commit Message:
FREESCAPE: add sound for castle cpc
Changed paths:
engines/freescape/games/castle/castle.cpp
engines/freescape/games/castle/cpc.cpp
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 53b150ba284..e377951a8fd 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -1002,7 +1002,7 @@ void CastleEngine::drawFullscreenGameOverAndWait() {
if (isDOS()) {
// TODO: playSound(X, false, _soundFxHandle);
- } else if (isSpectrum()) {
+ } else if (isSpectrum() || isCPC()) {
playSound(9, false, _soundFxHandle);
}
@@ -1849,7 +1849,7 @@ void CastleEngine::updateThunder() {
_gfx->drawThunder(_thunderTextures[0], _position + _thunderOffset, 100);
_thunderFrameDuration--;
if (_thunderFrameDuration == 0)
- if (isSpectrum())
+ if (isSpectrum() || isCPC())
playSound(8, false, _soundFxHandle);
return;
}
diff --git a/engines/freescape/games/castle/cpc.cpp b/engines/freescape/games/castle/cpc.cpp
index a1d1bb1374e..f0e86d68a64 100644
--- a/engines/freescape/games/castle/cpc.cpp
+++ b/engines/freescape/games/castle/cpc.cpp
@@ -30,14 +30,33 @@ namespace Freescape {
void CastleEngine::initCPC() {
_viewArea = Common::Rect(40, 33 - 2, 280, 152);
- _soundIndexShoot = 5;
- _soundIndexCollide = -1;
- _soundIndexFallen = -1;
- _soundIndexStepUp = -1;
- _soundIndexStepDown = -1;
- _soundIndexMenu = -1;
- _soundIndexStart = 6;
- _soundIndexAreaChange = 7;
+
+ // Sound indices verified from Z80 disassembly of CM.BIN.
+ // The CPC version uses the same sound IDs as the ZX Spectrum version
+ // (see castlemaster2-annotated-zx-spectrum.asm for SFX constant names).
+ //
+ // Direct calls in the binary:
+ // ld a,5 / call 0x786F at 0x6E5D -> shoot/throw rock (SFX_THROW_ROCK_OR_LAND)
+ // ld a,3 / call 0x786F at 0x25E5+ -> menu select / collision (SFX_MENU_SELECT)
+ // ld a,9 / call z,0x786F at 0x88F1 -> gate close (SFX_GATE_CLOSE)
+ // ld a,15 / call 0x786F at 0x8921 -> end game sequence
+ //
+ // Deferred via _requested_SFX variable (0xCFD9):
+ // ld a,12 / ld (0xCFD9),a at 0x62A5, 0x634A -> step up/down (SFX_CLIMB_DROP)
+ // ld a,7 / ld (0xCFD9),a at 0x7554 -> area change/teleport (SFX_GAME_START)
+ // ld a,6 / ld (0xCFD9),a at 0x8625 -> falling (SFX_FALLING)
+ // ld a,8 / ld (0xCFD9),a at 0x864E -> lightning/landing (SFX_LIGHTNING)
+ // ld a,13 / ld (0xCFD9),a at 0x7597 -> escaped (SFX_OPEN_ESCAPED)
+ _soundIndexShoot = 5; // SFX_THROW_ROCK_OR_LAND
+ _soundIndexCollide = 3; // SFX_MENU_SELECT (also used for collisions)
+ _soundIndexFall = 6; // SFX_FALLING (during fall)
+ _soundIndexFallen = 5; // SFX_THROW_ROCK_OR_LAND (landing after fall)
+ _soundIndexStartFalling = -1; // Not used separately in Castle CPC
+ _soundIndexStepUp = 12; // SFX_CLIMB_DROP
+ _soundIndexStepDown = 12; // SFX_CLIMB_DROP
+ _soundIndexMenu = 3; // SFX_MENU_SELECT
+ _soundIndexStart = 7; // SFX_GAME_START
+ _soundIndexAreaChange = 7; // SFX_GAME_START (same as ZX)
}
@@ -148,6 +167,7 @@ void CastleEngine::loadAssetsCPCFullGame() {
case Common::EN_ANY:
loadRiddles(&file, 0x1b75 - 2 - 9 * 2, 9);
load8bitBinary(&file, 0x791a, 16);
+ loadSoundsCPC(&file, 0x21E2, 48, 0x2212, 204, 0x2179, 105);
file.seek(0x2724);
for (int i = 0; i < 90; i++) {
Commit: 4f7d766b4a61c1aa314bffe6c9d7e4c59a94c688
https://github.com/scummvm/scummvm/commit/4f7d766b4a61c1aa314bffe6c9d7e4c59a94c688
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-08T21:30:55+01:00
Commit Message:
FREESCAPE: fixed a few bugs in the driller c64 music emulation
Changed paths:
engines/freescape/games/driller/c64.music.cpp
engines/freescape/games/driller/c64.music.h
diff --git a/engines/freescape/games/driller/c64.music.cpp b/engines/freescape/games/driller/c64.music.cpp
index 357f4a05af8..52493ae7a0b 100644
--- a/engines/freescape/games/driller/c64.music.cpp
+++ b/engines/freescape/games/driller/c64.music.cpp
@@ -99,7 +99,7 @@ const int NUM_INSTRUMENTS = sizeof(instrumentDataA0) / 8;
// Arpeggio Data (0x157A - 0x157E)
const uint8_t arpeggio_data[] = {0x00, 0x0C, 0x18};
-const int NUM_ARPEGGIOS = 1; // Only one arpeggio table is defined
+// Only one arpeggio table is defined (3 entries: +0, +12, +24 semitones)
// Music Data Pointers and Structures
// Need to load the actual PRG file into a buffer (_musicData)
@@ -168,9 +168,8 @@ const int NUM_PATTERNS = sizeof(pattern_addresses) / sizeof(pattern_addresses[0]
// Tune Data (0x1054, 0x15D5 - 0x15E5)
const uint8_t tune_tempo_data[] = {0x00, 0x03, 0x03}; // tempos for tune 0, 1, 2
const uint8_t *const tune_track_data[][3] = {
- {nullptr, nullptr, nullptr}, // Tune 0 (no data specified, likely silent/unused)
+ {nullptr, nullptr, nullptr}, // Tune 0 (null pointers = stop)
{voice1_track_data, voice2_track_data, voice3_track_data}, // Tune 1
- {voice1_track_data, voice2_track_data, voice3_track_data} // Tune 2 (Assume same as tune 1 for now if needed)
};
const int NUM_TUNES = sizeof(tune_tempo_data) / sizeof(tune_tempo_data[0]);
@@ -183,9 +182,8 @@ const int voice_sid_offset[] = {0, 7, 14};
DrillerSIDPlayer::DrillerSIDPlayer() : _sid(nullptr),
_playState(STOPPED),
_targetTuneIndex(0),
- _globalTempo(3), // Default tempo
- _globalTempoCounter(1), // Start immediately
- _framePhase(0)
+ _globalTempo(3), // Default tempo
+ _globalTempoCounter(1) // Start immediately
{
initSID();
@@ -290,22 +288,14 @@ void DrillerSIDPlayer::onTimer() {
}
// Corresponds to voice_done (0x09A1)
- // The original code increments x by 7 for each voice, so the third voice call has x=14 (0x0E)
- // This check ensures the tempo counter is handled only once after all voices are processed.
- // cpx #$0E ; bne @done
- _framePhase += 7;
- if (_framePhase >= 14) { // In practice, this will be 21, but we just need to check it's the third voice's turn
- _framePhase = 0;
-
- // dec tempo_ctr (0x09A5)
- _globalTempoCounter--;
-
- // bpl @done (0x09A8)
- if (_globalTempoCounter < 0) {
- // lda tempo; sta tempo_ctr (0x09AA)
- _globalTempoCounter = _globalTempo;
- debug(DEBUG_LEVEL >= 2, "Driller: Tempo Tick! Reloading counter to %d", _globalTempoCounter);
- }
+ // After all 3 voices processed (cpx #$0E), handle tempo counter once per frame
+ // dec tempo_ctr (0x09A5)
+ _globalTempoCounter--;
+
+ // bpl @done (0x09A8)
+ if (_globalTempoCounter < 0) {
+ // lda tempo; sta tempo_ctr (0x09AA)
+ _globalTempoCounter = _globalTempo;
}
}
@@ -464,13 +454,7 @@ void DrillerSIDPlayer::playVoice(int voiceIndex) {
const uint8_t *instA0 = &instrumentDataA0[instBase];
const uint8_t *instA1 = &instrumentDataA1[instBase];
- // Hard Restart / Buzz Effect Check (Inst A0[7] & 0x01) - Apply if active
- // This check was previously in applyNote, moved here to match L1005 check location relative to effects
- if (v.hardRestartActive) {
- applyHardRestart(v, sidOffset, instA0, instA1);
- }
-
- // Glide down effect? (L094E) - Inst A0[7] & 0x04
+ // Waveform transition effect (L0944-L095E) - Inst A0[7] & 0x04
// This logic updates ctrl register $D404, likely wave or gate
if (instA0[7] & 0x04) {
if (v.glideDownTimer > 0) { // voice1_two_ctr,x (0xD3E)
@@ -550,8 +534,8 @@ void DrillerSIDPlayer::playVoice(int voiceIndex) {
}
// Reset state related to previous note/effects for gate control
- _tempControl3 = 0xFF; // Reset gate mask (0x09D0) - Currently unused in C++ code
- v.whatever0 = 0; // Reset effect states (0x09D5 onwards)
+ v.gateMask = 0xFF; // Reset gate mask (0x09D0: lda #$FF; sta control3)
+ v.whatever0 = 0; // Reset effect states (0x09D5 onwards)
v.whatever1 = 0;
v.whatever2 = 0;
@@ -611,23 +595,17 @@ void DrillerSIDPlayer::playVoice(int voiceIndex) {
}
uint8_t portaParam = v.patternDataPtr[v.patternIndex]; // Consume data byte
- if (v.currentNote > 0) {
- // Set porta type (1=Down(FB), 2=Up(FC)) or (3=DownH, 4=UpH)
- if (cmd == 0xFB) { // effect_fb_1
- v.whatever2 = (instA0[7] & 0x02) ? 3 : 1; // (0A01)
- debug(DEBUG_LEVEL >= 2, "Driller V%d: Cmd FB, Porta Down Param = $%02X (Type %d)", voiceIndex, portaParam, v.whatever2);
- } else { // FC (effect_fc_2)
- v.whatever2 = (instA0[7] & 0x02) ? 4 : 2; // (0A17 -> 0A01)
- debug(DEBUG_LEVEL >= 2, "Driller V%d: Cmd FC, Porta Up Param = $%02X (Type %d)", voiceIndex, portaParam, v.whatever2);
- }
-
- v.portaStepRaw = portaParam; // Store raw porta speed (0A0A / 0A19->0A0A)
- v.whatever0 = 0; // Reset vibrato state (0A0D)
- v.whatever1 = 0; // Reset arpeggio state (0A0F)
- v.portaSpeed = 0; // Force recalc
- } else {
- debug(DEBUG_LEVEL >= 2, "Driller V%d: Ignoring FB/FC command, no note playing.", voiceIndex);
+ // FB = porta type 1 (down lo), FC = porta type 2 (up lo)
+ // Assembly: FB -> lda #$01 (0x09FF), FC -> lda #$02 (0x0A17)
+ if (cmd == 0xFB) {
+ v.whatever2 = 1; // (0x09FF: lda #$01; sta whatever+2)
+ } else { // FC
+ v.whatever2 = 2; // (0x0A17: lda #$02)
}
+ v.portaStepRaw = portaParam; // sta voice1_something (0x0A0A)
+ v.whatever1 = 0; // sta whatever+1 (0x0A0F)
+ v.whatever0 = 0; // sta whatever (0x0A12)
+ v.portaSpeed = 0; // Force recalc
v.patternIndex++; // Continue reading pattern (0A15)
} else if (cmd == 0xFA) { // --- Effect FA: Set Instrument --- (0x0A1B)
@@ -684,168 +662,160 @@ void DrillerSIDPlayer::playVoice(int voiceIndex) {
v.patternIndex++;
} else { // --- Plain Note --- (0x0A1D -> 0A44)
- v.currentNote = cmd; // Store note value (0A44)
- debug(DEBUG_LEVEL >= 2, "Driller V%d: Note Cmd = $%02X (%d)", voiceIndex, v.currentNote, v.currentNote);
- // Set delay counter based on previously read duration (FD command)
- v.delayCounter = v.noteDuration; // (0A47 -> 0A4A)
+ v.currentNote = cmd; // Store note value (0A44: sta stuff+3)
+ // Set delay counter based on previously read duration (0A47-0A4A)
+ v.delayCounter = v.noteDuration;
- // Reset hard restart counters (0A4D)
+ // Reset hard restart counters (0A4D-0A52)
v.whatever3 = 0;
v.whatever4 = 0;
- // Reset glide down timer (0A55)
+ // Reset glide down timer (0A55-0A57)
v.glideDownTimer = 2; // voice1_two_ctr = 2
- // Handle legato/slide (Instrument A0[7] & 0x02) (0A5D)
- if (instA0[7] & 0x02) { // Check legato bit
- debug(DEBUG_LEVEL >= 3, "Driller V%d: Legato instrument flag set.", voiceIndex);
- }
-
- // Apply Note Data
+ // Apply Note Data (0A5D-0AB3)
applyNote(v, sidOffset, instA0, instA1, voiceIndex);
- // Continue reading pattern (but we are done with this note)
+ // Continue reading pattern
v.patternIndex++;
- noteProcessed = true; // Exit the pattern reading loop for this frame
+ noteProcessed = true;
}
} // End while(!noteProcessed)
// --- End of inlined pattern reading logic ---
+
+ // L0AFC: Post-note effect setup - determine which continuous effect is active
+ postNoteEffectSetup(v, sidOffset, instA0, instA1);
}
}
- // ALWAYS apply continuous effects for the current state of the voice, then return.
+ // ALWAYS apply continuous effects (L0B33+) for the current state of the voice.
+ // This runs every frame: on tempo ticks after note processing, and on non-tempo ticks directly.
applyContinuousEffects(v, sidOffset, instA0, instA1);
-
- // After processing note or commands for this tick, if a note wasn't fully processed (e.g. pattern end)
- // we might need to apply effects. But if noteProcessed = true, applyNote was called which handles final writes.
- // If noteProcessed = false (e.g. loop break), effects might need applying.
- // Let's assume effects are only applied when a note holds or on non-tempo ticks.
- // The call to applyContinuousEffects happens *outside* this loop if the delay counter held.
}
// --- Note Application ---
-// --- Note Application ---
+// Corresponds to @plain_note (0x0A44) through L0AAD (0x0AB3)
void DrillerSIDPlayer::applyNote(VoiceState &v, int sidOffset, const uint8_t *instA0, const uint8_t *instA1, int voiceIndex) {
- // Corresponds to 0xA70 onwards
-
- uint8_t note = v.currentNote;
- uint16_t newPulseWidth = 0;
- uint8_t pwLoByte = 0;
- uint8_t pwHiNibble = 0;
- bool isRest = (note == 0);
- uint8_t writeAD = 0;
- uint8_t writeSR = 0;
- int currentInstNum = 0;
-
- // --- EFFECT INITIALIZATION ---
- // This block correctly determines which continuous effect (Arp, Vib, etc.)
- // should be active for the new note based on the instrument data.
- v.whatever0 = 0; // Vibrato flag
- v.whatever1 = 0; // Arpeggio flag
- v.whatever2 = 0; // Portamento flag
- v.portaSpeed = 0;
-
- if (instA1[4] != 0) { // Arpeggio from InstA1[4]
- uint8_t arpData = instA1[4];
- v.arpTableIndex = arpData & 0x0F;
- v.arpSpeedHiNibble = (arpData & 0xF0) >> 4;
- if (v.arpTableIndex >= NUM_ARPEGGIOS) v.arpTableIndex = 0;
- v.stuff_arp_counter = 0;
- v.stuff_arp_note_index = 0;
- v.whatever1 = 1;
- } else if (instA1[0] != 0) { // Vibrato from InstA1[0]
- v.things_vib_depth = instA1[0];
- v.things_vib_delay_reload = instA1[1];
- v.things_vib_delay_ctr = v.things_vib_delay_reload;
- v.things_vib_state = 0;
- v.whatever0 = 1;
- } else if (instA0[5] != 0) { // Arpeggio from InstA0[5]
- uint8_t arpData = instA0[5];
- v.arpTableIndex = arpData & 0x0F;
- v.arpSpeedHiNibble = (arpData & 0xF0) >> 4;
- if (v.arpTableIndex >= NUM_ARPEGGIOS) v.arpTableIndex = 0;
- v.stuff_arp_counter = 0;
- v.stuff_arp_note_index = 0;
- v.whatever1 = 1;
+ uint8_t note = v.currentNote; // Already stored at @plain_note (0x0A44)
+ // v.delayCounter already set from v.noteDuration (0x0A47-0x0A4A)
+ // v.whatever3/4 already reset to 0 (0x0A4D-0x0A52)
+ // v.glideDownTimer already set to 2 (0x0A55-0x0A57)
+
+ // Check legato bit instA0[7] & 0x02 (0x0A5D-0x0A6D)
+ if (instA0[7] & 0x02) {
+ // Legato: restore PW values from FA command backup
+ v.something_else[0] = v.something_else[1]; // something_else+1 -> something_else+0
+ v.something_else[2] = v.ctrl0; // ctrl0 -> something_else+2
}
- // --- NOTE HANDLING ---
- if (isRest) {
+ // Handle note=0 (rest) at L0A70
+ if (note == 0) {
+ // Load previous note from things+6 (0x0A75)
note = v.currentNoteSlideTarget;
- v.currentNoteSlideTarget = 0;
- if (note == 0) {
- v.keyOn = false;
- goto WriteFinalControlReg;
- }
- v.keyOn = true; // Keep gate on for slide
+ v.currentNote = note; // Update stuff+3 equivalent
+ v.currentNoteSlideTarget = 0; // Clear things+6 (0x0A7B-0x0A7D)
+
+ // dec control3 (0x0A83)
+ v.gateMask--;
+
+ // bne L0AAD - since control3 was 0xFF, now 0xFE, always branches
+ // Skip frequency write entirely for rests, jump to L0AAD
} else {
- v.currentNoteSlideTarget = note;
- v.keyOn = true;
+ // Non-zero note: store as previous note (L0A88)
+ v.currentNoteSlideTarget = note; // things+6 = note
+
+ // Set frequency from note (L0A8C-L0AA1)
+ if (note >= 96) note = 95;
+ SID_Write(sidOffset + 1, frq_hi[note]); // $D401
+ SID_Write(sidOffset + 0, frq_lo[note]); // $D400
+ // Store in stuff variables: stuff[0]=lo, stuff[1]=lo, stuff[2]=hi, stuff[4]=hi
+ uint8_t fLo = frq_lo[note];
+ uint8_t fHi = frq_hi[note];
+ v.stuff_freq_porta_vib = fLo | (fHi << 8); // stuff[0]/[4]
+ v.stuff_freq_base = fLo | (fHi << 8); // stuff[1]/[2]
+ v.stuff_freq_hard_restart = fLo | (fHi << 8);
+ v.currentFreq = v.stuff_freq_porta_vib;
+
+ // Write initial waveform (gate-on transient): instA0[6] -> $D404 (L0AA7-L0AAA)
+ SID_Write(sidOffset + 4, instA0[6]);
}
- // --- FREQUENCY ---
- if (note >= 96) note = 95;
- v.baseFreq = frq_lo[note] | (frq_hi[note] << 8);
- v.stuff_freq_base = v.baseFreq;
- v.stuff_freq_porta_vib = v.baseFreq;
- v.stuff_freq_hard_restart = v.baseFreq;
- v.currentFreq = v.baseFreq;
- SID_Write(sidOffset + 0, frq_lo[note]);
- SID_Write(sidOffset + 1, frq_hi[note]);
+ // L0AAD: Write control register with gate mask
+ // lda instA0[1]; AND control3; sta $D404 (0x0AAD-0x0AB3)
+ SID_Write(sidOffset + 4, instA0[1] & v.gateMask);
- // --- WAVEFORM ---
- v.waveform = instA0[6];
+ // Write ADSR from instrument (0x0AB6-0x0ABF)
+ SID_Write(sidOffset + 5, instA0[2]); // Attack / Decay
+ SID_Write(sidOffset + 6, instA0[3]); // Sustain / Release
- // --- HARD RESTART ---
- if (instA0[7] & 0x01) {
- v.hardRestartActive = true;
- v.hardRestartDelay = 0;
- v.hardRestartCounter = 0;
- v.hardRestartValue = instA1[5];
- } else {
- v.hardRestartActive = false;
+ // Write Pulse Width from something_else (0x0AC2-0x0ACB)
+ SID_Write(sidOffset + 2, v.something_else[0]); // PW Lo
+ SID_Write(sidOffset + 3, v.something_else[2]); // PW Hi
+
+ v.pulseWidth = v.something_else[0] | (v.something_else[2] << 8);
+}
+
+// --- Post-Note Effect Setup ---
+// Corresponds to L0AFC in the assembly. Runs after each note is applied.
+// Determines which continuous effect should be active based on instrument data.
+void DrillerSIDPlayer::postNoteEffectSetup(VoiceState &v, int sidOffset, const uint8_t *instA0, const uint8_t *instA1) {
+ // L0AFC: lda voice1_things+6,x; beq L0B33
+ if (v.currentNoteSlideTarget == 0)
+ return; // No note stored, skip to continuous effects (PW LFO etc.)
+
+ // Check if portamento already active from FB/FC pattern command (L0B04)
+ // lda voice1_whatever+2,x; bne L0B17
+ if (v.whatever2 != 0) {
+ // Already have porta from pattern command, go to porta processing
+ return; // Porta will run in applyContinuousEffects
}
- // --- ADSR (Corrected) ---
- // As per disassembly at 0xAB6, ADSR is read from instA0[2] and instA0[3].
- writeAD = instA0[2];
- writeSR = instA0[3];
- currentInstNum = v.instrumentIndex / 8;
-
- // --- PATCH: Override ADSR for specific instruments ---
- // Instruments 1 and 4 seem to have incorrect ADSR data in the disassembly,
- // requiring this hardcoded override to sound correct.
- if (currentInstNum == 1 || currentInstNum == 4) {
- writeAD = 0xA0;
- writeSR = 0xF0;
+ int instBase = v.instrumentIndex;
+ if (instBase < 0 || (size_t)instBase >= sizeof(instrumentDataA0))
+ instBase = 0;
+
+ // Check instA1[4] for instrument-level portamento (L0B09)
+ // lda possibly_instrument_a1+4,y; beq L0B1A
+ if (instA1[4] != 0) {
+ v.whatever2 = instA1[4]; // Store as porta type (L0B0E)
+ v.portaStepRaw = instA1[3]; // Store porta speed (L0B11-L0B14)
+ v.portaSpeed = 0; // Force recalc
+ return; // jmp L0C5A - porta will run in applyContinuousEffects
}
- SID_Write(sidOffset + 5, writeAD); // Attack / Decay
- SID_Write(sidOffset + 6, writeSR); // Sustain / Release
- debug(DEBUG_LEVEL >= 3, "Driller V%d: Set ADSR = $%02X / $%02X", voiceIndex, writeAD, writeSR);
-
- // --- PULSE WIDTH (Corrected) ---
- // As per disassembly at 0xAC2, Pulse Width is derived from the nibbles of instA0[0],
- // which are loaded into the `something_else` variables by the FA (Set Instrument) command.
- pwLoByte = v.something_else[0];
- pwHiNibble = v.something_else[2] & 0x0F;
- newPulseWidth = pwLoByte | (pwHiNibble << 8);
- v.pulseWidth = newPulseWidth;
- SID_Write(sidOffset + 2, v.pulseWidth & 0xFF);
- SID_Write(sidOffset + 3, (v.pulseWidth >> 8) & 0x0F);
- debug(DEBUG_LEVEL >= 3, "Driller V%d: Set PW = %d ($%03X) from instA0[0] nibbles", voiceIndex, v.pulseWidth, v.pulseWidth);
-
-WriteFinalControlReg:
- // --- FINAL CONTROL REGISTER (GATE/WAVEFORM) ---
- uint8_t ctrl = v.waveform;
- if (v.keyOn) {
- ctrl |= 0x01; // Gate On
- } else {
- ctrl &= 0xFE; // Gate Off
+ // Check instA0[5] for arpeggio (L0B1A -> L0E67)
+ // lda possibly_instrument_a0+5,y; beq L0B22
+ if (instA0[5] != 0) {
+ // L0E67: arpeggio setup
+ uint8_t arpData = instA0[5];
+ v.arpTableIndex = arpData & 0x0F; // and #$0F -> ctrl1
+ v.arpSpeedHiNibble = (arpData & 0xF0) >> 4; // and #$F0; lsr*4 -> stuff+5
+ v.stuff_arp_counter = 0; // sta stuff+6
+ v.whatever1 = 1; // sta whatever+1
+ v.whatever0 = 0; // sta whatever
+ return; // jmp voice_done
+ }
+
+ // L0B22: Clear arpeggio flag (A=0 from beq)
+ v.whatever1 = 0;
+
+ // Check instA1[0] for vibrato (L0B25 -> L0E89)
+ // lda possibly_instrument_a1,y; beq L0B2D
+ if (instA1[0] != 0) {
+ // L0E89: vibrato setup
+ v.things_vib_depth = instA1[0]; // sta things+1
+ v.things_vib_delay_reload = instA1[1]; // sta things+2
+ v.things_vib_delay_ctr = v.things_vib_delay_reload; // sta things+3
+ v.things_vib_state = 0; // sta things
+ v.whatever1 = 0; // sta whatever+1
+ v.whatever0 = 1; // sta whatever
+ return; // jmp voice_done
}
- SID_Write(sidOffset + 4, ctrl);
- debug(DEBUG_LEVEL >= 2, "Driller V%d: Final Control Reg Write = $%02X (Wave=$%02X, Gate=%d)", voiceIndex, ctrl, v.waveform, v.keyOn);
+
+ // L0B2D: Clear vibrato flag (A=0 from beq)
+ v.whatever0 = 0;
+ // jmp voice_done
}
// --- Continuous Effect Application (Vibrato, Porta, Arp) ---
@@ -855,155 +825,150 @@ void DrillerSIDPlayer::applyContinuousEffects(VoiceState &v, int sidOffset, cons
uint16_t freq = v.stuff_freq_porta_vib; // Start with base freq + porta/vib from previous step
bool freqDirty = false; // Track if frequency needs writing
- // Instrument A0[4] based frequency LFO (L0B33) - PW LFO?
+ // PW LFO (L0B33-L0B82) - instA0[4] = modulation speed (stored in control1)
uint8_t lfoSpeed = instA0[4];
if (lfoSpeed != 0) {
- // This LFO modifies 'something_else', which we mapped to PW registers based on FA command logic?
- // Or does it modify PW directly based on current PW? Let's assume it modifies current PW.
- uint16_t currentPW = v.pulseWidth; // Use the state variable
- if (v.whatever2_vibDirToggle == 0) { // Direction toggle (0B3B)
- currentPW += lfoSpeed;
- if (currentPW > 0x0E00 || currentPW < lfoSpeed) { // Check wrap around too
- currentPW = 0x0E00; // Clamp
- v.whatever2_vibDirToggle = 1; // Change direction (0B5D)
+ // Operates on something_else[0] (lo byte) and something_else[2] (hi nibble)
+ if (v.whatever2_vibDirToggle == 0) {
+ // Add phase (L0B40-L0B5D): clc; adc control1 on lo; adc #0 on hi
+ uint16_t sum = (uint16_t)v.something_else[0] + lfoSpeed;
+ v.something_else[0] = sum & 0xFF;
+ v.something_else[2] = v.something_else[2] + (sum >> 8);
+ SID_Write(sidOffset + 2, v.something_else[0]); // $D402
+ SID_Write(sidOffset + 3, v.something_else[2]); // $D403
+ // clc; cmp #$0E - check hi byte >= 0x0E (L0B59)
+ if (v.something_else[2] >= 0x0E) {
+ v.whatever2_vibDirToggle = 1; // inc whatever2 (L0B5D)
}
} else {
- // Need signed arithmetic potentially if currentPW could go below lfoSpeed
- if (currentPW >= lfoSpeed) {
- currentPW -= lfoSpeed;
- } else {
- currentPW = 0;
- }
- if (currentPW < 0x0800) { // Limit check (0B7B)
- currentPW = 0x0800; // Clamp
- v.whatever2_vibDirToggle = 0; // Change direction (0B7F)
+ // Subtract phase (L0B62-L0B7F): sec; sbc control1 on lo; sbc #0 on hi
+ uint16_t diff = (uint16_t)v.something_else[0] + 0x100 - lfoSpeed;
+ v.something_else[0] = diff & 0xFF;
+ if (diff < 0x100) // borrow
+ v.something_else[2]--;
+ SID_Write(sidOffset + 2, v.something_else[0]); // $D402
+ SID_Write(sidOffset + 3, v.something_else[2]); // $D403
+ // clc; cmp #$08 - check hi byte < 0x08 (L0B7B)
+ if (v.something_else[2] < 0x08) {
+ v.whatever2_vibDirToggle = 0; // dec whatever2 (L0B7F)
}
}
- currentPW &= 0x0FFF;
- if (v.pulseWidth != currentPW) {
- v.pulseWidth = currentPW;
- SID_Write(sidOffset + 2, v.pulseWidth & 0xFF); // Write PW Lo (0B4A / 0B6C)
- SID_Write(sidOffset + 3, (v.pulseWidth >> 8) & 0x0F); // Write PW Hi (0B55 / 0B77)
- debug(1, "Driller 1: PW LFO Updated PW = %d ($%03X)", v.pulseWidth, v.pulseWidth);
- }
+ v.pulseWidth = v.something_else[0] | (v.something_else[2] << 8);
}
// Arpeggio (L0B82) - Check 'whatever1' flag
if (v.whatever1) {
- const uint8_t *arpTable = &arpeggio_data[0]; // Only one table defined
-
- // Speed calculation from 0B98 - checks counter against 'stuff+5' (arpSpeedHiNibble)
- uint8_t speed = v.arpSpeedHiNibble; // This was set from InstA1[4] or InstA0[5] hi nibble
- if (speed == 0)
- speed = 1; // Avoid division by zero or infinite loop
-
- v.stuff_arp_counter++;
- if (v.stuff_arp_counter >= speed) {
- v.stuff_arp_counter = 0;
- // Advance arpeggio note index (0BA0 / 0BBA)
- v.stuff_arp_note_index = (v.stuff_arp_note_index + 1) % 3; // Cycle 0, 1, 2
- debug(1, "Driller 1: Arp Step -> Note Index %d", v.stuff_arp_note_index);
+ // Assembly: single counter (stuff+6) cycles 0..speed-1, used directly as arp table index
+ // lda stuff+6; cmp stuff+5; bne L0BA5; lda #0; sta stuff+6
+ uint8_t speed = v.arpSpeedHiNibble; // stuff+5: set from instA0[5] hi nibble
+ if (v.stuff_arp_counter == speed) {
+ v.stuff_arp_counter = 0; // Reset when counter == speed (L0BA0)
}
- // Calculate arpeggio note (0BA6)
- uint8_t baseNote = v.currentNote; // Note from pattern
- if (baseNote > 0 && baseNote < 96) {
- uint8_t arpOffset = arpTable[v.stuff_arp_note_index]; // Offset from table (0BAA)
+ // tay; lda stuff+3; clc; adc arpeggio_0,y (L0BA5-L0BAD)
+ uint8_t baseNote = v.currentNote;
+ if (baseNote > 0 && baseNote < 96 && v.stuff_arp_counter < 3) {
+ uint8_t arpOffset = arpeggio_data[v.stuff_arp_counter]; // Counter IS the table index
uint8_t arpNote = baseNote + arpOffset;
if (arpNote >= 96)
- arpNote = 95; // Clamp
+ arpNote = 95;
- // Set frequency based on arpeggio note
freq = frq_lo[arpNote] | (frq_hi[arpNote] << 8);
- freqDirty = true;
- // Arpeggio overrides other frequency effects for this frame
- goto WriteFrequency;
- } else {
- // If base note is invalid (e.g., 0), maybe use baseFreq? Or just skip arp?
- // Fall through to allow other effects if arp base note is invalid
+ SID_Write(sidOffset + 0, frq_lo[arpNote]); // sta $D400 (L0BB1)
+ SID_Write(sidOffset + 1, frq_hi[arpNote]); // sta $D401 (L0BB7)
+ v.currentFreq = freq;
}
+
+ v.stuff_arp_counter++; // inc stuff+6 (L0BBA)
+ // jmp voice_done - arpeggio skips vibrato/porta
+ return;
}
// Vibrato (L0BC0 / L0BC8) - Check 'whatever0' flag
+ // Assembly applies frequency modification EVERY frame.
+ // The timer (things+3) only controls when the direction state advances.
if (v.whatever0) {
- if (v.things_vib_delay_reload > 0) { // Only run if delay is set
-
- // --- Fix 3a: Simplify Counter Logic ---
- v.things_vib_delay_ctr--; // Decrement first
- if (v.things_vib_delay_ctr == 0) { // Check if zero AFTER decrementing
- // --- End Fix 3a ---
-
- v.things_vib_delay_ctr = v.things_vib_delay_reload; // Reload counter
-
- int state = v.things_vib_state;
- int32_t current_freq_signed = v.stuff_freq_porta_vib; // Apply vibrato based on current freq (inc. porta)
-
- // Use level 1 for this crucial debug message
- debug(1, "Driller V1: Vib Step - State %d, Depth %d", state, (int16_t)v.things_vib_depth);
+ int state = v.things_vib_state;
+ uint8_t freqLo = v.stuff_freq_porta_vib & 0xFF;
+ uint8_t freqHi = (v.stuff_freq_porta_vib >> 8) & 0xFF;
+
+ // Apply depth based on state (L0C06, L0C2F, L0BD1)
+ // States 0, 3, 4: subtract (down). States 1, 2: add (up).
+ // State 0: L0C06 (beq). States 1,2: L0C2F (cmp #3; bcc). States 3,4: fall through.
+ if (state == 1 || state == 2) {
+ // Add (L0C2F): clc; lda stuff,x; adc things+1,x
+ uint16_t sum = (uint16_t)freqLo + (v.things_vib_depth & 0xFF);
+ freqLo = sum & 0xFF;
+ freqHi = freqHi + (sum >> 8);
+ } else {
+ // Subtract (L0C06/L0BD1): sec; lda stuff,x; sbc things+1,x
+ uint16_t diff = (uint16_t)freqLo + 0x100 - (v.things_vib_depth & 0xFF);
+ freqLo = diff & 0xFF;
+ if (diff < 0x100) // borrow occurred
+ freqHi--;
+ }
- // Apply depth based on state (L0C06, L0C2F, L0BD1)
- // ... (rest of vibrato logic is likely okay) ...
- // States 0, 2, 3 are down; State 1, 4 are up
- if (state == 1 || state == 4) { // Up sweep
- current_freq_signed += v.things_vib_depth;
- } else { // Down sweep (0, 2, 3)
- current_freq_signed -= v.things_vib_depth;
- }
+ v.stuff_freq_porta_vib = (uint16_t)freqLo | ((uint16_t)freqHi << 8);
+ freq = v.stuff_freq_porta_vib;
+ freqDirty = true;
- // Clamp frequency after modification
- if (current_freq_signed < 0)
- current_freq_signed = 0;
- if (current_freq_signed > 0xFFFF)
- current_freq_signed = 0xFFFF;
- v.stuff_freq_porta_vib = (uint16_t)current_freq_signed; // Store result for next frame's base
- freq = v.stuff_freq_porta_vib; // Use vibrato-modified frequency for this frame
- freqDirty = true;
-
- // Advance state (0BF4 / 0C29 / 0C52)
- v.things_vib_state++;
- if (v.things_vib_state >= 5) { // Cycle states 0..4 (0BFA)
- v.things_vib_state = 1; // Loop back to state 1 (upward sweep) (0BFE) - Correct based on diss.
- }
- // Use level 1 for this crucial debug message
- debug(1, "Driller V1: Vib Freq Updated = %d, Next State %d", freq, v.things_vib_state);
+ // Decrement timer, advance state only when expired (dec things+3; bne done)
+ v.things_vib_delay_ctr--;
+ if (v.things_vib_delay_ctr == 0) {
+ v.things_vib_delay_ctr = v.things_vib_delay_reload;
+ v.things_vib_state++;
+ if (v.things_vib_state >= 5) { // cmp #$05; bcc
+ v.things_vib_state = 1; // Reset to state 1 (0BFE)
}
}
} // end if(v.whatever0)
// Portamento (L0C5A) - Check 'whatever2' flag
- if (v.whatever2) { // Note: 'else if' removed, allow porta+vib? Keep 'else if'.
- // Calculate porta speed if not already done (or if param changed?)
- if (v.portaSpeed == 0) { // Calculate only once per porta command
- int16_t speed = v.portaStepRaw; // Raw value from FB/FC command (e.g., 0x01 or 0x80)
- // Disassembly L0C7B (type 1) / L0CA6 (type 2) / L0C96 (type 3) / L0C6B (type 4)
- // Types 1 & 3 are down, 2 & 4 are up. Speed seems absolute value?
- // Let's assume portaStepRaw is the step magnitude.
- if (v.whatever2 == 1 || v.whatever2 == 3) { // Down
- v.portaSpeed = -speed; // Ensure negative for down
- } else { // Up (2 or 4)
- v.portaSpeed = speed; // Ensure positive for up
- }
- debug(1, "Driller 1: Porta Recalc Speed = %d (Raw=%d, Type=%d)", v.portaSpeed, v.portaStepRaw, v.whatever2);
+ // 4 distinct types matching assembly:
+ // Type 1 (L0C7B): CLC+SBC on lo byte (slide down, borrow to hi)
+ // Type 2 (L0CA6): CLC+ADC on lo byte (slide up, carry to hi)
+ // Type 3 (L0C96): SEC+SBC on hi byte only (fast slide down)
+ // Type >= 4 (L0C6B): CLC+ADC on hi byte only (fast slide up)
+ if (v.whatever2) {
+ uint8_t freqLo = v.stuff_freq_porta_vib & 0xFF; // stuff[0]
+ uint8_t freqHi = (v.stuff_freq_porta_vib >> 8) & 0xFF; // stuff[4]
+ uint8_t speed = v.portaStepRaw & 0xFF; // voice1_something
+
+ if (v.whatever2 == 1) {
+ // Type 1 (L0C7B): clc; sbc = subtract (speed+1) from lo, borrow to hi
+ uint16_t diff = (uint16_t)freqLo - speed; // CLC means borrow, so effectively -(speed+1)
+ freqLo = (diff - 1) & 0xFF; // CLC+SBC = subtract with extra borrow
+ if ((diff - 1) > 0xFF) freqHi--; // Propagate borrow
+ SID_Write(sidOffset + 0, freqLo);
+ SID_Write(sidOffset + 1, freqHi);
+ } else if (v.whatever2 == 2) {
+ // Type 2 (L0CA6): clc; adc on lo, carry to hi
+ uint16_t sum = (uint16_t)freqLo + speed;
+ freqLo = sum & 0xFF;
+ freqHi = freqHi + (sum >> 8);
+ SID_Write(sidOffset + 0, freqLo);
+ SID_Write(sidOffset + 1, freqHi);
+ } else if (v.whatever2 == 3) {
+ // Type 3 (L0C96): sec; sbc on hi byte only (fast slide down)
+ freqHi = freqHi - speed;
+ SID_Write(sidOffset + 1, freqHi);
+ } else {
+ // Type >= 4 (L0C6B): clc; adc on hi byte only (fast slide up)
+ freqHi = freqHi + speed;
+ SID_Write(sidOffset + 1, freqHi);
}
- // Apply portamento step
- int32_t tempFreqSigned = v.stuff_freq_porta_vib; // Apply to current frequency
- tempFreqSigned += v.portaSpeed; // Add signed speed
-
- // Clamp frequency
- if (tempFreqSigned > 0xFFFF)
- tempFreqSigned = 0xFFFF;
- if (tempFreqSigned < 0)
- tempFreqSigned = 0;
+ v.stuff_freq_porta_vib = (uint16_t)freqLo | ((uint16_t)freqHi << 8);
+ v.currentFreq = v.stuff_freq_porta_vib;
+ }
- v.stuff_freq_porta_vib = (uint16_t)tempFreqSigned; // Store result for next frame
- freq = v.stuff_freq_porta_vib; // Use the porta-modified frequency for this frame
- freqDirty = true;
- debug(DEBUG_LEVEL >= 3, "Driller: Porta Step -> Freq = %d", freq);
+ // After porta, check for hard restart (L0CBE)
+ // lda instA0[7]; and #$01; beq voice_done; jmp L1005
+ if (instA0[7] & 0x01) {
+ applyHardRestart(v, sidOffset, instA0, instA1);
}
-WriteFrequency:
- // Write final frequency to SID if it was changed by effects
+ // Write final frequency if modified by vibrato (arp and porta write directly)
if (freqDirty && v.currentFreq != freq) {
v.currentFreq = freq;
SID_Write(sidOffset + 0, freq & 0xFF);
diff --git a/engines/freescape/games/driller/c64.music.h b/engines/freescape/games/driller/c64.music.h
index 0ecd3127b80..57168d0e2d1 100644
--- a/engines/freescape/games/driller/c64.music.h
+++ b/engines/freescape/games/driller/c64.music.h
@@ -207,16 +207,13 @@ class DrillerSIDPlayer {
uint8_t _targetTuneIndex; // Tune index requested via startMusic
// Global Timing
- uint8_t _globalTempo; // Tempo value for current tune (0xD10)
+ uint8_t _globalTempo; // Tempo value for current tune (0xD10)
int8_t _globalTempoCounter; // Frame counter for tempo (0xD12), signed to handle < 0 check
- uint8_t _framePhase; // Tracks which voice is being processed (0, 7, 14)
// Voice States
VoiceState _voiceState[3];
- // Internal helpers
- uint8_t _tempControl3; // Temporary storage for gate mask (0xD13)
- // uint8_t _tempControl1; // Temp storage from instrument data (0xD11)
+ // Gate mask is now per-voice (v.gateMask) matching assembly's control3
public:
DrillerSIDPlayer();
@@ -232,6 +229,7 @@ private:
void handleResetVoices();
void playVoice(int voiceIndex);
void applyNote(VoiceState &v, int sidOffset, const uint8_t *instA0, const uint8_t *instA1, int voiceIndex);
+ void postNoteEffectSetup(VoiceState &v, int sidOffset, const uint8_t *instA0, const uint8_t *instA1);
void applyContinuousEffects(VoiceState &v, int sidOffset, const uint8_t *instA0, const uint8_t *instA1);
void applyHardRestart(VoiceState &v, int sidOffset, const uint8_t *instA0, const uint8_t *instA1);
};
Commit: ba72f4991f613c86e389f08bdec3284b56ae3e37
https://github.com/scummvm/scummvm/commit/ba72f4991f613c86e389f08bdec3284b56ae3e37
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-08T21:30:55+01:00
Commit Message:
FREESCAPE: add gate bitmap in castle cpc
Changed paths:
engines/freescape/games/castle/castle.cpp
engines/freescape/games/castle/cpc.cpp
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index e377951a8fd..31652e5133a 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -419,7 +419,7 @@ void CastleEngine::initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *inf
void CastleEngine::beforeStarting() {
if (isDOS())
waitInLoop(250);
- else if (isSpectrum())
+ else if (isSpectrum() || isCPC())
waitInLoop(100);
else if (isAmiga() || isAtariST())
waitInLoop(250);
@@ -1700,6 +1700,8 @@ void CastleEngine::drawLiftingGate(Graphics::Surface *surface) {
duration = 100;
else if (isAmiga() || isAtariST())
duration = 250;
+ else if (isCPC())
+ duration = 100;
if ((_gameStateControl == kFreescapeGameStateStart || _gameStateControl == kFreescapeGameStateRestart) && _ticks <= duration) { // Draw the _gameOverBackgroundFrame gate lifting up slowly
int gate_w = _gameOverBackgroundFrame->w;
diff --git a/engines/freescape/games/castle/cpc.cpp b/engines/freescape/games/castle/cpc.cpp
index f0e86d68a64..a451f469965 100644
--- a/engines/freescape/games/castle/cpc.cpp
+++ b/engines/freescape/games/castle/cpc.cpp
@@ -231,6 +231,125 @@ void CastleEngine::loadAssetsCPCFullGame() {
// Flag Animation: CPC offset 0x2654 (16x9 px, 4 frames)
_flagFrames = loadFramesWithHeaderCPC(&file, 0x2654, 4, cpcPalette);
+ // Gate image (portcullis) for game start/end animation.
+ // The CPC gate is NOT a pre-rendered bitmap; it is procedurally generated
+ // from small pixel pattern tables stored at file offset 0x75EF-0x764C.
+ // Structure: 10 columns à 6 bytes (=240 px wide), repeating bands of
+ // 4-row horizontal bars and 20-row inter-bar vertical bars, with a
+ // single last-row at the bottom. Total height = 119 px (CPC viewport).
+ {
+ // Horizontal bar pattern: 4 rows à 6 bytes (file offset 0x760B)
+ static const byte kGateHorizBar[4][6] = {
+ {0xD2, 0xF0, 0xF0, 0xF0, 0xF0, 0xB4},
+ {0xD2, 0xFA, 0xF8, 0xFB, 0xFB, 0xB5},
+ {0xDA, 0xFB, 0xFF, 0xF0, 0xFE, 0xB5},
+ {0x5A, 0xF0, 0xF0, 0xF0, 0xF0, 0xA5},
+ };
+ // Inter-bar pattern: 20 rows à 2 bytes (file offset 0x7623)
+ // Byte 0 = left-edge pixels (ink in 0xCC bit positions = pixels 0,1)
+ // Byte 1 = right-edge pixels (ink in 0x33 bit positions = pixels 2,3)
+ static const byte kGateInterBar[20][2] = {
+ {0xC0, 0x30}, {0xC0, 0x30}, {0xC8, 0x31}, {0xC8, 0x31},
+ {0xC0, 0x31}, {0xC8, 0x31}, {0xC0, 0x31}, {0xC0, 0x31},
+ {0xC0, 0x30}, {0xC0, 0x31}, {0xC0, 0x31}, {0xC8, 0x30},
+ {0xC8, 0x30}, {0xC0, 0x30}, {0xC8, 0x30}, {0xC8, 0x30},
+ {0xC8, 0x30}, {0xC8, 0x31}, {0xC8, 0x31}, {0xC8, 0x31},
+ };
+ // Last row pattern: 1 row à 2 bytes (file offset 0x764B)
+ static const byte kGateLastRow[2] = {0x80, 0x10};
+
+ static const int kGateWidth = 240; // 10 columns à 6 bytes à 4 px/byte
+ static const int kGateHeight = 119; // CPC max gate y-coordinate (0x77)
+ static const int kColumns = 10;
+ static const int kBytesPerCol = 6;
+
+ uint32 keyColor = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0x24, 0xA5);
+
+ _gameOverBackgroundFrame = new Graphics::ManagedSurface();
+ _gameOverBackgroundFrame->create(kGateWidth, kGateHeight, _gfx->_texturePixelFormat);
+ _gameOverBackgroundFrame->fillRect(Common::Rect(0, 0, kGateWidth, kGateHeight), keyColor);
+
+ for (int y = 0; y < kGateHeight; y++) {
+ int fromBottom = kGateHeight - 1 - y;
+
+ if (fromBottom == 0) {
+ // Last row: edge pixels only (same masking as inter-bar)
+ for (int col = 0; col < kColumns; col++) {
+ int bx = col * kBytesPerCol * 4;
+ // Left edge byte: pixels 0,1 from pattern
+ for (int p = 0; p < 2; p++) {
+ int ink = getCPCPixel(kGateLastRow[0], p, true);
+ if (ink)
+ _gameOverBackgroundFrame->setPixel(bx + p, y, cpcPalette[ink]);
+ }
+ // Right edge byte: pixels 2,3 from pattern
+ for (int p = 2; p < 4; p++) {
+ int ink = getCPCPixel(kGateLastRow[1], p, true);
+ if (ink)
+ _gameOverBackgroundFrame->setPixel(bx + (kBytesPerCol - 1) * 4 + p, y, cpcPalette[ink]);
+ }
+ }
+ } else {
+ // Determine row type from bottom-up gate structure:
+ // fromBottom 1-14: bottom inter-bar (14 rows, pattern indices 0-13)
+ // fromBottom 15-18: bottom horizontal bar (4 rows)
+ // fromBottom >= 19: repeating 24-row sections (20 inter-bar + 4 horiz-bar)
+ bool isHorizBar = false;
+ int patIdx = 0;
+
+ if (fromBottom <= 14) {
+ // Bottom inter-bar section
+ patIdx = 14 - fromBottom; // 0-13
+ } else if (fromBottom <= 18) {
+ // Bottom horizontal bar section
+ isHorizBar = true;
+ patIdx = 18 - fromBottom; // 0-3
+ } else {
+ // Repeating zone: 24-row cycle (20 inter-bar + 4 horizontal bar)
+ int inSection = (fromBottom - 19) % 24;
+ if (inSection < 20) {
+ patIdx = 19 - inSection; // 0-19
+ } else {
+ isHorizBar = true;
+ patIdx = 23 - inSection; // 0-3
+ }
+ }
+
+ if (isHorizBar) {
+ // Horizontal bar: all 6 bytes per column are gate pixels
+ for (int col = 0; col < kColumns; col++) {
+ int bx = col * kBytesPerCol * 4;
+ for (int bi = 0; bi < kBytesPerCol; bi++) {
+ byte cpcByte = kGateHorizBar[patIdx][bi];
+ for (int p = 0; p < 4; p++) {
+ int ink = getCPCPixel(cpcByte, p, true);
+ if (ink)
+ _gameOverBackgroundFrame->setPixel(bx + bi * 4 + p, y, cpcPalette[ink]);
+ }
+ }
+ }
+ } else {
+ // Inter-bar: only edge pixels per column, middle is transparent
+ for (int col = 0; col < kColumns; col++) {
+ int bx = col * kBytesPerCol * 4;
+ // Left edge (byte 0): pixels 0,1 from pattern byte 0
+ for (int p = 0; p < 2; p++) {
+ int ink = getCPCPixel(kGateInterBar[patIdx][0], p, true);
+ if (ink)
+ _gameOverBackgroundFrame->setPixel(bx + p, y, cpcPalette[ink]);
+ }
+ // Right edge (byte 5): pixels 2,3 from pattern byte 1
+ for (int p = 2; p < 4; p++) {
+ int ink = getCPCPixel(kGateInterBar[patIdx][1], p, true);
+ if (ink)
+ _gameOverBackgroundFrame->setPixel(bx + (kBytesPerCol - 1) * 4 + p, y, cpcPalette[ink]);
+ }
+ }
+ }
+ }
+ }
+ }
+
// Note: Riddle frames are not loaded from external files for CPC.
// The _riddleTopFrame, _riddleBackgroundFrame, and _riddleBottomFrame
// will remain nullptr (initialized in constructor).
@@ -261,6 +380,9 @@ void CastleEngine::loadAssetsCPCFullGame() {
}
void CastleEngine::drawCPCUI(Graphics::Surface *surface) {
+ drawLiftingGate(surface);
+ drawDroppingGate(surface);
+
uint32 color = _gfx->_paperColor;
uint8 r, g, b;
Commit: 49f2dc9b34b8c4a73ee4bac959ba3aab622c656d
https://github.com/scummvm/scummvm/commit/49f2dc9b34b8c4a73ee4bac959ba3aab622c656d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-08T21:30:55+01:00
Commit Message:
FREESCAPE: properly parse and show dark amiga title
Changed paths:
engines/freescape/games/dark/amiga.cpp
diff --git a/engines/freescape/games/dark/amiga.cpp b/engines/freescape/games/dark/amiga.cpp
index 9308d0c502c..738bd3e430a 100644
--- a/engines/freescape/games/dark/amiga.cpp
+++ b/engines/freescape/games/dark/amiga.cpp
@@ -20,6 +20,8 @@
*/
#include "common/file.h"
+#include "graphics/palette.h"
+
#include "freescape/freescape.h"
#include "freescape/games/dark/dark.h"
#include "freescape/language/8bitDetokeniser.h"
@@ -29,7 +31,39 @@ namespace Freescape {
void DarkEngine::loadAssetsAmigaFullGame() {
Common::File file;
file.open("0.drk");
- _title = loadAndConvertNeoImage(&file, 0x9930);
+ // Load title image: Amiga non-interleaved bitplanes with Atari ST palette
+ // Palette: 16 words at file offset 0x9934, Atari ST 3-bit $0RGB format
+ file.seek(0x9934);
+ Graphics::Palette pal(16);
+ for (int i = 0; i < 16; i++) {
+ byte v1 = file.readByte();
+ byte v2 = file.readByte();
+ byte r = floor((v1 & 0x07) * 255.0 / 7.0);
+ byte g = floor((v2 & 0x70) * 255.0 / 7.0 / 16.0);
+ byte b = floor((v2 & 0x07) * 255.0 / 7.0);
+ pal.set(i, r, g, b);
+ }
+
+ // Bitplanes: 4 planes x 8000 bytes at file offset 0x99B0, non-interleaved
+ file.seek(0x99B0);
+ Graphics::ManagedSurface *titleSurface = new Graphics::ManagedSurface();
+ titleSurface->create(320, 200, Graphics::PixelFormat::createFormatCLUT8());
+ titleSurface->fillRect(Common::Rect(0, 0, 320, 200), 0);
+ for (int plane = 0; plane < 4; plane++) {
+ for (int y = 0; y < 200; y++) {
+ for (int x = 0; x < 40; x++) {
+ byte b = file.readByte();
+ for (int n = 0; n < 8; n++) {
+ int px = x * 8 + (7 - n);
+ int bit = ((b >> n) & 0x01) << plane;
+ int sample = titleSurface->getPixel(px, y) | bit;
+ titleSurface->setPixel(px, y, sample);
+ }
+ }
+ }
+ }
+ titleSurface->convertToInPlace(_gfx->_texturePixelFormat, pal.data(), pal.size());
+ _title = titleSurface;
file.close();
Common::SeekableReadStream *stream = decryptFileAmigaAtari("1.drk", "0.drk", 798);
Commit: b4868a69f73229fc260bc819b960fe6106b82bae
https://github.com/scummvm/scummvm/commit/b4868a69f73229fc260bc819b960fe6106b82bae
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-08T21:32:56+01:00
Commit Message:
FREESCAPE: EMSCRIPTEN -> USE_FORCED_GLES
Changed paths:
engines/freescape/gfx_opengl_shaders.cpp
diff --git a/engines/freescape/gfx_opengl_shaders.cpp b/engines/freescape/gfx_opengl_shaders.cpp
index 06ba3db09f1..404f500a6fb 100644
--- a/engines/freescape/gfx_opengl_shaders.cpp
+++ b/engines/freescape/gfx_opengl_shaders.cpp
@@ -575,12 +575,12 @@ void OpenGLShaderRenderer::renderFace(const Common::Array<Math::Vector3d> &verti
_triangleShader->use();
_triangleShader->setUniform("mvpMatrix", _mvpMatrix);
- #if !defined(EMSCRIPTEN)
+#if !USE_FORCED_GLES2
if (_debugRenderWireframe) {
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
useColor(0, 255, 0);
}
- #endif
+#endif
if (vertices.size() == 2) {
const Math::Vector3d &v1 = vertices[1];
@@ -598,10 +598,10 @@ void OpenGLShaderRenderer::renderFace(const Common::Array<Math::Vector3d> &verti
glDrawArrays(GL_LINES, 0, 2);
glLineWidth(1);
- #if !defined(EMSCRIPTEN)
+#if !USE_FORCED_GLES2
if (_debugRenderWireframe)
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
- #endif
+#endif
return;
}
@@ -643,10 +643,10 @@ void OpenGLShaderRenderer::renderFace(const Common::Array<Math::Vector3d> &verti
glDrawArrays(GL_LINES, 0, 2);
}
- #if !defined(EMSCRIPTEN)
+#if !USE_FORCED_GLES2
if (_debugRenderWireframe)
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
- #endif
+#endif
}
void OpenGLShaderRenderer::depthTesting(bool enabled) {
More information about the Scummvm-git-logs
mailing list