[Scummvm-git-logs] scummvm master -> 421ca52e30226563cc3dad11fd79b2f13e4f110c
neuromancer
noreply at scummvm.org
Mon May 25 12:09:48 UTC 2026
This automated email contains information about 12 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
cae04f51ea FREESCAPE: basic support for castle atari
424a64b399 FREESCAPE: added complementary occlusion filtering to the renderer
0612df8cdf FREESCAPE: small fixes to the depth sorting
1f06e4f6e2 FREESCAPE: optional adlib rendition of castle music (c64)
db8c052c2c FREESCAPE: optional AY rendition of castle music (c64)
6e0926588c FREESCAPE: initial code to load castle c64
bb7693b60e FREESCAPE: namespace/static cleanup
3402c77839 FREESCAPE: initial implemnetation of castle c64 music
17341bc0f7 FREESCAPE: refactored of castle music code
5b4a8afee9 FREESCAPE: missing common .cpp for castle music
cfe7df2705 FREESCAPE: more function UI for castle c64
421ca52e30 FREESCAPE: improved precision on the drilling for driller
Commit: cae04f51ea0b3df0ed97a5213e58eeeb91171304
https://github.com/scummvm/scummvm/commit/cae04f51ea0b3df0ed97a5213e58eeeb91171304
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-25T14:08:38+02:00
Commit Message:
FREESCAPE: basic support for castle atari
Changed paths:
A engines/freescape/games/castle/atari.cpp
engines/freescape/detection.cpp
engines/freescape/games/castle/amiga.cpp
engines/freescape/games/castle/castle.cpp
engines/freescape/games/castle/castle.h
engines/freescape/loaders/8bitBinaryLoader.cpp
engines/freescape/module.mk
diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index e99ea19e535..f4247e5ff73 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -895,6 +895,23 @@ static const ADGameDescription gameDescriptions[] = {
ADGF_UNSTABLE,
GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERAMIGA, GAMEOPTION_WASD_CONTROLS)
},
+ // Full Castle Master, Atari ST.
+ // The player must provide the Copylock-decrypted game executable as "M.PRG"
+ // and the intro program as "J.PRG" (both Huffman-packed; the engine
+ // decompresses them at load time).
+ {
+ "castlemaster",
+ "",
+ {
+ {"M.PRG", 0, "70975b6656cd00a52ddede00d9ef3e64", 266232},
+ {"J.PRG", 0, "4934cf2f304b8ae5327e92b773acd35c", 58514},
+ AD_LISTEND
+ },
+ Common::EN_ANY,
+ Common::kPlatformAtariST,
+ ADGF_UNSTABLE,
+ GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERATARIST, GAMEOPTION_WASD_CONTROLS)
+ },
{
"castlemaster",
"",
diff --git a/engines/freescape/games/castle/amiga.cpp b/engines/freescape/games/castle/amiga.cpp
index 94f4802790d..5b6bfaeda54 100644
--- a/engines/freescape/games/castle/amiga.cpp
+++ b/engines/freescape/games/castle/amiga.cpp
@@ -69,10 +69,50 @@ byte kAmigaCastleRiddlePalette[16][3] = {
{0xee, 0xcc, 0x66},
};
+// Data offsets used by the intro player. The Amiga and Atari ST builds share
+// the same intro engine/data but lay everything out at different offsets (the
+// Atari intro lives in J.PRG), so the offsets are parameterised here.
+struct CastleIntroLayout {
+ int scrollPlaneA, scrollPlaneB, staticPlane;
+ int fillBands, overlay;
+ int logo, foreground;
+ int sprite1, sprite2, selSprite, objSprite, objArrow;
+ int char1Slot, char2Slot, motionSlot;
+ int motionStatic1, motionSelect, motionLoop;
+ int palBlack, palMain, palSelect;
+ int langTableEN, langTableFR, langTableDE;
+ int palScale; // 0: palette channels are 4-bit (Amiga); 1: 3-bit (Atari ST), scale up
+};
+
+static const CastleIntroLayout kAmigaIntroLayout = {
+ 0x247c, 0x2d64, 0x4664, // scrollPlaneA/B, staticPlane
+ 0x5ba4, 0x5c0c, // fillBands, overlay
+ 0x14ff4, 0x1ac74, // logo, foreground
+ 0x700c, 0xd4ac, 0x108ac, 0x12dec, 0x136dc, // sprite1/2/sel/obj/arrow
+ 0x1e26, 0x1eb8, 0x1f4a, // char1Slot, char2Slot, motionSlot
+ 0x2054, 0x2256, 0x21b0, // motionStatic1, motionSelect, motionLoop
+ 0x1dc6, 0x1de6, 0x1e06, // palBlack, palMain, palSelect
+ 0x1cf2, 0x1d2e, 0x1d6a, // langTable EN/FR/DE
+ 0
+};
+
+static const CastleIntroLayout kAtariIntroLayout = {
+ 0x22, 0x90a, 0x220a,
+ 0x374a, 0x37b2,
+ 0x12b9a, 0x1881a,
+ 0x4bb2, 0xb052, 0xe452, 0x10992, 0x11282,
+ 0x1b32a, 0x1b3bc, 0x1b44e,
+ 0x1b558, 0x1b75a, 0x1b6b4,
+ 0x1b2ca, 0x1b2ea, 0x1b30a,
+ 0x1b178, 0x1b1b4, 0x1b1f0,
+ 1
+};
+
class CastleAmigaIntroPlayer {
public:
- CastleAmigaIntroPlayer(CastleEngine *engine, const Common::Array<byte> &introText)
- : _engine(engine), _data(introText) {
+ CastleAmigaIntroPlayer(CastleEngine *engine, const Common::Array<byte> &introText,
+ const CastleIntroLayout &layout = kAmigaIntroLayout)
+ : _engine(engine), _data(introText), _l(layout) {
_screen[0].resize(kScreenBytes);
_screen[1].resize(kScreenBytes);
reset();
@@ -85,10 +125,10 @@ public:
drawBaseScreen();
drawStaticLogo();
drawStaticForeground();
- fadePalette(0x1dc6);
+ fadePalette(_l.palBlack);
pageFlip();
displayFrame(false, false);
- fadePalette(0x1de6);
+ fadePalette(_l.palMain);
int key = 0;
while (!_aborted) {
@@ -111,7 +151,7 @@ public:
_phaseDone = 0;
_renderMode = 2;
- _motionPtr = 0x2054;
+ _motionPtr = _l.motionStatic1;
updateAnimation();
clearDrawBuffer();
@@ -119,11 +159,11 @@ public:
drawMovingCharacters();
drawOverlay();
drawLanguageText();
- fadePalette(0x1dc6);
+ fadePalette(_l.palBlack);
pageFlip();
displayFrame(false, false);
displayFrame(false, false);
- fadePalette(0x1e06);
+ fadePalette(_l.palSelect);
setSelectionMouseEnabled(true);
while (!_aborted) {
@@ -157,17 +197,17 @@ public:
_choiceState = 0;
_phaseDone = 0;
clearDrawBuffer();
- _motionPtr = 0x2256;
+ _motionPtr = _l.motionSelect;
updateAnimation();
drawBaseScreen();
drawStaticForeground();
drawAnimatedObject();
drawStaticLogo();
- fadePalette(0x1dc6);
+ fadePalette(_l.palBlack);
pageFlip();
displayFrame(false, false);
displayFrame(false, false);
- fadePalette(0x1de6);
+ fadePalette(_l.palMain);
int guard = 0;
while (!_aborted && !_phaseDone && guard++ < 600) {
@@ -196,6 +236,7 @@ private:
CastleEngine *_engine;
const Common::Array<byte> &_data;
+ const CastleIntroLayout &_l;
Common::Array<byte> _screen[2];
uint16 _palette[32];
int _displayBuffer;
@@ -256,9 +297,9 @@ private:
_renderMode = 1;
_arrowFrame = 0;
_choice = 1;
- _character1Ptr = READ_BE_UINT32(_data.data() + 0x1e26);
- _character2Ptr = READ_BE_UINT32(_data.data() + 0x1eb8);
- _motionPtr = READ_BE_UINT32(_data.data() + 0x1f4a);
+ _character1Ptr = READ_BE_UINT32(_data.data() + _l.char1Slot);
+ _character2Ptr = READ_BE_UINT32(_data.data() + _l.char2Slot);
+ _motionPtr = READ_BE_UINT32(_data.data() + _l.motionSlot);
}
static int16 highWord(int32 value) {
@@ -305,9 +346,9 @@ private:
void drawBaseScreen() {
int dst = 38 * kRowBytes;
- dst = drawScrollingPlane(0x247c, _scrollA, 57, dst);
- dst = drawScrollingFourPlane(0x2d64, _scrollB, 40, dst);
- dst = drawStaticFourPlane(0x4664, 33, dst);
+ dst = drawScrollingPlane(_l.scrollPlaneA, _scrollA, 57, dst);
+ dst = drawScrollingFourPlane(_l.scrollPlaneB, _scrollB, 40, dst);
+ dst = drawStaticFourPlane(_l.staticPlane, 33, dst);
drawFillBands(dst);
}
@@ -401,7 +442,7 @@ private:
}
void drawFillBands(int dst) {
- int src = 0x5ba4;
+ int src = _l.fillBands;
for (int band = 0; band < 13; band++) {
int count = READ_BE_UINT16(_data.data() + src);
src += 2;
@@ -422,7 +463,7 @@ private:
}
void drawOverlay() {
- int src = 0x5c0c;
+ int src = _l.overlay;
int dst = 0x1a18;
for (int row = 0; row < 30; row++) {
for (int col = 0; col < 20; col++) {
@@ -476,18 +517,18 @@ private:
}
void drawStaticLogo() {
- drawMaskedBlock4(0x14ff4, 0x820, 0x94, 20);
+ drawMaskedBlock4(_l.logo, 0x820, 0x94, 20);
}
void drawStaticForeground() {
- drawMaskedBlock4(0x1ac74, 0, 0x27, 20);
+ drawMaskedBlock4(_l.foreground, 0, 0x27, 20);
}
void drawMovingCharacters() {
- drawLargeSprite4(0x700c, _sprite1Frame, 0xe60, _sprite1X, _sprite1Y, 0x73, 0x20, 0x08, 4);
- drawLargeSprite4(0xd4ac, _sprite2Frame, 0xd00, _sprite2X, _sprite2Y, 0x68, 0x20, 0x08, 4);
+ drawLargeSprite4(_l.sprite1, _sprite1Frame, 0xe60, _sprite1X, _sprite1Y, 0x73, 0x20, 0x08, 4);
+ drawLargeSprite4(_l.sprite2, _sprite2Frame, 0xd00, _sprite2X, _sprite2Y, 0x68, 0x20, 0x08, 4);
if (_choiceState != 0 && _selectionLift == 0) {
- drawLargeSprite4(0x108ac, 0, 0, _selectionX - 0x20, _selectionY - 0x76, 0x95, 0x40, 0x10, 8);
+ drawLargeSprite4(_l.selSprite, 0, 0, _selectionX - 0x20, _selectionY - 0x76, 0x95, 0x40, 0x10, 8);
return;
}
drawAnimatedObject();
@@ -499,10 +540,10 @@ private:
_selectionLift = 0;
objectVisible = false;
} else {
- drawSprite1(0x12dec + _objectFrame * 0x34, _objectX, _objectY, 13);
+ drawSprite1(_l.objSprite + _objectFrame * 0x34, _objectX, _objectY, 13);
}
if (objectVisible && _renderMode == 3) {
- int src = 0x136dc;
+ int src = _l.objArrow;
if (_choice != 2)
src += 0x36;
src += _arrowFrame * 18;
@@ -706,10 +747,10 @@ private:
int currentLanguageTextTable() const {
if (_engine->_language == Common::FR_FRA)
- return 0x1d2e;
+ return _l.langTableFR;
if (_engine->_language == Common::DE_DEU)
- return 0x1d6a;
- return 0x1cf2;
+ return _l.langTableDE;
+ return _l.langTableEN;
}
int selectionSplitX() const {
@@ -792,6 +833,12 @@ private:
uint16 cur = _palette[i];
int channels[3] = {cur & 0xf, (cur >> 4) & 0xf, (cur >> 8) & 0xf};
int targets[3] = {dst & 0xf, (dst >> 4) & 0xf, (dst >> 8) & 0xf};
+ if (_l.palScale) {
+ // Atari ST palettes store 3-bit channels (0-7); expand to
+ // the 4-bit range the player and display expect.
+ for (int c = 0; c < 3; c++)
+ targets[c] = ((targets[c] & 0x7) << 1) | ((targets[c] & 0x7) >> 2);
+ }
for (int c = 0; c < 3; c++) {
int diff = targets[c] - channels[c];
if (ABS(diff) >= threshold && diff != 0)
@@ -839,7 +886,7 @@ private:
if (READ_BE_UINT32(_data.data() + _motionPtr) == 0xffffffff) {
_phaseDone = 1;
- _motionPtr = 0x21b0;
+ _motionPtr = _l.motionLoop;
}
_objectFrame = (int16)READ_BE_UINT16(_data.data() + _motionPtr);
_objectX = (int16)READ_BE_UINT16(_data.data() + _motionPtr + 2);
@@ -1767,6 +1814,48 @@ bool CastleEngine::playAmigaIntro() {
return played;
}
+bool CastleEngine::playAtariIntro() {
+ // The Atari ST intro is the separate J.PRG program (Huffman-packed, not
+ // Copylock-protected). Decompress it to obtain the intro data, which uses
+ // the same engine as the Amiga intro but at the offsets in kAtariIntroLayout.
+ Common::File probe;
+ if (!probe.open("J.PRG"))
+ return false; // No intro program provided; skip.
+ probe.close();
+
+ Common::SeekableReadStream *stream = decompressAtari("J.PRG");
+ int size = stream->size();
+ if (size <= 0x1c) {
+ delete stream;
+ return false;
+ }
+ stream->seek(0);
+ uint16 magic = stream->readUint16BE();
+ uint32 textSize = stream->readUint32BE();
+ if (magic != 0x601a || textSize == 0 || textSize + 0x1c > (uint32)size) {
+ delete stream;
+ return false;
+ }
+ Common::Array<byte> introText;
+ introText.resize(textSize);
+ stream->seek(0x1c);
+ stream->read(introText.data(), textSize);
+ delete stream;
+
+ // TODO(castle-atari): locate and play the intro music (the Amiga build uses
+ // a separate "musicdat" ProTracker module; the Atari equivalent still needs
+ // to be found). The intro currently plays silently.
+
+ bool selectedPrincess = false;
+ CastleAmigaIntroPlayer player(this, introText, kAtariIntroLayout);
+ bool played = player.run(selectedPrincess);
+ if (played)
+ _selectedPrincess = selectedPrincess;
+
+ _gfx->clear(0, 0, 0, true);
+ return played;
+}
+
void CastleEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
drawLiftingGate(surface);
drawDroppingGate(surface);
diff --git a/engines/freescape/games/castle/atari.cpp b/engines/freescape/games/castle/atari.cpp
new file mode 100644
index 00000000000..8b04b6a186e
--- /dev/null
+++ b/engines/freescape/games/castle/atari.cpp
@@ -0,0 +1,358 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+#include "common/file.h"
+#include "common/memstream.h"
+#include "common/endian.h"
+
+#include "freescape/freescape.h"
+#include "freescape/games/castle/castle.h"
+#include "freescape/language/8bitDetokeniser.h"
+
+namespace Freescape {
+
+// RLE output stage of the Castle Master (Atari ST) packer. The Huffman tree
+// produces a byte stream that is run-length encoded as follows (both counters
+// start at -1): when idle the next byte is a *control* byte; values < 0x80
+// repeat the following single byte (value + 1) times, values >= 0x80 copy the
+// following (value & 0x7F) + 1 bytes literally.
+static void emitByteAtari(Common::MemoryWriteStreamDynamic &out, int &rep, int &lit, byte b) {
+ if (rep >= 0) {
+ for (int i = 0; i <= rep; i++)
+ out.writeByte(b);
+ rep = -1;
+ } else if (lit >= 0) {
+ out.writeByte(b);
+ lit--;
+ } else if (b < 0x80) {
+ rep = b;
+ } else {
+ lit = b & 0x7F;
+ }
+}
+
+// Decompress a Castle Master (Atari ST) self-extracting GEMDOS executable.
+//
+// The player is expected to provide the Copylock-decrypted file (named
+// "M.PRG"): a GEMDOS executable (magic 0x601A) whose DATA segment holds a
+// Huffman-tree + RLE packed stream that, when expanded, yields the actual
+// Castle Master game executable (also a GEMDOS PRG).
+//
+// Packed stream layout (at the start of the DATA segment, i.e. file offset
+// 0x1C + TEXT size):
+// u32 count number of 32-bit words in the bitstream
+// u16 nodeTblSize size of the Huffman node table, in bytes
+// ... nodeTable 4-byte nodes (left s16 BE @ +0, right s16 BE @ +2)
+// ... bitstream count*4 bytes, consumed MSB-first as big-endian u32s
+//
+// Walking the tree from the root, each bit selects the left (0) / right (1)
+// child word `v`: 0 <= v <= 0x201 is an internal node (continue at node v);
+// otherwise it is a leaf whose high byte (unless 0xFF) and low byte are fed to
+// the RLE stage, after which the walk resets to the root.
+Common::SeekableReadStream *CastleEngine::decompressAtari(const Common::Path &filename) {
+ Common::File file;
+ if (!file.open(filename))
+ error("Failed to open '%s'", filename.toString().c_str());
+
+ int fileSize = file.size();
+ byte *buffer = (byte *)malloc(fileSize);
+ file.read(buffer, fileSize);
+ file.close();
+
+ if (READ_BE_UINT16(buffer) != 0x601a) {
+ free(buffer);
+ error("'%s' is not a GEMDOS executable (expected Copylock-decrypted M.PRG)", filename.toString().c_str());
+ }
+
+ uint32 textSize = READ_BE_UINT32(buffer + 2);
+ uint32 packedOffset = 0x1c + textSize; // start of the DATA segment
+
+ uint32 count = READ_BE_UINT32(buffer + packedOffset);
+ uint16 nodeTableSize = READ_BE_UINT16(buffer + packedOffset + 4);
+ const byte *nodes = buffer + packedOffset + 6;
+ const byte *bitstream = nodes + nodeTableSize;
+
+ Common::MemoryWriteStreamDynamic out(DisposeAfterUse::NO);
+ int rep = -1;
+ int lit = -1;
+ uint32 node = 0; // byte offset of the current node within the node table
+ uint32 pos = 0;
+
+ for (uint32 w = 0; w < count; w++) {
+ uint32 word = READ_BE_UINT32(bitstream + pos);
+ pos += 4;
+ for (int bit = 0; bit < 32; bit++) {
+ uint32 msb = word >> 31;
+ word = (word << 1) & 0xffffffff;
+ uint32 nodeOffset = msb ? node + 2 : node;
+ uint16 value = READ_BE_UINT16(nodes + nodeOffset);
+ if (value < 0x8000 && value <= 0x201) {
+ node = value * 4; // internal node
+ } else {
+ uint8 hi = (value >> 8) & 0xff;
+ if (hi != 0xff)
+ emitByteAtari(out, rep, lit, hi);
+ emitByteAtari(out, rep, lit, value & 0xff);
+ node = 0; // leaf -> back to the root
+ }
+ }
+ }
+
+ free(buffer);
+
+ if (out.size() < 2 || READ_BE_UINT16(out.getData()) != 0x601a)
+ error("Castle Master (Atari ST) decompression failed (no 0x601A header)");
+
+ debugC(1, kFreescapeDebugParser, "Castle Master (Atari ST): decompressed %d bytes", (int)out.size());
+ return new Common::MemoryReadStream(out.getData(), out.size(), DisposeAfterUse::YES);
+}
+
+extern byte kAmigaCastlePalette[16][3];
+extern byte kAmigaCastleRiddlePalette[16][3];
+
+void CastleEngine::loadAssetsAtariFullGame() {
+ // The player provides the Copylock-decrypted executable as "M.PRG"; it is
+ // still Huffman-packed, so decompress it to obtain the real game binary.
+ // The Atari ST build shares the Amiga data *format* (68000, big-endian) but
+ // lays everything out at different offsets. These offsets were located by
+ // matching the shared world/asset bytes against the Amiga "x" file.
+ Common::SeekableReadStream *file = decompressAtari("M.PRG");
+
+ _viewArea = Common::Rect(40, 29, 280, 154);
+ loadMessagesVariableSize(file, 0x27946, 178);
+ loadRiddles(file, 0x28410, 19);
+
+ // Font: 90 characters, 8x8, 4 interleaved bitplanes (identical bytes to the
+ // Amiga build, so the Amiga 16-colour palette applies).
+ file->seek(0x2f32a);
+ Common::Array<Graphics::ManagedSurface *> chars;
+ Common::Array<Graphics::ManagedSurface *> charsRiddle;
+ for (int i = 0; i < 90; i++) {
+ Graphics::ManagedSurface *img = loadFrameFromPlanes(file, 8, 8);
+ Graphics::ManagedSurface *imgRiddle = new Graphics::ManagedSurface();
+ imgRiddle->copyFrom(*img);
+
+ chars.push_back(img);
+ chars[i]->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+
+ charsRiddle.push_back(imgRiddle);
+ charsRiddle[i]->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
+ }
+ _font = Font(chars);
+ _font.setCharWidth(9);
+ _fontRiddle = Font(charsRiddle);
+ _fontRiddle.setCharWidth(9);
+
+ // Area database: 87 rooms followed by 3 trailing areas, then the global
+ // area 255.
+ load8bitBinary(file, 0x33694, 16);
+ for (int i = 0; i < 3; i++) {
+ Area *newArea = load8bitArea(file, 16);
+ if (newArea) {
+ if (!_areaMap.contains(newArea->getAreaID()))
+ _areaMap[newArea->getAreaID()] = newArea;
+ else
+ error("Repeated area ID: %d", newArea->getAreaID());
+ } else
+ error("Invalid area %d?", i);
+ }
+
+ loadPalettes(file, 0x32594);
+
+ // COLOR15 cycling table, terminated by 0xFFFF.
+ file->seek(0x27928);
+ while (true) {
+ uint16 val = file->readUint16BE();
+ if (val == 0xFFFF)
+ break;
+ _gfx->_colorCyclingTable.push_back(val);
+ }
+
+ file->seek(0x49284); // Global area 255
+ _areaMap[255] = load8bitArea(file, 16);
+
+ // In-game border frame (the "Castle Master" title + castle walls + bottom
+ // UI bar surrounding the 3D viewport). 320x200, stored as Atari ST
+ // word-interleaved bitplanes; identical artwork to the Amiga build (which
+ // keeps it in vertical-planar form), so the Amiga palette applies.
+ file->seek(0x4a364);
+ _border = loadFrameFromPlanesInterleaved(file, 20, 200);
+ _border->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+
+ // Mountains panorama (63 words x 22 rows, interleaved) - same bytes/format
+ // as the Amiga build.
+ file->seek(0x4f24);
+ _background = loadFrameFromPlanesInterleaved(file, 63, 22);
+ _background->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+
+ // Spirit meter, strength-weight and key/eye sprites (shared with the Amiga
+ // build, relocated in the Atari binary).
+ file->seek(0x55d40);
+ _spiritsMeterIndicatorBackgroundFrame = loadFrameFromPlanesInterleaved(file, 5, 10);
+ _spiritsMeterIndicatorBackgroundFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+
+ file->seek(0x55ed0);
+ _spiritsMeterIndicatorFrame = loadFrameFromPlanesInterleaved(file, 1, 10);
+ _spiritsMeterIndicatorFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+
+ file->seek(0x569e0);
+ for (int i = 0; i < 4; i++) {
+ Graphics::ManagedSurface *frame = loadFrameFromPlanesInterleaved(file, 1, 14);
+ frame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+ _strenghtWeightsFrames.push_back(frame);
+ }
+
+ file->seek(0x594a0);
+ 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: 5 frames x 2 words x 11 rows.
+ file->seek(0x5974a);
+ 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: a 16-word transparency mask followed by the top/background/
+ // bottom frames, masked and drawn with the riddle palette.
+ file->seek(0x59ae4);
+ uint16 riddleMask[16];
+ for (int i = 0; i < 16; i++)
+ riddleMask[i] = file->readUint16BE();
+
+ file->seek(0x59b04);
+ _riddleTopFrame = loadFrameFromPlanesInterleaved(file, 16, 20);
+ _riddleBackgroundFrame = loadFrameFromPlanesInterleaved(file, 16, 1);
+ _riddleBottomFrame = loadFrameFromPlanesInterleaved(file, 16, 8);
+
+ Graphics::ManagedSurface *riddleFrames[] = {_riddleTopFrame, _riddleBackgroundFrame, _riddleBottomFrame};
+ for (int f = 0; f < 3; f++) {
+ Graphics::ManagedSurface *frame = riddleFrames[f];
+ for (int y = 0; y < frame->h; y++) {
+ for (int x = 0; x < frame->w; x++) {
+ int col = x / 16;
+ int bit = 15 - (x % 16);
+ if (!(riddleMask[col] & (1 << bit)))
+ frame->setPixel(x, y, 0);
+ }
+ }
+ }
+ _riddleTopFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
+ _riddleBackgroundFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
+ _riddleBottomFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
+
+ // Castle gate that lifts up at the start of the game: 256x120, built from a
+ // 3-bitplane + 1-bit-mask source (24 top tile rows repeated + 19 bottom
+ // rows). Identical source bytes to the Amiga build.
+ {
+ 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;
+ static const int kMaskBytesPerRow = kColumnsPerRow * 2;
+ static const int kGateWidth = 256;
+ static const int kGateHeight = 120;
+
+ byte pixelData[kTotalSrcRows * kPixelBytesPerRow];
+ byte maskData[kTotalSrcRows * kMaskBytesPerRow];
+ file->seek(0x56ed0);
+ file->read(pixelData, sizeof(pixelData));
+ file->seek(0x57ef0);
+ 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);
+
+ int destRow = 0;
+ for (int r = kTopRows - 5; r < kTopRows; r++) {
+ for (int col = 0; col < kColumnsPerRow; col++) {
+ uint16 mask = READ_BE_UINT16(&maskData[r * kMaskBytesPerRow + col * 2]);
+ int pOff = r * 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++;
+ }
+ for (int block = 0; block < 4; block++) {
+ for (int r = 0; r < kTopRows; r++) {
+ for (int col = 0; col < kColumnsPerRow; col++) {
+ uint16 mask = READ_BE_UINT16(&maskData[r * kMaskBytesPerRow + col * 2]);
+ int pOff = r * 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++;
+ }
+ }
+ 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++;
+ }
+ }
+
+ // TODO(castle-atari): the info menu, menu buttons and movement/sound
+ // indicators (drawInfoMenu) use Atari-specific artwork that does not match
+ // the Amiga bytes in any plane format, so they are not loaded yet; the info
+ // menu is guarded against the missing surfaces. The mouse cursor / crosshair
+ // sprites also still need to be located.
+
+ delete file;
+}
+
+} // End of namespace Freescape
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 38abf132082..1ff4a17d609 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -618,8 +618,9 @@ void CastleEngine::gotoArea(uint16 areaID, int entranceID) {
_gfx->_colorPair[_currentArea->_usualBackgroundColor] = _currentArea->_extraColor[0];
_gfx->_colorPair[_currentArea->_paperColor] = _currentArea->_extraColor[2];
_gfx->_colorPair[_currentArea->_inkColor] = _currentArea->_extraColor[3];
- } else if (isAmiga()) {
- // Unclear why these colors are always overwritten
+ } else if (isAmiga() || isAtariST()) {
+ // Unclear why these colors are always overwritten (the Atari ST build
+ // shares the Amiga rendering and needs the same 3D-world greys).
byte (*palette)[16][3] = (byte (*)[16][3])_gfx->_palette;
(*palette)[1][0] = 0x44;
@@ -1534,7 +1535,7 @@ void CastleEngine::loadAssets() {
_outOfReachMessage = _messagesList[7];
_noEffectMessage = _messagesList[8];
- if (!isAmiga() && !isCPC()) {
+ if (!isAmiga() && !isAtariST() && !isCPC()) {
Graphics::Surface *tmp;
tmp = loadBundledImage("castle_gate", !isDOS());
_gameOverBackgroundFrame = new Graphics::ManagedSurface;
@@ -2049,6 +2050,10 @@ void CastleEngine::updateTimeVariables() {
void CastleEngine::borderScreen() {
if (isAmiga() && isDemo())
return; // Skip character selection
+ if (isAtariST()) {
+ playAtariIntro();
+ return;
+ }
if (isAmiga()) {
if (playAmigaIntro())
return;
@@ -2291,7 +2296,7 @@ void CastleEngine::drawLiftingGate(Graphics::Surface *surface) {
else if (isCPC())
duration = 100;
- if ((_gameStateControl == kFreescapeGameStateStart || _gameStateControl == kFreescapeGameStateRestart) && _ticks <= duration) { // Draw the _gameOverBackgroundFrame gate lifting up slowly
+ if (_gameOverBackgroundFrame && (_gameStateControl == kFreescapeGameStateStart || _gameStateControl == kFreescapeGameStateRestart) && _ticks <= duration) { // Draw the _gameOverBackgroundFrame gate lifting up slowly
int gate_w = _gameOverBackgroundFrame->w;
int gate_h = _gameOverBackgroundFrame->h;
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index 9106fc7f739..64a5a097113 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -67,11 +67,13 @@ public:
void loadAssetsDOSDemo() override;
void loadAssetsAmigaDemo() override;
void loadAssetsAmigaFullGame() override;
+ void loadAssetsAtariFullGame() override;
void loadAssetsZXFullGame() override;
void loadAssetsCPCFullGame() override;
void borderScreen() override;
void selectCharacterScreen();
bool playAmigaIntro();
+ bool playAtariIntro();
void drawOption();
void initZX();
@@ -186,6 +188,7 @@ public:
private:
Common::SeekableReadStream *decryptFile(const Common::Path &filename);
+ Common::SeekableReadStream *decompressAtari(const Common::Path &filename);
void loadRiddles(Common::SeekableReadStream *file, int offset, int number);
void loadDOSFonts(Common::SeekableReadStream *file, int pos);
void drawFullscreenRiddleAndWait(uint16 riddle);
diff --git a/engines/freescape/loaders/8bitBinaryLoader.cpp b/engines/freescape/loaders/8bitBinaryLoader.cpp
index 838e05deec8..b6849a87146 100644
--- a/engines/freescape/loaders/8bitBinaryLoader.cpp
+++ b/engines/freescape/loaders/8bitBinaryLoader.cpp
@@ -749,7 +749,7 @@ Area *FreescapeEngine::load8bitArea(Common::SeekableReadStream *file, uint16 nco
}
} else if (isCastle()) {
byte idx = readField(file, 8);
- if (isAmiga())
+ if (isAmiga() || isAtariST())
name = _messagesList[idx + 51];
else if (isSpectrum() || isCPC() || isC64())
name = areaNumber == 255 ? "GLOBAL" : _messagesList[idx + (isCastleMaster2() ? 41 : 16)];
@@ -764,7 +764,7 @@ Area *FreescapeEngine::load8bitArea(Common::SeekableReadStream *file, uint16 nco
debugC(1, kFreescapeDebugParser, "Extra colors: %x %x %x %x", extraColor[0], extraColor[1], extraColor[2], extraColor[3]);
}
- if (isAmiga()) {
+ if (isAmiga() || isAtariST()) {
extraColor[0] = readField(file, 8);
extraColor[1] = readField(file, 8);
extraColor[2] = readField(file, 8);
@@ -864,10 +864,10 @@ Area *FreescapeEngine::load8bitArea(Common::SeekableReadStream *file, uint16 nco
void FreescapeEngine::load8bitBinary(Common::SeekableReadStream *file, int offset, int ncolors) {
file->seek(offset);
uint8 numberOfAreas = readField(file, 8);
- // The Castle Master Amiga binary stores the count as 0x68 (104) but the
- // area pointer table only has 87 valid entries; the demo and the full
- // game share the same asset section so the same override applies.
- if (isAmiga() && isCastle())
+ // The Castle Master Amiga/Atari ST binaries store the count as 0x68 (104)
+ // but the area pointer table only has 87 valid entries; the demo and the
+ // full game share the same asset section so the same override applies.
+ if ((isAmiga() || isAtariST()) && isCastle())
numberOfAreas = 87;
debugC(1, kFreescapeDebugParser, "Number of areas: %d", numberOfAreas);
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index 363ddc47f30..89319af53cd 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -11,6 +11,7 @@ MODULE_OBJS := \
freescape.o \
games/castle/castle.o \
games/castle/amiga.o \
+ games/castle/atari.o \
games/castle/c64.o \
games/castle/cpc.o \
games/castle/dos.o \
Commit: 424a64b3990e7269294958b03f1c15e271487062
https://github.com/scummvm/scummvm/commit/424a64b3990e7269294958b03f1c15e271487062
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-25T14:08:38+02:00
Commit Message:
FREESCAPE: added complementary occlusion filtering to the renderer
Changed paths:
engines/freescape/area.cpp
engines/freescape/area.h
engines/freescape/freescape.cpp
engines/freescape/ui.cpp
diff --git a/engines/freescape/area.cpp b/engines/freescape/area.cpp
index 35423f59446..8f160055107 100644
--- a/engines/freescape/area.cpp
+++ b/engines/freescape/area.cpp
@@ -87,6 +87,14 @@ Area::Area(uint16 areaID_, uint16 areaFlags_, ObjectMap *objectsByID_, ObjectMap
_lastTick = 0;
_lastDepthLayerTick = 0;
+ _lastFov = 0.0f;
+ _lastAspectRatio = 0.0f;
+ _lastNearClipPlane = 0.0f;
+ _lastFarClipPlane = 0.0f;
+ _lastDepthLayerFov = 0.0f;
+ _lastDepthLayerAspectRatio = 0.0f;
+ _lastDepthLayerNearClipPlane = 0.0f;
+ _lastDepthLayerFarClipPlane = 0.0f;
_lastRenderDepthLayer = kRenderDepthAll;
_lastForegroundDistance = 0.0f;
}
@@ -227,6 +235,78 @@ void Area::resetArea() {
}
+static float aabbMaxProjection(const Math::AABB &aabb, const Math::Vector3d &axis) {
+ const Math::Vector3d min = aabb.getMin();
+ const Math::Vector3d max = aabb.getMax();
+ Math::Vector3d support(
+ axis.x() >= 0.0f ? max.x() : min.x(),
+ axis.y() >= 0.0f ? max.y() : min.y(),
+ axis.z() >= 0.0f ? max.z() : min.z());
+
+ return support.dotProduct(axis);
+}
+
+static float aabbMinProjection(const Math::AABB &aabb, const Math::Vector3d &axis) {
+ const Math::Vector3d min = aabb.getMin();
+ const Math::Vector3d max = aabb.getMax();
+ Math::Vector3d support(
+ axis.x() >= 0.0f ? min.x() : max.x(),
+ axis.y() >= 0.0f ? min.y() : max.y(),
+ axis.z() >= 0.0f ? min.z() : max.z());
+
+ return support.dotProduct(axis);
+}
+
+static bool aabbIntersectsHalfSpace(const Math::AABB &aabb, const Math::Vector3d &camera, const Math::Vector3d &normal, float padding) {
+ return aabbMaxProjection(aabb, normal) - camera.dotProduct(normal) >= -padding;
+}
+
+static bool aabbIntersectsViewVolume(const Math::AABB &aabb, const Math::Vector3d &camera, const Math::Vector3d &direction, float fov, float aspectRatio, float nearClipPlane, float farClipPlane) {
+ if (!aabb.isValid())
+ return false;
+
+ Math::Vector3d front = direction.getNormalized();
+ if (front.getSquareMagnitude() == 0.0f)
+ return true;
+
+ const Math::Vector3d worldUp(0.0f, 1.0f, 0.0f);
+ Math::Vector3d right = Math::Vector3d::crossProduct(front, worldUp);
+ if (right.getSquareMagnitude() < 0.0001f)
+ right = Math::Vector3d(1.0f, 0.0f, 0.0f);
+ else
+ right.normalize();
+ Math::Vector3d up = Math::Vector3d::crossProduct(right, front).getNormalized();
+
+ const float padding = 32.0f;
+ const float horizontalScale = tan(Math::deg2rad(fov) / 2.0f) * 1.25f;
+ const float verticalScale = MAX(horizontalScale / MAX(aspectRatio, 0.001f), horizontalScale);
+ const float minDepth = aabbMinProjection(aabb, front) - camera.dotProduct(front);
+ const float maxDepth = aabbMaxProjection(aabb, front) - camera.dotProduct(front);
+
+ if (maxDepth < nearClipPlane - padding)
+ return false;
+ if (minDepth > farClipPlane + padding)
+ return false;
+
+ if (!aabbIntersectsHalfSpace(aabb, camera, front * horizontalScale + right, padding))
+ return false;
+ if (!aabbIntersectsHalfSpace(aabb, camera, front * horizontalScale - right, padding))
+ return false;
+ if (!aabbIntersectsHalfSpace(aabb, camera, front * verticalScale + up, padding))
+ return false;
+ if (!aabbIntersectsHalfSpace(aabb, camera, front * verticalScale - up, padding))
+ return false;
+
+ return true;
+}
+
+static bool objectIsSortCandidate(Object *obj, const Math::Vector3d &camera, const Math::Vector3d &direction, float fov, float aspectRatio, float nearClipPlane, float farClipPlane) {
+ if (!obj || obj->isDestroyed() || obj->isInvisible() || !obj->isGeometric())
+ return false;
+
+ return aabbIntersectsViewVolume(obj->_occlusionBox, camera, direction, fov, aspectRatio, nearClipPlane, farClipPlane);
+}
+
static float aabbNearestDepth(const Math::AABB &aabb, const Math::Vector3d &camera, const Math::Vector3d &direction) {
const Math::Vector3d min = aabb.getMin();
const Math::Vector3d max = aabb.getMax();
@@ -280,10 +360,12 @@ static bool objectInDepthLayer(Object *obj, const Math::Vector3d &camera, const
return depthLayer == Area::kRenderDepthForeground ? foreground : !foreground;
}
-void Area::draw(Freescape::Renderer *gfx, uint32 animationTicks, Math::Vector3d camera, Math::Vector3d direction, bool insideWait) {
+void Area::draw(Freescape::Renderer *gfx, uint32 animationTicks, Math::Vector3d camera, Math::Vector3d direction, bool insideWait, float fov, float aspectRatio, float nearClipPlane, float farClipPlane) {
bool runAnimation = animationTicks != _lastTick;
bool cameraChanged = camera != _lastCameraPosition;
- bool sort = runAnimation || cameraChanged || _sortedObjects.empty();
+ bool directionChanged = direction != _lastCameraDirection;
+ bool projectionChanged = fov != _lastFov || aspectRatio != _lastAspectRatio || nearClipPlane != _lastNearClipPlane || farClipPlane != _lastFarClipPlane;
+ bool sort = runAnimation || cameraChanged || directionChanged || projectionChanged || _sortedObjects.empty();
assert(_drawableObjects.size() > 0);
if (sort)
@@ -316,7 +398,7 @@ void Area::draw(Freescape::Renderer *gfx, uint32 animationTicks, Math::Vector3d
continue;
}
- if (sort)
+ if (sort && objectIsSortCandidate(obj, camera, direction, fov, aspectRatio, nearClipPlane, farClipPlane))
_sortedObjects.push_back(obj);
}
}
@@ -449,15 +531,23 @@ void Area::draw(Freescape::Renderer *gfx, uint32 animationTicks, Math::Vector3d
gfx->drawAABB(obj->_occlusionBox, 255, 0, 0);
}
_lastTick = animationTicks;
- if (sort)
+ if (sort) {
_lastCameraPosition = camera;
+ _lastCameraDirection = direction;
+ _lastFov = fov;
+ _lastAspectRatio = aspectRatio;
+ _lastNearClipPlane = nearClipPlane;
+ _lastFarClipPlane = farClipPlane;
+ }
}
-void Area::drawDepthLayer(Freescape::Renderer *gfx, uint32 animationTicks, Math::Vector3d camera, Math::Vector3d direction, bool insideWait, RenderDepthLayer depthLayer, float foregroundDistance) {
+void Area::drawDepthLayer(Freescape::Renderer *gfx, uint32 animationTicks, Math::Vector3d camera, Math::Vector3d direction, bool insideWait, RenderDepthLayer depthLayer, float foregroundDistance, float fov, float aspectRatio, float nearClipPlane, float farClipPlane) {
bool runAnimation = depthLayer != kRenderDepthBackground && animationTicks != _lastDepthLayerTick;
bool cameraChanged = camera != _lastDepthLayerCameraPosition;
+ bool directionChanged = direction != _lastDepthLayerCameraDirection;
+ bool projectionChanged = fov != _lastDepthLayerFov || aspectRatio != _lastDepthLayerAspectRatio || nearClipPlane != _lastDepthLayerNearClipPlane || farClipPlane != _lastDepthLayerFarClipPlane;
bool layerChanged = depthLayer != _lastRenderDepthLayer || (depthLayer != kRenderDepthAll && ABS(foregroundDistance - _lastForegroundDistance) > 0.001f);
- bool sort = runAnimation || cameraChanged || layerChanged || _depthLayerSortedObjects.empty();
+ bool sort = runAnimation || cameraChanged || directionChanged || projectionChanged || layerChanged || _depthLayerSortedObjects.empty();
Math::Vector3d normalizedDirection = direction.getNormalized();
assert(_drawableObjects.size() > 0);
@@ -493,7 +583,9 @@ void Area::drawDepthLayer(Freescape::Renderer *gfx, uint32 animationTicks, Math:
continue;
}
- if (sort && objectInDepthLayer(obj, camera, normalizedDirection, depthLayer, foregroundDistance))
+ if (sort &&
+ objectInDepthLayer(obj, camera, normalizedDirection, depthLayer, foregroundDistance) &&
+ objectIsSortCandidate(obj, camera, direction, fov, aspectRatio, nearClipPlane, farClipPlane))
_depthLayerSortedObjects.push_back(obj);
}
}
@@ -629,6 +721,11 @@ void Area::drawDepthLayer(Freescape::Renderer *gfx, uint32 animationTicks, Math:
_lastDepthLayerTick = animationTicks;
if (sort) {
_lastDepthLayerCameraPosition = camera;
+ _lastDepthLayerCameraDirection = direction;
+ _lastDepthLayerFov = fov;
+ _lastDepthLayerAspectRatio = aspectRatio;
+ _lastDepthLayerNearClipPlane = nearClipPlane;
+ _lastDepthLayerFarClipPlane = farClipPlane;
_lastRenderDepthLayer = depthLayer;
_lastForegroundDistance = foregroundDistance;
}
diff --git a/engines/freescape/area.h b/engines/freescape/area.h
index 51435e34b54..4c6bb1ed9e0 100644
--- a/engines/freescape/area.h
+++ b/engines/freescape/area.h
@@ -64,8 +64,8 @@ public:
uint8 getScale();
void remapColor(int index, int color);
void unremapColor(int index);
- void draw(Renderer *gfx, uint32 animationTicks, Math::Vector3d camera, Math::Vector3d direction, bool insideWait);
- void drawDepthLayer(Renderer *gfx, uint32 animationTicks, Math::Vector3d camera, Math::Vector3d direction, bool insideWait, RenderDepthLayer depthLayer, float foregroundDistance);
+ void draw(Renderer *gfx, uint32 animationTicks, Math::Vector3d camera, Math::Vector3d direction, bool insideWait, float fov, float aspectRatio, float nearClipPlane, float farClipPlane);
+ void drawDepthLayer(Renderer *gfx, uint32 animationTicks, Math::Vector3d camera, Math::Vector3d direction, bool insideWait, RenderDepthLayer depthLayer, float foregroundDistance, float fov, float aspectRatio, float nearClipPlane, float farClipPlane);
void drawGroup(Renderer *gfx, Group *group, bool runAnimation);
void show();
@@ -115,9 +115,19 @@ public:
private:
Math::Vector3d _lastCameraPosition;
+ Math::Vector3d _lastCameraDirection;
+ float _lastFov;
+ float _lastAspectRatio;
+ float _lastNearClipPlane;
+ float _lastFarClipPlane;
ObjectArray _sortedObjects;
ObjectArray _depthLayerSortedObjects;
Math::Vector3d _lastDepthLayerCameraPosition;
+ Math::Vector3d _lastDepthLayerCameraDirection;
+ float _lastDepthLayerFov;
+ float _lastDepthLayerAspectRatio;
+ float _lastDepthLayerNearClipPlane;
+ float _lastDepthLayerFarClipPlane;
RenderDepthLayer _lastRenderDepthLayer;
float _lastForegroundDistance;
uint32 _lastDepthLayerTick;
diff --git a/engines/freescape/freescape.cpp b/engines/freescape/freescape.cpp
index 1e5a53ac115..7273128bedf 100644
--- a/engines/freescape/freescape.cpp
+++ b/engines/freescape/freescape.cpp
@@ -646,8 +646,9 @@ void FreescapeEngine::drawFrame() {
return;
}
+ const float fov = 75.0f;
float aspectRatio = isCastle() ? 1.6 : 2.18;
- _gfx->updateProjectionMatrix(75.0, aspectRatio, _nearClipPlane, farClipPlane);
+ _gfx->updateProjectionMatrix(fov, aspectRatio, _nearClipPlane, farClipPlane);
_gfx->positionCamera(_position, _position + _cameraFront, _roll);
if (_underFireFrames > 0) {
@@ -668,7 +669,7 @@ void FreescapeEngine::drawFrame() {
drawBackground();
if (_avoidRenderingFrames == 0) { // Avoid rendering inside objects
- _currentArea->draw(_gfx, _ticks / 10, _position, _cameraFront, false);
+ _currentArea->draw(_gfx, _ticks / 10, _position, _cameraFront, false, fov, aspectRatio, _nearClipPlane, farClipPlane);
if (_gameStateControl == kFreescapeGameStatePlaying &&
_currentArea->hasActiveGroups() && _ticks % 50 == 0) {
executeMovementConditions();
@@ -739,7 +740,7 @@ void FreescapeEngine::drawFrameStereo(int farClipPlane) {
drawBackground();
if (_avoidRenderingFrames == 0)
- _currentArea->drawDepthLayer(_gfx, _ticks / 10, _position, _cameraFront, false, Area::kRenderDepthBackground, stereoForegroundDistance);
+ _currentArea->drawDepthLayer(_gfx, _ticks / 10, _position, _cameraFront, false, Area::kRenderDepthBackground, stereoForegroundDistance, fov, aspectRatio, _nearClipPlane, farClipPlane);
for (int pass = 0; pass < 2; pass++) {
_gfx->setStereoEye(pass == 0 ? Renderer::kStereoEyeLeft : Renderer::kStereoEyeRight);
@@ -749,7 +750,7 @@ void FreescapeEngine::drawFrameStereo(int farClipPlane) {
_gfx->clearDepthBuffer();
if (_avoidRenderingFrames == 0) // Avoid rendering inside objects
- _currentArea->drawDepthLayer(_gfx, _ticks / 10, _position, _cameraFront, false, Area::kRenderDepthForeground, stereoForegroundDistance);
+ _currentArea->drawDepthLayer(_gfx, _ticks / 10, _position, _cameraFront, false, Area::kRenderDepthForeground, stereoForegroundDistance, fov, aspectRatio, _nearClipPlane, farClipPlane);
if (_underFireFrames > 0) {
for (auto &it : _sensors) {
diff --git a/engines/freescape/ui.cpp b/engines/freescape/ui.cpp
index 30b948573d4..283ea1512be 100644
--- a/engines/freescape/ui.cpp
+++ b/engines/freescape/ui.cpp
@@ -86,12 +86,13 @@ void FreescapeEngine::waitInLoop(int maxWait) {
if (_currentArea->isOutside())
farClipPlane *= 100;
+ const float fov = 75.0f;
float aspectRatio = isCastle() ? 1.6 : 2.18;
- _gfx->updateProjectionMatrix(75.0, aspectRatio, _nearClipPlane, farClipPlane);
+ _gfx->updateProjectionMatrix(fov, aspectRatio, _nearClipPlane, farClipPlane);
_gfx->positionCamera(_position, _position + _cameraFront, _roll);
drawBackground();
- _currentArea->draw(_gfx, _ticks / 10, _position, _cameraFront, true);
+ _currentArea->draw(_gfx, _ticks / 10, _position, _cameraFront, true, fov, aspectRatio, _nearClipPlane, farClipPlane);
drawBorder();
drawUI();
Commit: 0612df8cdfc3dc25150b9e0be0242ec10c80b24d
https://github.com/scummvm/scummvm/commit/0612df8cdfc3dc25150b9e0be0242ec10c80b24d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-25T14:08:38+02:00
Commit Message:
FREESCAPE: small fixes to the depth sorting
Changed paths:
engines/freescape/area.cpp
diff --git a/engines/freescape/area.cpp b/engines/freescape/area.cpp
index 8f160055107..09de952404a 100644
--- a/engines/freescape/area.cpp
+++ b/engines/freescape/area.cpp
@@ -414,7 +414,7 @@ void Area::draw(Freescape::Renderer *gfx, uint32 animationTicks, Math::Vector3d
bool signMinB = minB >= 0;
bool signMaxB = maxB >= 0;
if (minA >= maxB - 0.5f) { // A is clearly "greater" than B (L9c9b_one_object_clearly_further_than_the_other)
- if (signMinA != signMaxB) // A covers 0 (L9ce6_first_object_is_closer)
+ if (signMinA != signMaxA) // A covers 0 (L9ce6_first_object_is_closer)
return 1; // A is closer
if (signMinB != signMaxB) // B covers 0 (L9cec_second_object_is_closer)
return 2; // B is closer
@@ -601,7 +601,7 @@ void Area::drawDepthLayer(Freescape::Renderer *gfx, uint32 animationTicks, Math:
bool signMinB = minB >= 0;
bool signMaxB = maxB >= 0;
if (minA >= maxB - 0.5f) { // A is clearly "greater" than B (L9c9b_one_object_clearly_further_than_the_other)
- if (signMinA != signMaxB) // A covers 0 (L9ce6_first_object_is_closer)
+ if (signMinA != signMaxA) // A covers 0 (L9ce6_first_object_is_closer)
return 1; // A is closer
if (signMinB != signMaxB) // B covers 0 (L9cec_second_object_is_closer)
return 2; // B is closer
Commit: 1f06e4f6e2e76da3077ef9060426dbbf5ac50e42
https://github.com/scummvm/scummvm/commit/1f06e4f6e2e76da3077ef9060426dbbf5ac50e42
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-25T14:08:38+02:00
Commit Message:
FREESCAPE: optional adlib rendition of castle music (c64)
Changed paths:
A engines/freescape/games/castle/castle.musicdata.h
A engines/freescape/games/castle/opl.music.cpp
A engines/freescape/games/castle/opl.music.h
engines/freescape/detection.cpp
engines/freescape/games/castle/castle.cpp
engines/freescape/games/castle/castle.h
engines/freescape/games/castle/dos.cpp
engines/freescape/module.mk
diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index f4247e5ff73..c0bd24e6f28 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -851,7 +851,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformDOS,
ADGF_DEMO,
- GUIO5(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_WASD_CONTROLS)
+ GUIO6(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_WASD_CONTROLS, GAMEOPTION_OPL_MUSIC)
},
{
"castlemaster",
@@ -966,7 +966,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::UNK_LANG, // Multi-language
Common::kPlatformDOS,
ADGF_NO_FLAGS,
- GUIO5(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_WASD_CONTROLS)
+ GUIO6(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_WASD_CONTROLS, GAMEOPTION_OPL_MUSIC)
},
{
"castlemaster",
@@ -982,7 +982,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::UNK_LANG, // Multi-language
Common::kPlatformDOS,
ADGF_NO_FLAGS,
- GUIO5(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_WASD_CONTROLS)
+ GUIO6(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_WASD_CONTROLS, GAMEOPTION_OPL_MUSIC)
},
{
"castlemaster",
@@ -1030,7 +1030,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::ES_ESP,
Common::kPlatformDOS,
ADGF_NO_FLAGS,
- GUIO5(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_WASD_CONTROLS)
+ GUIO6(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_WASD_CONTROLS, GAMEOPTION_OPL_MUSIC)
},
// Castle Master 2: The Crypt
{
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 1ff4a17d609..bcde8ece90e 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -36,6 +36,7 @@
#include "freescape/gfx.h"
#include "freescape/games/castle/castle.h"
#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/music.h"
namespace Freescape {
@@ -113,6 +114,7 @@ CastleEngine::CastleEngine(OSystem *syst, const ADGameDescription *gd) : Freesca
_menuRunIndicator = nullptr;
_menuFxOnIndicator = nullptr;
_menuFxOffIndicator = nullptr;
+ _playerMusic = nullptr;
_spiritsMeter = 32;
_spiritsToKill = 26;
@@ -260,6 +262,11 @@ CastleEngine::~CastleEngine() {
_menuFxOffIndicator->free();
delete _menuFxOffIndicator;
}
+
+ if (_playerMusic) {
+ _playerMusic->stopMusic();
+ delete _playerMusic;
+ }
}
Graphics::ManagedSurface *CastleEngine::loadFrameWithHeader(Common::SeekableReadStream *file, int pos, uint32 front, uint32 back) {
@@ -714,6 +721,9 @@ void CastleEngine::initGameState() {
_droppingGateStartTicks = 0;
_thunderFrameDuration = 0;
+
+ if (_playerMusic)
+ _playerMusic->startMusic();
}
bool CastleEngine::checkIfGameEnded() {
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index 64a5a097113..4cd85376d44 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -21,6 +21,8 @@
namespace Freescape {
+class MusicPlayer;
+
struct RiddleText {
int8 _dx;
int8 _dy;
@@ -175,6 +177,7 @@ public:
Common::String _ghostInAreaMessage;
Common::Array<byte> _modData; // Embedded ProTracker module (Amiga demo)
+ MusicPlayer *_playerMusic;
Common::Array<int> _keysCollected;
bool _useRockTravel;
int _spiritsMeter;
diff --git a/engines/freescape/games/castle/castle.musicdata.h b/engines/freescape/games/castle/castle.musicdata.h
new file mode 100644
index 00000000000..f79e4ed646b
--- /dev/null
+++ b/engines/freescape/games/castle/castle.musicdata.h
@@ -0,0 +1,157 @@
+/* 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_CASTLE_MUSICDATA_H
+#define FREESCAPE_CASTLE_MUSICDATA_H
+
+#include "common/scummsys.h"
+
+/**
+ * Shared music data for Castle Master backported music players.
+ *
+ * Song data extracted from the default subtune in the C64 PSID. The order
+ * lists are expanded from the original repeat commands, while the pattern data
+ * remains in the original note/rest/instrument command format.
+ */
+
+namespace Freescape {
+namespace CastleMusicData {
+
+static const byte kOrderTranspose = 0x80;
+static const byte kOrderEnd = 0xFF;
+
+struct InstrumentData {
+ byte arpeggio;
+ byte pulseWidth;
+ byte control;
+ byte attackDecay;
+ byte sustainRelease;
+ byte gateOffTime;
+ byte pitchStep;
+ byte vibrato;
+ byte effect;
+};
+
+static const InstrumentData kInstruments[] = {
+ { 0x00, 0x05, 0x41, 0x0A, 0x0F, 0x02, 0x7F, 0x00, 0x00 },
+ { 0x00, 0x05, 0x81, 0x25, 0x07, 0x02, 0x7F, 0x00, 0x05 },
+ { 0x00, 0x05, 0x00, 0x1B, 0x07, 0x02, 0x7F, 0x00, 0x05 },
+ { 0x00, 0x05, 0x81, 0x18, 0x07, 0x02, 0x7F, 0x00, 0x00 },
+ { 0x00, 0x08, 0x41, 0x06, 0x0A, 0x02, 0x7F, 0x20, 0x05 },
+ { 0x00, 0x06, 0x41, 0x09, 0x33, 0x02, 0x7F, 0x10, 0x00 },
+ { 0x00, 0x04, 0x41, 0x0A, 0x0F, 0x02, 0x7F, 0x18, 0x00 },
+ { 0x00, 0x08, 0x41, 0x13, 0x0F, 0x02, 0x7F, 0x00, 0x05 }
+};
+
+static const byte kOrderList0[] = {
+ 0x80, 0xFB, 0x00, 0x03, 0x03, 0x03, 0x03, 0x0A, 0x0A, 0x0D, 0x0D, 0x0D, 0x12, 0x12, 0x12, 0x12,
+ 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x1A, 0xFF
+};
+
+static const byte kOrderList1[] = {
+ 0x80, 0xFB, 0x01, 0x04, 0x06, 0x08, 0x08, 0x0B, 0x0B, 0x0E, 0x11, 0x11, 0x13, 0x13, 0x16, 0x13,
+ 0x13, 0x16, 0x18, 0x18, 0x16, 0x1A, 0x00, 0x01, 0x1B, 0xFF
+};
+
+static const byte kOrderList2[] = {
+ 0x80, 0xFB, 0x02, 0x05, 0x07, 0x09, 0x09, 0x0C, 0x0C, 0x0F, 0x10, 0x10, 0x14, 0x15, 0x17, 0x14,
+ 0x15, 0x17, 0x19, 0x19, 0x17, 0x1B, 0x01, 0x00, 0x1C, 0xFF
+};
+
+static const byte *const kChannelOrderLists[] = {
+ kOrderList0,
+ kOrderList1,
+ kOrderList2
+};
+
+static const uint16 kPatternOffsets[] = {
+ 0, 52, 104, 124, 158, 164, 170, 182, 196, 216, 238, 288,
+ 311, 334, 390, 426, 460, 512, 553, 634, 690, 724, 778, 833,
+ 873, 927, 937, 943, 949
+};
+
+static const byte kPatternData[] = {
+ 0x86, 0x1B, 0x18, 0x1E, 0x18, 0x19, 0x18, 0x1B, 0x18, 0x16, 0x18, 0x19, 0x18, 0x1D, 0x18, 0x19,
+ 0x18, 0x86, 0x1B, 0x0C, 0x27, 0x0C, 0x20, 0x0C, 0x22, 0x0C, 0x23, 0x0C, 0x1E, 0x0C, 0x20, 0x0C,
+ 0x22, 0x0C, 0x86, 0x1B, 0x0C, 0x27, 0x0C, 0x20, 0x0C, 0x22, 0x0C, 0x23, 0x0C, 0x1E, 0x0C, 0x20,
+ 0x0C, 0x22, 0x0C, 0xFF, 0x85, 0xA0, 0x0C, 0x1B, 0x18, 0x1E, 0x18, 0x19, 0x18, 0x1B, 0x18, 0x16,
+ 0x18, 0x19, 0x18, 0x1D, 0x18, 0x19, 0x18, 0x85, 0x1B, 0x0C, 0x27, 0x0C, 0x20, 0x0C, 0x22, 0x0C,
+ 0x23, 0x0C, 0x1E, 0x0C, 0x20, 0x0C, 0x22, 0x0C, 0x85, 0x1B, 0x0C, 0x27, 0x0C, 0x20, 0x0C, 0x22,
+ 0x0C, 0x23, 0x0C, 0x1E, 0x0C, 0x20, 0x0C, 0xFF, 0x80, 0xA0, 0xC0, 0x27, 0x18, 0x22, 0x18, 0x23,
+ 0x18, 0x25, 0x18, 0x27, 0x18, 0x22, 0x18, 0x20, 0x18, 0x1E, 0x18, 0xFF, 0x86, 0x0F, 0x24, 0x0F,
+ 0x0C, 0x0A, 0x24, 0x0A, 0x0C, 0x06, 0x24, 0x06, 0x0C, 0x08, 0x24, 0x08, 0x0C, 0x0F, 0x24, 0x0F,
+ 0x0C, 0x0A, 0x24, 0x0A, 0x0C, 0x06, 0x24, 0x06, 0x0C, 0x08, 0x24, 0x08, 0x0C, 0xFF, 0x86, 0x1B,
+ 0xC0, 0xA0, 0xC0, 0xFF, 0x85, 0x1B, 0xC0, 0xA0, 0xC0, 0xFF, 0x85, 0x27, 0x30, 0x2F, 0x30, 0x2C,
+ 0x30, 0x2E, 0x30, 0xA0, 0xC0, 0xFF, 0x85, 0xA0, 0x0C, 0x27, 0x30, 0x2F, 0x30, 0x2C, 0x30, 0x2E,
+ 0x30, 0xA0, 0xB4, 0xFF, 0x86, 0x27, 0x30, 0x2A, 0x30, 0x29, 0x18, 0x2A, 0x18, 0x29, 0x18, 0x27,
+ 0x0C, 0x25, 0x0C, 0x27, 0x30, 0x22, 0x90, 0xFF, 0x86, 0xA0, 0x0C, 0x27, 0x30, 0x2A, 0x30, 0x29,
+ 0x18, 0x2A, 0x18, 0x29, 0x18, 0x27, 0x0C, 0x25, 0x0C, 0x27, 0x30, 0x22, 0x84, 0xFF, 0x80, 0x0F,
+ 0x18, 0x1B, 0x0C, 0x0F, 0x0C, 0x0A, 0x18, 0x16, 0x0C, 0x0A, 0x0C, 0x06, 0x18, 0x12, 0x0C, 0x06,
+ 0x0C, 0x08, 0x18, 0x14, 0x0C, 0x08, 0x0C, 0x0F, 0x18, 0x1B, 0x0C, 0x0F, 0x0C, 0x0A, 0x18, 0x16,
+ 0x0C, 0x0A, 0x0C, 0x0B, 0x18, 0x17, 0x0C, 0x0B, 0x0C, 0x0D, 0x18, 0x19, 0x0C, 0x0D, 0x0C, 0xFF,
+ 0x85, 0x27, 0x30, 0x2E, 0x30, 0x29, 0x30, 0x2A, 0x30, 0x27, 0x48, 0x86, 0x27, 0x18, 0x29, 0x18,
+ 0x2A, 0x18, 0x2C, 0x18, 0x2E, 0x18, 0xFF, 0x85, 0x2A, 0x30, 0x31, 0x30, 0x2C, 0x30, 0x2E, 0x30,
+ 0x1B, 0x54, 0x86, 0x27, 0x18, 0x29, 0x18, 0x2A, 0x18, 0x2C, 0x18, 0x2E, 0x0C, 0xFF, 0x80, 0x0F,
+ 0x18, 0x1B, 0x0C, 0x0F, 0x0C, 0x0A, 0x0C, 0x16, 0x0C, 0x0A, 0x0C, 0x06, 0x18, 0x06, 0x0C, 0x12,
+ 0x0C, 0x06, 0x0C, 0x08, 0x0C, 0x14, 0x0C, 0x08, 0x18, 0x0F, 0x18, 0x1B, 0x0C, 0x0F, 0x0C, 0x0A,
+ 0x0C, 0x16, 0x0C, 0x0A, 0x0C, 0x06, 0x18, 0x06, 0x0C, 0x12, 0x0C, 0x06, 0x0C, 0x08, 0x0C, 0x14,
+ 0x0C, 0x0D, 0x0C, 0x19, 0x0C, 0xFF, 0x85, 0xA0, 0x18, 0x27, 0x0C, 0x25, 0x0C, 0x27, 0x30, 0x22,
+ 0x0C, 0x22, 0x0C, 0x20, 0x0C, 0x22, 0x18, 0x22, 0x0C, 0x1E, 0x18, 0x1B, 0x30, 0x1E, 0x30, 0x1D,
+ 0x18, 0x1E, 0x0C, 0x1D, 0x0C, 0x1B, 0x18, 0x19, 0x18, 0xFF, 0x85, 0xA0, 0x18, 0x2A, 0x0C, 0x29,
+ 0x0C, 0x2A, 0x30, 0x25, 0x0C, 0x25, 0x0C, 0x23, 0x0C, 0x25, 0x18, 0x25, 0x0C, 0x22, 0x18, 0x1E,
+ 0x60, 0x27, 0x18, 0x25, 0x18, 0x22, 0x0C, 0x20, 0x0C, 0x22, 0x18, 0xFF, 0x85, 0x27, 0x0C, 0x33,
+ 0x0C, 0x2C, 0x0C, 0x2E, 0x0C, 0x2F, 0x0C, 0x2A, 0x0C, 0x2C, 0x0C, 0x2E, 0x0C, 0x27, 0x0C, 0x33,
+ 0x0C, 0x2C, 0x0C, 0x2E, 0x0C, 0x2F, 0x0C, 0x2A, 0x0C, 0x2C, 0x0C, 0x2E, 0x0C, 0x27, 0x0C, 0x33,
+ 0x0C, 0x2C, 0x0C, 0x2E, 0x0C, 0x2F, 0x0C, 0x2A, 0x0C, 0x2C, 0x0C, 0x2E, 0x0C, 0x27, 0x60, 0xFF,
+ 0x80, 0xA0, 0x18, 0x27, 0x18, 0x29, 0x18, 0x2A, 0x18, 0x2C, 0x0C, 0x2A, 0x0C, 0x29, 0x0C, 0x2A,
+ 0x0C, 0x2E, 0x24, 0x2A, 0x06, 0x29, 0x06, 0x27, 0x6C, 0x86, 0x22, 0x0C, 0x25, 0x0C, 0x27, 0x0C,
+ 0x29, 0x0C, 0x2A, 0x0C, 0x29, 0x0C, 0x25, 0x0C, 0xFF, 0x84, 0x12, 0x0C, 0x23, 0x0C, 0x81, 0x1E,
+ 0x0C, 0x84, 0x12, 0x0C, 0x23, 0x0C, 0x23, 0x0C, 0x81, 0x1E, 0x18, 0x84, 0x12, 0x0C, 0x23, 0x0C,
+ 0x81, 0x12, 0x0C, 0x84, 0x12, 0x0C, 0x23, 0x06, 0x19, 0x06, 0x12, 0x0C, 0x81, 0x1E, 0x18, 0x84,
+ 0x12, 0x0C, 0x23, 0x0C, 0x81, 0x1E, 0x0C, 0x84, 0x12, 0x0C, 0x23, 0x0C, 0x23, 0x0C, 0x81, 0x1E,
+ 0x18, 0x84, 0x12, 0x0C, 0x23, 0x0C, 0x81, 0x12, 0x0C, 0x84, 0x12, 0x0C, 0x23, 0x06, 0x19, 0x06,
+ 0x12, 0x0C, 0x81, 0x1E, 0x0C, 0x1E, 0x06, 0x1E, 0x06, 0xFF, 0x80, 0x0F, 0x18, 0x1B, 0x0C, 0x0F,
+ 0x0C, 0x1B, 0x0C, 0x1B, 0x0C, 0x0F, 0x0C, 0x08, 0x18, 0x08, 0x0C, 0x14, 0x0C, 0x08, 0x0C, 0x06,
+ 0x0C, 0x12, 0x0C, 0x06, 0x18, 0x0F, 0x18, 0x0F, 0x0C, 0x0F, 0x0C, 0x1B, 0x0C, 0x1B, 0x0C, 0x0F,
+ 0x0C, 0x08, 0x18, 0x14, 0x0C, 0x14, 0x0C, 0x08, 0x0C, 0x0A, 0x0C, 0x16, 0x0C, 0x0D, 0x0C, 0x19,
+ 0x0C, 0xFF, 0x85, 0x27, 0x48, 0x2E, 0x18, 0x2C, 0x18, 0x2A, 0x18, 0x29, 0x0C, 0x2A, 0x0C, 0x2C,
+ 0x18, 0x27, 0x24, 0x25, 0x0C, 0x27, 0x18, 0x2A, 0x18, 0x2C, 0x0C, 0x2E, 0x0C, 0x25, 0x0C, 0x2A,
+ 0x24, 0x29, 0x18, 0xFF, 0x85, 0x27, 0x18, 0x27, 0x06, 0x29, 0x06, 0x2A, 0x0C, 0x2C, 0x0C, 0x2A,
+ 0x0C, 0x29, 0x0C, 0x2A, 0x0C, 0x27, 0x18, 0x25, 0x18, 0x22, 0x18, 0x20, 0x0C, 0x22, 0x0C, 0x1E,
+ 0x18, 0x1D, 0x0C, 0x1E, 0x0C, 0x1B, 0x18, 0x1B, 0x0C, 0x1D, 0x0C, 0x1E, 0x0C, 0x20, 0x0C, 0x25,
+ 0x18, 0x20, 0x18, 0x20, 0x06, 0x22, 0x06, 0x25, 0x0C, 0xFF, 0x08, 0x18, 0x14, 0x0C, 0x08, 0x0C,
+ 0x14, 0x0C, 0x12, 0x0C, 0x14, 0x0C, 0x06, 0x18, 0x06, 0x0C, 0x12, 0x0C, 0x06, 0x0C, 0x12, 0x18,
+ 0x11, 0x0C, 0x12, 0x0C, 0x08, 0x18, 0x14, 0x0C, 0x08, 0x0C, 0x14, 0x0C, 0x08, 0x0C, 0x09, 0x0C,
+ 0x0A, 0x18, 0x22, 0x0C, 0x25, 0x0C, 0x22, 0x0C, 0x20, 0x0C, 0x22, 0x0C, 0x25, 0x0C, 0x2A, 0x0C,
+ 0xFF, 0x85, 0x1E, 0x18, 0x1E, 0x18, 0x20, 0x0C, 0x22, 0x18, 0x25, 0x18, 0x22, 0x0C, 0x20, 0x0C,
+ 0x22, 0x0C, 0x20, 0x18, 0x1B, 0x18, 0x1E, 0x18, 0x1E, 0x0C, 0x20, 0x0C, 0x22, 0x0C, 0x25, 0x18,
+ 0x27, 0x3C, 0x22, 0x18, 0x25, 0x0C, 0x2A, 0x0C, 0xFF, 0x80, 0x0F, 0x18, 0x1B, 0x0C, 0x0F, 0x0C,
+ 0x1B, 0x0C, 0x1B, 0x0C, 0x0F, 0x0C, 0x09, 0x18, 0x09, 0x0C, 0x15, 0x0C, 0x09, 0x0C, 0x15, 0x0C,
+ 0x15, 0x0C, 0x09, 0x18, 0x0F, 0x18, 0x1B, 0x0C, 0x0F, 0x0C, 0x1B, 0x0C, 0x1B, 0x0C, 0x0F, 0x0C,
+ 0x09, 0x18, 0x09, 0x0C, 0x15, 0x0C, 0x09, 0x0C, 0x15, 0x0C, 0x15, 0x0C, 0x09, 0x18, 0xFF, 0x85,
+ 0x27, 0x60, 0x2D, 0x60, 0x27, 0x60, 0x21, 0x60, 0xFF, 0x85, 0x0F, 0xC0, 0xA0, 0xC0, 0xFF, 0x86,
+ 0x27, 0xC0, 0xA0, 0xC0, 0xFF, 0x85, 0x1B, 0xC0, 0xA0, 0xC0, 0xFF
+};
+
+} // namespace CastleMusicData
+} // namespace Freescape
+
+#endif
diff --git a/engines/freescape/games/castle/dos.cpp b/engines/freescape/games/castle/dos.cpp
index 19c543f8e2f..ffd3d575045 100644
--- a/engines/freescape/games/castle/dos.cpp
+++ b/engines/freescape/games/castle/dos.cpp
@@ -20,10 +20,12 @@
*/
#include "common/file.h"
+#include "common/config-manager.h"
#include "common/memstream.h"
#include "freescape/freescape.h"
#include "freescape/games/castle/castle.h"
+#include "freescape/games/castle/opl.music.h"
#include "freescape/language/8bitDetokeniser.h"
namespace Freescape {
@@ -301,6 +303,9 @@ void CastleEngine::loadAssetsDOSFullGame() {
stream = decryptFile("CMEDF");
load8bitBinary(stream, 0, 16);
delete stream;
+
+ if (ConfMan.getBool("opl_music"))
+ _playerMusic = new CastleOPLMusicPlayer();
} else
error("Not implemented yet");
@@ -433,6 +438,9 @@ void CastleEngine::loadAssetsDOSDemo() {
stream = decryptFile("CDEDF");
load8bitBinary(stream, 0, 16);
delete stream;
+
+ if (ConfMan.getBool("opl_music"))
+ _playerMusic = new CastleOPLMusicPlayer();
} else
error("Not implemented yet");
diff --git a/engines/freescape/games/castle/opl.music.cpp b/engines/freescape/games/castle/opl.music.cpp
new file mode 100644
index 00000000000..1ead6651c6d
--- /dev/null
+++ b/engines/freescape/games/castle/opl.music.cpp
@@ -0,0 +1,422 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "engines/freescape/games/castle/opl.music.h"
+
+#include "common/textconsole.h"
+#include "common/util.h"
+#include "freescape/freescape.h"
+#include "freescape/games/castle/castle.musicdata.h"
+
+using namespace Freescape::CastleMusicData;
+
+namespace Freescape {
+
+namespace {
+
+struct OPLBasePatch {
+ byte modChar;
+ byte carChar;
+ byte modLevel;
+ byte carLevel;
+ byte modAD;
+ byte carAD;
+ byte modSR;
+ byte carSR;
+ byte modWave;
+ byte carWave;
+ byte feedbackConnection;
+};
+
+// ============================================================================
+// Embedded music data (adapted from Castle Master C64 patterns)
+// ============================================================================
+
+// OPL2 F-number/block table (95 entries)
+// Format: fnum (bits 0-9) | block (bits 10-12)
+// Derived from SID frequency table
+const uint16 kOPLFreqs[] = {
+ 0x0000, 0x0168, 0x017D, 0x0194, 0x01AD, 0x01C5,
+ 0x01E1, 0x01FD, 0x021B, 0x023B, 0x025E, 0x0282,
+ 0x02A8, 0x02D0, 0x02FB, 0x0328, 0x0358, 0x038B,
+ 0x03C1, 0x03FA, 0x061B, 0x063C, 0x065E, 0x0682,
+ 0x06A7, 0x06D0, 0x06FB, 0x0728, 0x0758, 0x078B,
+ 0x07C1, 0x07FA, 0x0A1B, 0x0A3B, 0x0A5D, 0x0A81,
+ 0x0AA8, 0x0AD0, 0x0AFB, 0x0B2B, 0x0B58, 0x0B8B,
+ 0x0BC1, 0x0BFA, 0x0E1B, 0x0E3B, 0x0E5D, 0x0E81,
+ 0x0EA8, 0x0ED0, 0x0EFB, 0x0F28, 0x0F58, 0x0F8B,
+ 0x0FC1, 0x0FFA, 0x121B, 0x123B, 0x125D, 0x1281,
+ 0x12A8, 0x12D0, 0x12FB, 0x1328, 0x1358, 0x138B,
+ 0x13C1, 0x13FA, 0x161B, 0x163B, 0x1661, 0x1681,
+ 0x16A8, 0x16D0, 0x16FB, 0x1729, 0x1758, 0x178B,
+ 0x17C1, 0x17FA, 0x1A1B, 0x1A3B, 0x1A5D, 0x1A81,
+ 0x1AA8, 0x1AD0, 0x1AFB, 0x1B28, 0x1B58, 0x1B8B,
+ 0x1BC1, 0x1BFA, 0x1E1B, 0x1E3B, 0x1E5D
+};
+
+// ============================================================================
+// OPL2 FM base patches (our own creation, but driven by the original SID data)
+// ============================================================================
+
+// OPL operator register offsets for channels 0-2
+const byte kOPLModOffset[] = { 0x00, 0x01, 0x02 };
+const byte kOPLCarOffset[] = { 0x03, 0x04, 0x05 };
+
+const OPLBasePatch kOPLBasePatches[] = {
+ { 0x21, 0x21, 0x22, 0x03, 0xF2, 0xF3, 0x74, 0x56, 0x00, 0x00, 0x04 },
+ { 0x02, 0x01, 0x1E, 0x00, 0xE4, 0xF2, 0x64, 0x46, 0x00, 0x01, 0x06 },
+ { 0x31, 0x21, 0x26, 0x06, 0xC3, 0xE3, 0x64, 0x47, 0x00, 0x00, 0x02 },
+ { 0x01, 0x01, 0x28, 0x04, 0xB2, 0xE4, 0x73, 0x58, 0x00, 0x00, 0x01 },
+ { 0x11, 0x01, 0x18, 0x00, 0xE2, 0xF3, 0x65, 0x47, 0x02, 0x00, 0x0C },
+ { 0x22, 0x21, 0x16, 0x00, 0xF4, 0xF2, 0x55, 0x36, 0x00, 0x00, 0x08 },
+ { 0x22, 0x21, 0x14, 0x00, 0xF4, 0xF3, 0x55, 0x26, 0x00, 0x00, 0x08 },
+ { 0x05, 0x01, 0x10, 0x00, 0xF6, 0xF4, 0x44, 0x34, 0x01, 0x00, 0x0E }
+};
+
+const byte kMusicAttenuation = 12;
+
+byte attenuateOPLLevel(byte level) {
+ return MIN<byte>((level & 0x3F) + kMusicAttenuation, 0x3F) | (level & 0xC0);
+}
+
+uint16 scaleDuration(byte duration) {
+ return MAX<uint16>(1, duration);
+}
+
+} // namespace
+
+// ============================================================================
+// ChannelState
+// ============================================================================
+
+void CastleOPLMusicPlayer::ChannelState::reset(const byte *channelOrderList) {
+ orderList = channelOrderList;
+ orderPosition = 0;
+ patternIndex = 0;
+ patternDataOffset = 0;
+ patternOffset = 0;
+ delay = 0;
+ instrument = 0;
+ transpose = 0;
+ frequencyFnum = 0;
+ frequencyBlock = 0;
+ keyOn = false;
+}
+
+// ============================================================================
+// Constructor / Destructor
+// ============================================================================
+
+CastleOPLMusicPlayer::CastleOPLMusicPlayer()
+ : _opl(nullptr),
+ _musicActive(false),
+ _tick(0) {
+ _opl = OPL::Config::create();
+ if (!_opl || !_opl->init()) {
+ warning("CastleOPLMusicPlayer: Failed to create OPL emulator");
+ delete _opl;
+ _opl = nullptr;
+ }
+}
+
+CastleOPLMusicPlayer::~CastleOPLMusicPlayer() {
+ stopMusic();
+ delete _opl;
+}
+
+// ============================================================================
+// Public interface
+// ============================================================================
+
+void CastleOPLMusicPlayer::startMusic() {
+ if (!_opl)
+ return;
+ stopMusic();
+ _opl->start(new Common::Functor0Mem<void, CastleOPLMusicPlayer>(
+ this, &CastleOPLMusicPlayer::onTimer), 50);
+ setupSong();
+}
+
+void CastleOPLMusicPlayer::stopMusic() {
+ _musicActive = false;
+ if (_opl) {
+ silenceAll();
+ _opl->stop();
+ }
+}
+
+bool CastleOPLMusicPlayer::isPlaying() const {
+ return _musicActive;
+}
+
+// ============================================================================
+// OPL register helpers
+// ============================================================================
+
+void CastleOPLMusicPlayer::noteToFnumBlock(int note, uint16 &fnum, byte &block) const {
+ if (note < 0)
+ note = 0;
+ if (note > kMaxNote)
+ note = kMaxNote;
+
+ uint16 combined = kOPLFreqs[note];
+ fnum = combined & 0x03FF;
+ block = (combined >> 10) & 0x07;
+}
+
+void CastleOPLMusicPlayer::setFrequency(int channel, uint16 fnum, byte block) {
+ _channels[channel].frequencyFnum = fnum;
+ _channels[channel].frequencyBlock = block;
+ writeFrequency(channel, fnum, block);
+}
+
+void CastleOPLMusicPlayer::writeFrequency(int channel, uint16 fnum, byte block) {
+ if (!_opl)
+ return;
+
+ _opl->writeReg(0xA0 + channel, fnum & 0xFF);
+ byte b0 = ((fnum >> 8) & 0x03) | (block << 2);
+ if (_channels[channel].keyOn)
+ b0 |= 0x20;
+ _opl->writeReg(0xB0 + channel, b0);
+}
+
+void CastleOPLMusicPlayer::setOPLInstrument(int channel, byte instrument) {
+ if (!_opl)
+ return;
+
+ const OPLBasePatch &patch = kOPLBasePatches[instrument % ARRAYSIZE(kOPLBasePatches)];
+ byte mod = kOPLModOffset[channel];
+ byte car = kOPLCarOffset[channel];
+
+ _opl->writeReg(0x20 + mod, patch.modChar);
+ _opl->writeReg(0x20 + car, patch.carChar);
+ _opl->writeReg(0x40 + mod, attenuateOPLLevel(patch.modLevel));
+ _opl->writeReg(0x40 + car, attenuateOPLLevel(patch.carLevel));
+ _opl->writeReg(0x60 + mod, patch.modAD);
+ _opl->writeReg(0x60 + car, patch.carAD);
+ _opl->writeReg(0x80 + mod, patch.modSR);
+ _opl->writeReg(0x80 + car, patch.carSR);
+ _opl->writeReg(0xE0 + mod, patch.modWave);
+ _opl->writeReg(0xE0 + car, patch.carWave);
+ _opl->writeReg(0xC0 + channel, patch.feedbackConnection);
+}
+
+void CastleOPLMusicPlayer::noteOn(int channel, byte note) {
+ if (!_opl)
+ return;
+
+ noteOff(channel);
+
+ uint16 fnum = 0;
+ byte block = 0;
+ noteToFnumBlock(note + _channels[channel].transpose + 20, fnum, block);
+ _channels[channel].keyOn = true;
+ setFrequency(channel, fnum, block);
+}
+
+void CastleOPLMusicPlayer::noteOff(int channel) {
+ if (!_opl)
+ return;
+
+ _channels[channel].keyOn = false;
+ writeFrequency(channel, _channels[channel].frequencyFnum, _channels[channel].frequencyBlock);
+}
+
+// ============================================================================
+// Timer / sequencer core
+// ============================================================================
+
+void CastleOPLMusicPlayer::onTimer() {
+ if (!_musicActive)
+ return;
+
+ for (int i = 0; i < kChannelCount; i++) {
+ if (_channels[i].delay > 0) {
+ // The SID release tail covers its early gate-off; a hard OPL key-off
+ // here creates audible gaps, so hold until the next note or rest.
+ _channels[i].delay--;
+ continue;
+ }
+
+ parseCommands(i);
+ }
+
+ _tick++;
+}
+
+// ============================================================================
+// Song setup
+// ============================================================================
+
+void CastleOPLMusicPlayer::setupSong() {
+ silenceAll();
+ _tick = 0;
+
+ // Enable wave select (required for non-sine waveforms)
+ _opl->writeReg(0x01, 0x20);
+ _opl->writeReg(0xBD, 0x00);
+
+ for (int i = 0; i < kChannelCount; i++) {
+ _channels[i].reset(kChannelOrderLists[i]);
+ setOPLInstrument(i, i == 2 ? 6 : 5);
+ loadNextPattern(i);
+ }
+
+ _musicActive = true;
+}
+
+void CastleOPLMusicPlayer::silenceAll() {
+ if (!_opl)
+ return;
+
+ for (int i = 0; i < kChannelCount; i++) {
+ _channels[i].keyOn = false;
+ _opl->writeReg(0xB0 + i, 0x00);
+ _opl->writeReg(0x40 + kOPLModOffset[i], 0x3F);
+ _opl->writeReg(0x40 + kOPLCarOffset[i], 0x3F);
+ }
+}
+
+// ============================================================================
+// Pattern command parser
+// ============================================================================
+
+void CastleOPLMusicPlayer::loadNextPattern(int channel) {
+ ChannelState &c = _channels[channel];
+ int safety = 128;
+
+ while (safety-- > 0) {
+ byte value = c.orderList[c.orderPosition++];
+
+ if (value == kOrderEnd) {
+ c.orderPosition = 0;
+ continue;
+ }
+
+ if (value == kOrderTranspose) {
+ byte transpose = c.orderList[c.orderPosition++];
+ c.transpose = transpose >= 0x80 ? transpose - 0x100 : transpose;
+ continue;
+ }
+
+ if (value < ARRAYSIZE(kPatternOffsets)) {
+ c.patternIndex = value;
+ c.patternDataOffset = kPatternOffsets[value];
+ c.patternOffset = 0;
+ debugC(1, kFreescapeDebugMedia,
+ "Castle OPL t=%u ch=%d order=%u PATTERN %u dataOffset=%u transpose=%d",
+ (uint)_tick, channel, c.orderPosition - 1, c.patternIndex, c.patternDataOffset, c.transpose);
+ return;
+ }
+ }
+
+ c.patternDataOffset = 0;
+ c.patternOffset = 0;
+}
+
+byte CastleOPLMusicPlayer::readPatternByte(int channel) {
+ ChannelState &c = _channels[channel];
+ uint16 offset = c.patternDataOffset + c.patternOffset;
+ if (offset >= ARRAYSIZE(kPatternData)) {
+ loadNextPattern(channel);
+ offset = c.patternDataOffset + c.patternOffset;
+ }
+
+ c.patternOffset++;
+ return kPatternData[offset];
+}
+
+void CastleOPLMusicPlayer::parseCommands(int channel) {
+ ChannelState &c = _channels[channel];
+ int safety = 128;
+
+ while (safety-- > 0) {
+ byte command = readPatternByte(channel);
+ uint16 commandOffset = c.patternOffset - 1;
+
+ if (command == 0xFF) {
+ loadNextPattern(channel);
+ continue;
+ }
+
+ if (command >= 0x80 && command < 0x90) {
+ c.instrument = command & 0x0F;
+ setOPLInstrument(channel, c.instrument);
+ debugC(1, kFreescapeDebugMedia,
+ "Castle OPL t=%u ch=%d pat=%u pos=%u INST %u",
+ (uint)_tick, channel, c.patternIndex, commandOffset, c.instrument);
+ continue;
+ }
+
+ if (command == 0xA0) {
+ byte duration = readPatternByte(channel);
+ debugC(1, kFreescapeDebugMedia,
+ "Castle OPL t=%u ch=%d pat=%u pos=%u REST dur=%u inst=%u",
+ (uint)_tick, channel, c.patternIndex, commandOffset, duration, c.instrument);
+ noteOff(channel);
+ c.delay = scaleDuration(duration);
+ return;
+ }
+
+ if (command >= 0x90 && command < 0xC0) {
+ debugC(1, kFreescapeDebugMedia,
+ "Castle OPL t=%u ch=%d pat=%u pos=%u EFFECT $%02x skipped inst=%u",
+ (uint)_tick, channel, c.patternIndex, commandOffset, command, c.instrument);
+ continue;
+ }
+
+ if (command >= 0xC0) {
+ byte slideDuration = readPatternByte(channel);
+ byte slideLo = readPatternByte(channel);
+ byte slideHi = readPatternByte(channel);
+ debugC(1, kFreescapeDebugMedia,
+ "Castle OPL t=%u ch=%d pat=%u pos=%u SLIDE $%02x dur=%u delta=$%02x%02x skipped inst=%u",
+ (uint)_tick, channel, c.patternIndex, commandOffset, command, slideDuration, slideHi, slideLo, c.instrument);
+ continue;
+ }
+
+ byte duration = readPatternByte(channel);
+ if (command == 0) {
+ debugC(1, kFreescapeDebugMedia,
+ "Castle OPL t=%u ch=%d pat=%u pos=%u OFF dur=%u inst=%u",
+ (uint)_tick, channel, c.patternIndex, commandOffset, duration, c.instrument);
+ noteOff(channel);
+ } else {
+ int effectiveNote = command + c.transpose + 20;
+ noteOn(channel, command);
+ debugC(1, kFreescapeDebugMedia,
+ "Castle OPL t=%u ch=%d pat=%u pos=%u NOTE raw=$%02x effective=%d inst=%u transpose=%d dur=%u fnum=$%03x block=%u",
+ (uint)_tick, channel, c.patternIndex, commandOffset, command, effectiveNote, c.instrument,
+ c.transpose, duration, c.frequencyFnum, c.frequencyBlock);
+ }
+ c.delay = scaleDuration(duration);
+ return;
+ }
+
+ debugC(1, kFreescapeDebugMedia,
+ "Castle OPL t=%u ch=%d pat=%u parser safety stop inst=%u",
+ (uint)_tick, channel, c.patternIndex, c.instrument);
+ noteOff(channel);
+ c.delay = 12;
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/games/castle/opl.music.h b/engines/freescape/games/castle/opl.music.h
new file mode 100644
index 00000000000..1c86806391f
--- /dev/null
+++ b/engines/freescape/games/castle/opl.music.h
@@ -0,0 +1,90 @@
+/* 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_CASTLE_OPL_MUSIC_H
+#define FREESCAPE_CASTLE_OPL_MUSIC_H
+
+#include "audio/fmopl.h"
+#include "freescape/music.h"
+
+namespace Freescape {
+
+/**
+ * OPL2/AdLib music player for Castle Master DOS.
+ *
+ * Adapts the Castle Master C64 title music to the OPL2 FM chip by:
+ * - Reusing compact note, rest, and instrument pattern events
+ * - Converting SID note numbers to OPL F-number/block pairs
+ * - Mapping the original voice changes to OPL FM instrument patches
+ */
+class CastleOPLMusicPlayer : public MusicPlayer {
+public:
+ CastleOPLMusicPlayer();
+ ~CastleOPLMusicPlayer() override;
+
+ void startMusic() override;
+ void stopMusic() override;
+ bool isPlaying() const override;
+
+private:
+ enum {
+ kChannelCount = 3,
+ kMaxNote = 94
+ };
+
+ struct ChannelState {
+ const byte *orderList;
+ uint16 orderPosition;
+ byte patternIndex;
+ uint16 patternDataOffset;
+ uint16 patternOffset;
+ uint16 delay;
+ byte instrument;
+ int8 transpose;
+ uint16 frequencyFnum;
+ byte frequencyBlock;
+ bool keyOn;
+
+ void reset(const byte *channelOrderList);
+ };
+
+ OPL::OPL *_opl;
+ bool _musicActive;
+ uint32 _tick;
+ ChannelState _channels[kChannelCount];
+
+ void onTimer();
+ void setupSong();
+ void silenceAll();
+ void loadNextPattern(int channel);
+ byte readPatternByte(int channel);
+ void parseCommands(int channel);
+ void setOPLInstrument(int channel, byte instrument);
+ void noteOn(int channel, byte note);
+ void noteOff(int channel);
+ void setFrequency(int channel, uint16 fnum, byte block);
+ void writeFrequency(int channel, uint16 fnum, byte block);
+ void noteToFnumBlock(int note, uint16 &fnum, byte &block) const;
+};
+
+} // namespace Freescape
+
+#endif
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index 89319af53cd..0f2b3807ea5 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -15,6 +15,7 @@ MODULE_OBJS := \
games/castle/c64.o \
games/castle/cpc.o \
games/castle/dos.o \
+ games/castle/opl.music.o \
games/castle/zx.o \
games/dark/amiga.o \
games/dark/atari.o \
Commit: db8c052c2cc38e957f4c2d24fae6e1fb08402f74
https://github.com/scummvm/scummvm/commit/db8c052c2cc38e957f4c2d24fae6e1fb08402f74
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-25T14:08:39+02:00
Commit Message:
FREESCAPE: optional AY rendition of castle music (c64)
Changed paths:
A engines/freescape/games/castle/ay.music.cpp
A engines/freescape/games/castle/ay.music.h
engines/freescape/detection.cpp
engines/freescape/games/castle/cpc.cpp
engines/freescape/module.mk
diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index c0bd24e6f28..4bc79e2f4e1 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -924,7 +924,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::UNK_LANG, // Multi-language
Common::kPlatformAmstradCPC,
ADGF_NO_FLAGS,
- GUIO4(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_TRAVEL_ROCK, GAMEOPTION_WASD_CONTROLS)
+ GUIO5(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_TRAVEL_ROCK, GAMEOPTION_WASD_CONTROLS, GAMEOPTION_AY_MUSIC)
},
// C64 tape release
{
diff --git a/engines/freescape/games/castle/ay.music.cpp b/engines/freescape/games/castle/ay.music.cpp
new file mode 100644
index 00000000000..3b609995404
--- /dev/null
+++ b/engines/freescape/games/castle/ay.music.cpp
@@ -0,0 +1,536 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "engines/freescape/games/castle/ay.music.h"
+
+#include "common/textconsole.h"
+#include "common/util.h"
+#include "freescape/freescape.h"
+#include "freescape/games/castle/castle.musicdata.h"
+
+using namespace Freescape::CastleMusicData;
+
+namespace Freescape {
+
+namespace {
+
+// AY-3-8910 period table (95 entries, same note numbering as the Castle OPL
+// table and Total Eclipse AY player). AY clock = 1MHz.
+const uint16 kAYPeriods[] = {
+ 0, 3657, 3455, 3265, 3076, 2908, 2743, 2589,
+ 2447, 2309, 2176, 2055, 1939, 1832, 1728, 1632,
+ 1540, 1454, 1371, 1295, 1222, 1153, 1088, 1027,
+ 970, 915, 864, 816, 770, 726, 686, 647,
+ 611, 577, 544, 514, 485, 458, 432, 406,
+ 385, 363, 343, 324, 305, 288, 272, 257,
+ 242, 229, 216, 204, 192, 182, 171, 162,
+ 153, 144, 136, 128, 121, 114, 108, 102,
+ 96, 91, 86, 81, 76, 72, 68, 64,
+ 61, 57, 54, 51, 48, 45, 43, 40,
+ 38, 36, 34, 32, 30, 29, 27, 25,
+ 24, 23, 21, 20, 19, 18, 17
+};
+
+const byte kAYInstrumentVolumes[] = {
+ 13, 11, 10, 11, 13, 13, 14, 12
+};
+
+const uint16 kAttackRate[16] = {
+ 0x0F00, 0x0F00, 0x0F00, 0x0C80,
+ 0x07E5, 0x055B, 0x0469, 0x03C0,
+ 0x0300, 0x0133, 0x009A, 0x0060,
+ 0x004D, 0x001A, 0x000F, 0x000A
+};
+
+const uint16 kDecayReleaseRate[16] = {
+ 0x0F00, 0x0C80, 0x0640, 0x042B,
+ 0x02A2, 0x01C9, 0x0178, 0x0140,
+ 0x0100, 0x0066, 0x0033, 0x0020,
+ 0x001A, 0x0009, 0x0005, 0x0003
+};
+
+const int8 kVibrato10[] = { 0, 20, 10, -10, -20, 15, 5, -20 };
+const int8 kVibrato18[] = { 0, 3, -3, 3, -3, 3, -3, 3 };
+const int8 kVibrato20[] = { 0, -100, -100, -100, -100, -100, -100, -100 };
+const uint32 kSIDToAYPeriodScale = 1079000;
+
+const int8 *getVibratoTable(byte vibrato) {
+ switch (vibrato) {
+ case 0x10:
+ return kVibrato10;
+ case 0x18:
+ return kVibrato18;
+ case 0x20:
+ return kVibrato20;
+ default:
+ return nullptr;
+ }
+}
+
+uint16 applySIDFrequencyOffset(uint16 basePeriod, int16 frequencyOffset) {
+ if (basePeriod == 0 || frequencyOffset == 0)
+ return basePeriod;
+
+ int32 sidFrequency = (kSIDToAYPeriodScale + (basePeriod / 2)) / basePeriod;
+ sidFrequency = MAX<int32>(1, sidFrequency + frequencyOffset);
+
+ uint32 period = (kSIDToAYPeriodScale + (sidFrequency / 2)) / sidFrequency;
+ return CLIP<uint32>(period, 1, 4095);
+}
+
+} // namespace
+
+void CastleAYMusicPlayer::ChannelState::reset(const byte *channelOrderList) {
+ orderList = channelOrderList;
+ orderPosition = 0;
+ patternIndex = 0;
+ patternDataOffset = 0;
+ patternOffset = 0;
+ delay = 0;
+ instrument = 0;
+ transpose = 0;
+ currentNote = 0;
+ basePeriod = 0;
+ currentPeriod = 0;
+ adsrVolume = 0;
+ attackRate = 0;
+ decayRate = 0;
+ sustainLevel = 0;
+ releaseRate = 0;
+ sidFrequencyOffset = 0;
+ vibratoStep = 1;
+ vibratoReverse = false;
+ toneEnabled = false;
+ noiseEnabled = false;
+ active = false;
+ adsrPhase = kPhaseOff;
+}
+
+CastleAYMusicPlayer::CastleAYMusicPlayer(Audio::Mixer *mixer)
+ : AY8912Stream(44100, 1000000),
+ _mixer(mixer),
+ _musicActive(false),
+ _mixerRegister(0x38),
+ _tickSampleCount(0),
+ _tick(0) {
+}
+
+CastleAYMusicPlayer::~CastleAYMusicPlayer() {
+ stopMusic();
+}
+
+void CastleAYMusicPlayer::startMusic() {
+ if (!_mixer)
+ return;
+
+ stopMusic();
+ _mixer->playStream(Audio::Mixer::kMusicSoundType, &_handle, toAudioStream(),
+ -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO);
+ setupSong();
+}
+
+void CastleAYMusicPlayer::stopMusic() {
+ _musicActive = false;
+ silenceAll();
+ if (_mixer)
+ _mixer->stopHandle(_handle);
+}
+
+bool CastleAYMusicPlayer::isPlaying() const {
+ return _musicActive;
+}
+
+int CastleAYMusicPlayer::readBuffer(int16 *buffer, const int numSamples) {
+ if (!_musicActive) {
+ memset(buffer, 0, numSamples * sizeof(int16));
+ return numSamples;
+ }
+
+ int samplesGenerated = 0;
+ int samplesPerTick = (getRate() / 50) * 2;
+
+ while (samplesGenerated < numSamples) {
+ int remaining = samplesPerTick - _tickSampleCount;
+ int toGenerate = MIN(numSamples - samplesGenerated, remaining);
+
+ if (toGenerate > 0) {
+ generateSamples(buffer + samplesGenerated, toGenerate);
+ samplesGenerated += toGenerate;
+ _tickSampleCount += toGenerate;
+ }
+
+ if (_tickSampleCount >= samplesPerTick) {
+ _tickSampleCount -= samplesPerTick;
+ onTimer();
+ }
+ }
+
+ return samplesGenerated;
+}
+
+void CastleAYMusicPlayer::onTimer() {
+ if (!_musicActive)
+ return;
+
+ for (int i = 0; i < kChannelCount; i++) {
+ if (_channels[i].delay > 0) {
+ if (_channels[i].active) {
+ const InstrumentData &instrument = kInstruments[_channels[i].instrument % ARRAYSIZE(kInstruments)];
+ if (_channels[i].delay == instrument.gateOffTime)
+ releaseADSR(i);
+ }
+ applyFrameEffects(i);
+ updateADSR(i);
+ _channels[i].delay--;
+ continue;
+ }
+
+ parseCommands(i);
+ applyFrameEffects(i);
+ updateADSR(i);
+ }
+
+ _tick++;
+}
+
+void CastleAYMusicPlayer::setupSong() {
+ silenceAll();
+ _tick = 0;
+ _tickSampleCount = 0;
+
+ _mixerRegister = 0x38;
+ setReg(7, _mixerRegister);
+ setReg(6, 0x07);
+
+ for (int i = 0; i < kChannelCount; i++) {
+ _channels[i].reset(kChannelOrderLists[i]);
+ loadNextPattern(i);
+ }
+
+ _musicActive = true;
+}
+
+void CastleAYMusicPlayer::silenceAll() {
+ for (int r = 0; r < 14; r++)
+ setReg(r, 0);
+
+ _mixerRegister = 0x3F;
+ setReg(7, _mixerRegister);
+}
+
+void CastleAYMusicPlayer::loadNextPattern(int channel) {
+ ChannelState &c = _channels[channel];
+ int safety = 128;
+
+ while (safety-- > 0) {
+ byte value = c.orderList[c.orderPosition++];
+
+ if (value == kOrderEnd) {
+ c.orderPosition = 0;
+ continue;
+ }
+
+ if (value == kOrderTranspose) {
+ byte transpose = c.orderList[c.orderPosition++];
+ c.transpose = transpose >= 0x80 ? transpose - 0x100 : transpose;
+ continue;
+ }
+
+ if (value < ARRAYSIZE(kPatternOffsets)) {
+ c.patternIndex = value;
+ c.patternDataOffset = kPatternOffsets[value];
+ c.patternOffset = 0;
+ debugC(1, kFreescapeDebugMedia,
+ "Castle AY t=%u ch=%d order=%u PATTERN %u dataOffset=%u transpose=%d",
+ (uint)_tick, channel, c.orderPosition - 1, c.patternIndex, c.patternDataOffset, c.transpose);
+ return;
+ }
+ }
+
+ c.patternDataOffset = 0;
+ c.patternOffset = 0;
+}
+
+byte CastleAYMusicPlayer::readPatternByte(int channel) {
+ ChannelState &c = _channels[channel];
+ uint16 offset = c.patternDataOffset + c.patternOffset;
+ if (offset >= ARRAYSIZE(kPatternData)) {
+ loadNextPattern(channel);
+ offset = c.patternDataOffset + c.patternOffset;
+ }
+
+ c.patternOffset++;
+ return kPatternData[offset];
+}
+
+void CastleAYMusicPlayer::parseCommands(int channel) {
+ ChannelState &c = _channels[channel];
+ int safety = 128;
+
+ while (safety-- > 0) {
+ byte command = readPatternByte(channel);
+ uint16 commandOffset = c.patternOffset - 1;
+
+ if (command == 0xFF) {
+ loadNextPattern(channel);
+ continue;
+ }
+
+ if (command >= 0x80 && command < 0x90) {
+ c.instrument = command & 0x0F;
+ debugC(1, kFreescapeDebugMedia,
+ "Castle AY t=%u ch=%d pat=%u pos=%u INST %u",
+ (uint)_tick, channel, c.patternIndex, commandOffset, c.instrument);
+ continue;
+ }
+
+ if (command == 0xA0) {
+ byte duration = readPatternByte(channel);
+ debugC(1, kFreescapeDebugMedia,
+ "Castle AY t=%u ch=%d pat=%u pos=%u REST dur=%u inst=%u",
+ (uint)_tick, channel, c.patternIndex, commandOffset, duration, c.instrument);
+ noteOff(channel);
+ c.delay = MAX<uint16>(1, duration);
+ return;
+ }
+
+ if (command >= 0x90 && command < 0xC0) {
+ debugC(1, kFreescapeDebugMedia,
+ "Castle AY t=%u ch=%d pat=%u pos=%u EFFECT $%02x skipped inst=%u",
+ (uint)_tick, channel, c.patternIndex, commandOffset, command, c.instrument);
+ continue;
+ }
+
+ if (command >= 0xC0) {
+ byte slideDuration = readPatternByte(channel);
+ byte slideLo = readPatternByte(channel);
+ byte slideHi = readPatternByte(channel);
+ debugC(1, kFreescapeDebugMedia,
+ "Castle AY t=%u ch=%d pat=%u pos=%u SLIDE $%02x dur=%u delta=$%02x%02x skipped inst=%u",
+ (uint)_tick, channel, c.patternIndex, commandOffset, command, slideDuration, slideHi, slideLo, c.instrument);
+ continue;
+ }
+
+ byte duration = readPatternByte(channel);
+ if (command == 0) {
+ debugC(1, kFreescapeDebugMedia,
+ "Castle AY t=%u ch=%d pat=%u pos=%u OFF dur=%u inst=%u",
+ (uint)_tick, channel, c.patternIndex, commandOffset, duration, c.instrument);
+ noteOff(channel);
+ } else {
+ int effectiveNote = command + c.transpose + 20;
+ noteOn(channel, command);
+ debugC(1, kFreescapeDebugMedia,
+ "Castle AY t=%u ch=%d pat=%u pos=%u NOTE raw=$%02x effective=%d inst=%u transpose=%d dur=%u period=%u",
+ (uint)_tick, channel, c.patternIndex, commandOffset, command, effectiveNote, c.instrument,
+ c.transpose, duration, c.currentPeriod);
+ }
+ c.delay = MAX<uint16>(1, duration);
+ return;
+ }
+
+ debugC(1, kFreescapeDebugMedia,
+ "Castle AY t=%u ch=%d pat=%u parser safety stop inst=%u",
+ (uint)_tick, channel, c.patternIndex, c.instrument);
+ noteOff(channel);
+ c.delay = 12;
+}
+
+void CastleAYMusicPlayer::noteOn(int channel, byte note) {
+ ChannelState &c = _channels[channel];
+ const InstrumentData &instrument = kInstruments[c.instrument % ARRAYSIZE(kInstruments)];
+ byte control = sidControlForInstrument(c.instrument);
+
+ if ((control & 0x01) == 0) {
+ noteOff(channel);
+ return;
+ }
+
+ int effectiveNote = note + c.transpose + 20;
+ uint16 period = 0;
+ noteToPeriod(effectiveNote, period);
+ c.basePeriod = period;
+ c.sidFrequencyOffset = 0;
+ c.vibratoStep = 1;
+ c.vibratoReverse = false;
+ writeChannelPeriod(channel, c.basePeriod);
+
+ bool noiseEnabled = (control & 0x80) != 0;
+ bool toneEnabled = (control & 0x70) != 0;
+ if (noiseEnabled)
+ writeNoisePeriod(c.basePeriod);
+ setChannelOutput(channel, toneEnabled, noiseEnabled);
+
+ c.currentNote = CLIP<int>(effectiveNote, 0, kMaxNote);
+ c.active = true;
+ triggerADSR(channel, instrument.attackDecay, instrument.sustainRelease);
+}
+
+void CastleAYMusicPlayer::noteOff(int channel) {
+ ChannelState &c = _channels[channel];
+ c.active = false;
+ c.adsrPhase = kPhaseOff;
+ c.adsrVolume = 0;
+ c.currentNote = 0;
+ c.basePeriod = 0;
+ c.sidFrequencyOffset = 0;
+ setReg(8 + channel, 0);
+ setChannelOutput(channel, false, false);
+}
+
+void CastleAYMusicPlayer::writeChannelPeriod(int channel, uint16 period) {
+ _channels[channel].currentPeriod = period;
+ setReg(channel * 2, period & 0xFF);
+ setReg(channel * 2 + 1, (period >> 8) & 0x0F);
+}
+
+void CastleAYMusicPlayer::noteToPeriod(int note, uint16 &period) const {
+ if (note < 0)
+ note = 0;
+ if (note > kMaxNote)
+ note = kMaxNote;
+
+ period = kAYPeriods[note];
+}
+
+void CastleAYMusicPlayer::applyFrameEffects(int channel) {
+ ChannelState &c = _channels[channel];
+ if (c.adsrPhase == kPhaseOff || c.currentNote == 0 || c.basePeriod == 0)
+ return;
+
+ const InstrumentData &instrument = kInstruments[c.instrument % ARRAYSIZE(kInstruments)];
+ const int8 *vibratoTable = getVibratoTable(instrument.vibrato);
+ if (!vibratoTable)
+ return;
+
+ int8 sidDelta = vibratoTable[c.vibratoStep & 0x07];
+ c.sidFrequencyOffset += c.vibratoReverse ? -sidDelta : sidDelta;
+ c.vibratoStep++;
+ if (c.vibratoStep >= 8) {
+ c.vibratoStep = 1;
+ c.vibratoReverse = !c.vibratoReverse;
+ }
+
+ uint16 period = applySIDFrequencyOffset(c.basePeriod, c.sidFrequencyOffset);
+ writeChannelPeriod(channel, period);
+ if (c.noiseEnabled)
+ writeNoisePeriod(period);
+}
+
+void CastleAYMusicPlayer::triggerADSR(int channel, byte attackDecay, byte sustainRelease) {
+ ChannelState &c = _channels[channel];
+ c.adsrPhase = kPhaseAttack;
+ c.attackRate = kAttackRate[attackDecay >> 4];
+ c.decayRate = kDecayReleaseRate[attackDecay & 0x0F];
+ c.sustainLevel = sustainRelease >> 4;
+ c.releaseRate = kDecayReleaseRate[sustainRelease & 0x0F];
+}
+
+void CastleAYMusicPlayer::releaseADSR(int channel) {
+ ChannelState &c = _channels[channel];
+ c.active = false;
+ if (c.adsrPhase != kPhaseOff && c.adsrPhase != kPhaseRelease)
+ c.adsrPhase = kPhaseRelease;
+}
+
+void CastleAYMusicPlayer::updateADSR(int channel) {
+ ChannelState &c = _channels[channel];
+
+ switch (c.adsrPhase) {
+ case kPhaseAttack:
+ c.adsrVolume += c.attackRate;
+ if (c.adsrVolume >= 0x0F00) {
+ c.adsrVolume = 0x0F00;
+ c.adsrPhase = kPhaseDecay;
+ }
+ break;
+
+ case kPhaseDecay: {
+ uint16 sustainTarget = (uint16)c.sustainLevel << 8;
+ if (c.adsrVolume > c.decayRate + sustainTarget) {
+ c.adsrVolume -= c.decayRate;
+ } else {
+ c.adsrVolume = sustainTarget;
+ c.adsrPhase = kPhaseSustain;
+ }
+ break;
+ }
+
+ case kPhaseSustain:
+ break;
+
+ case kPhaseRelease:
+ if (c.adsrVolume > c.releaseRate) {
+ c.adsrVolume -= c.releaseRate;
+ } else {
+ c.adsrVolume = 0;
+ c.adsrPhase = kPhaseOff;
+ c.currentNote = 0;
+ setChannelOutput(channel, false, false);
+ }
+ break;
+
+ case kPhaseOff:
+ c.adsrVolume = 0;
+ break;
+ }
+
+ byte volume = (c.adsrVolume >> 8) * instrumentVolumeScale(c.instrument) / 15;
+ setReg(8 + channel, volume);
+}
+
+void CastleAYMusicPlayer::setChannelOutput(int channel, bool toneEnabled, bool noiseEnabled) {
+ _channels[channel].toneEnabled = toneEnabled;
+ _channels[channel].noiseEnabled = noiseEnabled;
+
+ if (toneEnabled)
+ _mixerRegister &= ~(1 << channel);
+ else
+ _mixerRegister |= (1 << channel);
+
+ if (noiseEnabled)
+ _mixerRegister &= ~(1 << (channel + 3));
+ else
+ _mixerRegister |= (1 << (channel + 3));
+
+ setReg(7, _mixerRegister);
+}
+
+void CastleAYMusicPlayer::writeNoisePeriod(uint16 period) {
+ setReg(6, CLIP<uint16>(period >> 5, 1, 31));
+}
+
+byte CastleAYMusicPlayer::sidControlForInstrument(byte instrument) const {
+ const InstrumentData &instrumentData = kInstruments[instrument % ARRAYSIZE(kInstruments)];
+
+ // Castle SID waveform sequence 5 switches the audible part of the note to
+ // triangle; on AY this maps to the tone generator rather than noise.
+ if (instrumentData.effect == 0x05)
+ return 0x11;
+
+ return instrumentData.control;
+}
+
+byte CastleAYMusicPlayer::instrumentVolumeScale(byte instrument) const {
+ return kAYInstrumentVolumes[instrument % ARRAYSIZE(kAYInstrumentVolumes)];
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/games/castle/ay.music.h b/engines/freescape/games/castle/ay.music.h
new file mode 100644
index 00000000000..4dd955e10b0
--- /dev/null
+++ b/engines/freescape/games/castle/ay.music.h
@@ -0,0 +1,122 @@
+/* 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_CASTLE_AY_MUSIC_H
+#define FREESCAPE_CASTLE_AY_MUSIC_H
+
+#include "audio/mixer.h"
+#include "audio/softsynth/ay8912.h"
+#include "freescape/music.h"
+
+namespace Freescape {
+
+/**
+ * AY-3-8910 music player for Castle Master CPC.
+ *
+ * Adapts the Castle Master C64 title music to the CPC AY chip by reusing the
+ * Castle order lists and pattern data shared with the AdLib rendition.
+ */
+class CastleAYMusicPlayer : public MusicPlayer, private Audio::AY8912Stream {
+public:
+ CastleAYMusicPlayer(Audio::Mixer *mixer);
+ ~CastleAYMusicPlayer() override;
+
+ void startMusic() override;
+ void stopMusic() override;
+ bool isPlaying() const override;
+
+ int readBuffer(int16 *buffer, const int numSamples) override;
+ bool endOfData() const override { return !_musicActive; }
+ bool endOfStream() const override { return false; }
+
+private:
+ enum {
+ kChannelCount = 3,
+ kMaxNote = 94
+ };
+
+ enum ADSRPhase {
+ kPhaseOff,
+ kPhaseAttack,
+ kPhaseDecay,
+ kPhaseSustain,
+ kPhaseRelease
+ };
+
+ struct ChannelState {
+ const byte *orderList;
+ uint16 orderPosition;
+ byte patternIndex;
+ uint16 patternDataOffset;
+ uint16 patternOffset;
+ uint16 delay;
+ byte instrument;
+ int8 transpose;
+ byte currentNote;
+ uint16 basePeriod;
+ uint16 currentPeriod;
+ uint16 adsrVolume;
+ uint16 attackRate;
+ uint16 decayRate;
+ byte sustainLevel;
+ uint16 releaseRate;
+ int16 sidFrequencyOffset;
+ byte vibratoStep;
+ bool vibratoReverse;
+ bool toneEnabled;
+ bool noiseEnabled;
+ bool active;
+ ADSRPhase adsrPhase;
+
+ void reset(const byte *channelOrderList);
+ };
+
+ Audio::Mixer *_mixer;
+ Audio::SoundHandle _handle;
+ bool _musicActive;
+ byte _mixerRegister;
+ int _tickSampleCount;
+ uint32 _tick;
+ ChannelState _channels[kChannelCount];
+
+ void onTimer();
+ void setupSong();
+ void silenceAll();
+ void loadNextPattern(int channel);
+ byte readPatternByte(int channel);
+ void parseCommands(int channel);
+ void noteOn(int channel, byte note);
+ void noteOff(int channel);
+ void writeChannelPeriod(int channel, uint16 period);
+ void noteToPeriod(int note, uint16 &period) const;
+ void applyFrameEffects(int channel);
+ void triggerADSR(int channel, byte attackDecay, byte sustainRelease);
+ void releaseADSR(int channel);
+ void updateADSR(int channel);
+ void setChannelOutput(int channel, bool toneEnabled, bool noiseEnabled);
+ void writeNoisePeriod(uint16 period);
+ byte sidControlForInstrument(byte instrument) const;
+ byte instrumentVolumeScale(byte instrument) const;
+};
+
+} // namespace Freescape
+
+#endif
diff --git a/engines/freescape/games/castle/cpc.cpp b/engines/freescape/games/castle/cpc.cpp
index f19a2a775dc..5a2896b7de0 100644
--- a/engines/freescape/games/castle/cpc.cpp
+++ b/engines/freescape/games/castle/cpc.cpp
@@ -19,10 +19,12 @@
*
*/
+#include "common/config-manager.h"
#include "common/file.h"
#include "common/memstream.h"
#include "freescape/freescape.h"
+#include "freescape/games/castle/ay.music.h"
#include "freescape/games/castle/castle.h"
#include "freescape/language/8bitDetokeniser.h"
@@ -530,6 +532,9 @@ void CastleEngine::loadAssetsCPCFullGame() {
it._value->addObjectFromArea(id, _areaMap[255]);
}
}
+
+ if (ConfMan.getBool("ay_music"))
+ _playerMusic = new CastleAYMusicPlayer(_mixer);
}
void CastleEngine::drawCPCUI(Graphics::Surface *surface) {
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index 0f2b3807ea5..ed7e0356e00 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -12,6 +12,7 @@ MODULE_OBJS := \
games/castle/castle.o \
games/castle/amiga.o \
games/castle/atari.o \
+ games/castle/ay.music.o \
games/castle/c64.o \
games/castle/cpc.o \
games/castle/dos.o \
Commit: 6e0926588cce897514cd7ac4d014fa5e5d7383ee
https://github.com/scummvm/scummvm/commit/6e0926588cce897514cd7ac4d014fa5e5d7383ee
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-25T14:08:39+02:00
Commit Message:
FREESCAPE: initial code to load castle c64
Changed paths:
engines/freescape/detection.cpp
engines/freescape/games/castle/c64.cpp
engines/freescape/games/castle/castle.cpp
engines/freescape/games/castle/castle.h
diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index 4bc79e2f4e1..cf29d171bdf 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -930,7 +930,7 @@ static const ADGameDescription gameDescriptions[] = {
{
"castlemaster",
"",
- AD_ENTRY1s("CASTLEMASTER.C64.DATA", "d433af0fc854d91fb22f986e274d809b", 51198),
+ AD_ENTRY1s("CASTLEMASTER.C64.DATA", "d433af0fc854d91fb22f986e274d809b", 51201),
Common::EN_ANY,
Common::kPlatformC64,
ADGF_UNSTABLE | GF_C64_TAPE,
diff --git a/engines/freescape/games/castle/c64.cpp b/engines/freescape/games/castle/c64.cpp
index 41bb61c9bf0..d213c55b45e 100644
--- a/engines/freescape/games/castle/c64.cpp
+++ b/engines/freescape/games/castle/c64.cpp
@@ -20,6 +20,7 @@
*/
#include "common/file.h"
+#include "graphics/managed_surface.h"
#include "freescape/freescape.h"
#include "freescape/games/castle/castle.h"
@@ -27,12 +28,273 @@
namespace Freescape {
+enum {
+ kCastleC64DatabaseOffset = 0x9951,
+ kCastleC64RuntimeDemoPointerOffset = 0x42,
+ kCastleC64RuntimeAreaTableOffset = 0x4f,
+ kCastleC64CompactDemoPointerOffset = 0x3e,
+ kCastleC64CompactAreaTableOffset = 0x4b,
+ kCastleC64BackgroundColor = 0x00,
+ kCastleC64ScreenHighNibbleColor = 0x0c,
+ kCastleC64ColorRamColor = 0x01
+};
+
+struct CastleC64Repeat {
+ uint16 offset;
+ byte count;
+ byte value;
+};
+
+const uint16 kCastleC64DatabaseSkips[] = {
+ 0x01fc, 0x02fc, 0x05fa, 0x08f9, 0x09f9, 0x0af9, 0x0bf4, 0x0cf0,
+ 0x0deb, 0x0fe9, 0x10e5, 0x11e3, 0x13e1, 0x14de, 0x18dd, 0x19db,
+ 0x1cd9, 0x1ed7, 0x20cf, 0x21c8
+};
+
+const CastleC64Repeat kCastleC64DatabaseRepeats[] = {
+ { 0x0006, 4, 0x00 }, { 0x0009, 4, 0xff }, { 0x0010, 4, 0x55 }, { 0x001b, 4, 0xaa },
+ { 0x02dd, 4, 0x02 }, { 0x0327, 4, 0x00 }, { 0x0347, 4, 0x01 }, { 0x0355, 4, 0x00 },
+ { 0x05fe, 4, 0x00 }, { 0x0610, 4, 0x01 }, { 0x0936, 4, 0x00 }, { 0x0aca, 4, 0x00 },
+ { 0x0b58, 6, 0x00 }, { 0x0bd9, 6, 0x00 }, { 0x0c5a, 4, 0x01 }, { 0x0c69, 4, 0x01 },
+ { 0x0c78, 4, 0x02 }, { 0x0c87, 4, 0x02 }, { 0x0c95, 4, 0x00 }, { 0x0d43, 6, 0x00 },
+ { 0x0d9e, 6, 0x00 }, { 0x0ec9, 6, 0x00 }, { 0x0fea, 5, 0x00 }, { 0x1085, 6, 0x00 },
+ { 0x1163, 6, 0x00 }, { 0x127e, 6, 0x00 }, { 0x140b, 4, 0x06 }, { 0x1497, 6, 0x00 },
+ { 0x1543, 5, 0x00 }, { 0x1910, 6, 0x00 }, { 0x1a1b, 6, 0x00 }, { 0x1da9, 6, 0x00 },
+ { 0x1edb, 6, 0x00 }, { 0x1ee1, 6, 0x00 }, { 0x1ee7, 6, 0x00 }, { 0x20d0, 4, 0x00 },
+ { 0x2128, 6, 0x00 }, { 0x217e, 6, 0x00 }, { 0x21c5, 4, 0x00 }, { 0x2250, 6, 0x00 },
+ { 0x2255, 113, 0x00 }
+};
+
+uint16 readCastleC64Uint16LE(const Common::Array<byte> &data, uint32 offset) {
+ if (offset + 1 >= data.size())
+ error("Castle C64 database pointer read out of range at 0x%x", offset);
+
+ return data[offset] | (data[offset + 1] << 8);
+}
+
+Common::Array<byte> normalizeCastleC64Database(Common::SeekableReadStream *file) {
+ file->seek(kCastleC64DatabaseOffset);
+ if (file->pos() != kCastleC64DatabaseOffset)
+ error("Unable to seek to Castle C64 database at 0x%x", kCastleC64DatabaseOffset);
+ if (file->size() <= kCastleC64DatabaseOffset)
+ error("Castle C64 database file is too short");
+
+ uint32 rawSize = file->size() - kCastleC64DatabaseOffset;
+ Common::Array<byte> raw;
+ raw.resize(rawSize);
+ if (file->read(&raw[0], rawSize) != rawSize)
+ error("Unable to read Castle C64 database");
+ if (raw.size() < 3)
+ error("Castle C64 database is too short");
+
+ const uint16 decodedSize = readCastleC64Uint16LE(raw, 1);
+ Common::Array<byte> decoded;
+ uint32 sourceOffset = 0;
+ uint skipIndex = 0;
+ uint repeatIndex = 0;
+
+ while (decoded.size() < decodedSize) {
+ if (sourceOffset >= raw.size())
+ error("Castle C64 database normalization ran out of source data");
+
+ if (skipIndex < ARRAYSIZE(kCastleC64DatabaseSkips) && sourceOffset == kCastleC64DatabaseSkips[skipIndex]) {
+ sourceOffset++;
+ skipIndex++;
+ continue;
+ }
+
+ if (repeatIndex < ARRAYSIZE(kCastleC64DatabaseRepeats) && sourceOffset == kCastleC64DatabaseRepeats[repeatIndex].offset) {
+ const CastleC64Repeat &repeat = kCastleC64DatabaseRepeats[repeatIndex];
+ if (sourceOffset + 2 >= raw.size() || raw[sourceOffset + 1] != repeat.count || raw[sourceOffset + 2] != repeat.value)
+ error("Castle C64 database repeat mismatch at 0x%x", sourceOffset);
+
+ for (uint i = 0; i < repeat.count && decoded.size() < decodedSize; i++)
+ decoded.push_back(repeat.value);
+
+ sourceOffset += 3;
+ repeatIndex++;
+ continue;
+ }
+
+ decoded.push_back(raw[sourceOffset++]);
+ }
+
+ if (skipIndex != ARRAYSIZE(kCastleC64DatabaseSkips) || repeatIndex != ARRAYSIZE(kCastleC64DatabaseRepeats))
+ error("Castle C64 database normalization did not consume all relocation entries");
+
+ debugC(1, kFreescapeDebugParser, "Castle C64 normalized database: 0x%x -> 0x%x bytes", sourceOffset, decodedSize);
+ return decoded;
+}
+
+class CastleC64DatabaseReadStream : public Common::SeekableReadStream {
+public:
+ CastleC64DatabaseReadStream(const Common::Array<byte> &data) : _data(data), _pos(0), _eos(false), _colorMapRead(false) {
+ if (_data.size() < kCastleC64RuntimeAreaTableOffset)
+ error("Castle C64 normalized database is too short");
+
+ for (uint i = 0; i < 4; i++)
+ _compactPointerBytes[i] = _data[kCastleC64RuntimeDemoPointerOffset + i];
+
+ byte areaCount = _data[0];
+ _areaTable.resize(areaCount * 2);
+ for (uint i = 0; i < areaCount; i++) {
+ uint16 areaOffset = readCastleC64Uint16LE(_data, kCastleC64RuntimeAreaTableOffset + 2 * i);
+ areaOffset += 4; // The shared C64 parser subtracts four from area pointers.
+ _areaTable[2 * i] = areaOffset & 0xff;
+ _areaTable[2 * i + 1] = areaOffset >> 8;
+ }
+ }
+
+ uint32 read(void *dataPtr, uint32 dataSize) override {
+ if (!dataPtr)
+ return 0;
+
+ if (_pos >= _data.size()) {
+ _eos = true;
+ return 0;
+ }
+
+ if (dataSize > _data.size() - _pos) {
+ dataSize = _data.size() - _pos;
+ _eos = true;
+ }
+
+ byte *dst = (byte *)dataPtr;
+ for (uint32 i = 0; i < dataSize; i++)
+ dst[i] = byteAt(_pos++);
+
+ if (_pos >= kCastleC64RuntimeDemoPointerOffset)
+ _colorMapRead = true;
+
+ return dataSize;
+ }
+
+ bool eos() const override {
+ return _eos;
+ }
+
+ void clearErr() override {
+ _eos = false;
+ }
+
+ int64 pos() const override {
+ return _pos;
+ }
+
+ int64 size() const override {
+ return _data.size();
+ }
+
+ bool seek(int64 offs, int whence = SEEK_SET) override {
+ switch (whence) {
+ case SEEK_END:
+ offs = _data.size() + offs;
+ break;
+ case SEEK_CUR:
+ offs = _pos + offs;
+ break;
+ case SEEK_SET:
+ default:
+ break;
+ }
+
+ if (offs < 0)
+ offs = 0;
+ if (offs > (int64)_data.size())
+ offs = _data.size();
+
+ _pos = offs;
+ _eos = false;
+ return true;
+ }
+
+private:
+ byte byteAt(uint32 offset) const {
+ if (_colorMapRead && offset >= kCastleC64CompactDemoPointerOffset && offset < kCastleC64CompactDemoPointerOffset + 4)
+ return _compactPointerBytes[offset - kCastleC64CompactDemoPointerOffset];
+
+ if (offset >= kCastleC64CompactAreaTableOffset && offset < kCastleC64CompactAreaTableOffset + _areaTable.size())
+ return _areaTable[offset - kCastleC64CompactAreaTableOffset];
+
+ return _data[offset];
+ }
+
+ const Common::Array<byte> &_data;
+ uint32 _pos;
+ bool _eos;
+ bool _colorMapRead;
+ byte _compactPointerBytes[4];
+ Common::Array<byte> _areaTable;
+};
+
void CastleEngine::initC64() {
_viewArea = Common::Rect(32, 32, 288, 136);
}
extern byte kC64Palette[16][3];
+void CastleEngine::loadMessagesC64(Common::SeekableReadStream *file, int offset, int number) {
+ file->seek(offset);
+ debugC(1, kFreescapeDebugParser, "String table:");
+
+ for (int i = 0; i < number; i++) {
+ Common::String message;
+ while (true) {
+ byte c = file->readByte();
+ if (c <= 1)
+ break;
+ if (c < 0x20)
+ continue;
+ if (c > 0xf0)
+ c = ' ';
+ message += c;
+ }
+
+ _messagesList.push_back(message);
+ debugC(1, kFreescapeDebugParser, "'%s'", _messagesList[i].c_str());
+ }
+ debugC(1, kFreescapeDebugParser, "End of messages at %" PRIx64, file->pos());
+}
+
+void CastleEngine::loadRiddlesC64(Common::SeekableReadStream *file, int offset, int number) {
+ file->seek(offset);
+
+ for (int i = 0; i < number; i++) {
+ Riddle riddle;
+ riddle._origin = Common::Point(40, 33);
+
+ int numberLines = file->readByte();
+ debugC(1, kFreescapeDebugParser, "c64 riddle %d number of lines: %d", i, numberLines);
+
+ for (int j = 0; j < numberLines; j++) {
+ int8 x = (int8)file->readByte();
+ int8 y = (int8)file->readByte();
+ int size = file->readByte();
+
+ if (size == 0xff)
+ continue;
+
+ file->readByte(); // color/control byte
+ Common::String message;
+ int chars = 0;
+ while (chars < size) {
+ byte c = file->readByte();
+ if (c <= 1 || c < 0x20 || c > 0xf0)
+ continue;
+ message += c;
+ chars++;
+ }
+
+ debugC(1, kFreescapeDebugParser, "'%s' with offset: %d, %d", message.c_str(), x, y);
+ riddle._lines.push_back(RiddleText(x, y, message));
+ }
+
+ _riddleList.push_back(riddle);
+ }
+
+ debugC(1, kFreescapeDebugParser, "End of C64 riddles at %" PRIx64, file->pos());
+}
+
void CastleEngine::loadAssetsC64FullGame() {
Common::File file;
file.open("castlemaster.c64.data");
@@ -40,12 +302,36 @@ void CastleEngine::loadAssetsC64FullGame() {
if (!file.isOpen())
error("Failed to open castlemaster.c64.data");
- loadMessagesVariableSize(&file, 0x13a9, 75);
- // TODO: riddles need C64-specific parsing (embedded control bytes differ from CPC)
- //loadRiddles(&file, 0x1811, 9);
- load8bitBinary(&file, 0x9951, 16);
+ loadMessagesC64(&file, 0x13a9, 75);
+ loadRiddlesC64(&file, 0x1823, 9);
+ Common::Array<byte> database = normalizeCastleC64Database(&file);
+ CastleC64DatabaseReadStream databaseStream(database);
+ load8bitBinary(&databaseStream, 0, 16);
+
+ for (uint i = 0; i < database[0]; i++) {
+ uint16 areaOffset = readCastleC64Uint16LE(database, kCastleC64RuntimeAreaTableOffset + 2 * i);
+ if (areaOffset + 6 >= database.size())
+ error("Castle C64 area color read out of range at 0x%x", areaOffset);
+
+ byte areaID = database[areaOffset + 2];
+ if (!_areaMap.contains(areaID))
+ continue;
+
+ Area *area = _areaMap[areaID];
+ // Original C64 code sets the multicolor bitmap colors as follows:
+ // $2435 -> $d020/$d021, $2436 -> screen high nibble,
+ // area byte +6 -> screen low nibble, and $2438 -> color RAM.
+ area->_usualBackgroundColor = kCastleC64BackgroundColor;
+ area->_underFireBackgroundColor = kCastleC64ScreenHighNibbleColor;
+ area->_paperColor = database[areaOffset + 6] & 0x0f;
+ area->_inkColor = kCastleC64ColorRamColor;
+ debugC(1, kFreescapeDebugParser, "Castle C64 area %d colors: background=%d screen1=%d screen2=%d colorRAM=%d", areaID, area->_usualBackgroundColor, area->_underFireBackgroundColor, area->_paperColor, area->_inkColor);
+ }
+
+ _border = new Graphics::ManagedSurface();
+ _border->create(_screenW, _screenH, _gfx->_texturePixelFormat);
+ _border->fillRect(Common::Rect(0, 0, _screenW, _screenH), _gfx->_texturePixelFormat.ARGBToColor(0xff, 0, 0, 0));
- // TODO: extract border from game data or add bundled image
// TODO: title screen is in BASIC loader (file 009) - not yet extracted
}
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index bcde8ece90e..5b315b9c117 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -608,7 +608,7 @@ void CastleEngine::gotoArea(uint16 areaID, int entranceID) {
// Ignore sky/ground fields
_gfx->_keyColor = 0;
_gfx->clearColorPairArray();
- if (isCPC())
+ if (isCPC() || isC64())
_gfx->fillColorPairArray();
swapPalette(areaID);
@@ -1545,7 +1545,7 @@ void CastleEngine::loadAssets() {
_outOfReachMessage = _messagesList[7];
_noEffectMessage = _messagesList[8];
- if (!isAmiga() && !isAtariST() && !isCPC()) {
+ if (!isAmiga() && !isAtariST() && !isCPC() && !isC64()) {
Graphics::Surface *tmp;
tmp = loadBundledImage("castle_gate", !isDOS());
_gameOverBackgroundFrame = new Graphics::ManagedSurface;
@@ -2095,6 +2095,9 @@ void CastleEngine::borderScreen() {
delete surface;
}
+ if (isC64())
+ return;
+
if (!isCastleMaster2())
selectCharacterScreen();
}
@@ -2120,7 +2123,7 @@ void CastleEngine::selectCharacterScreen() {
surface->create(_screenW, _screenH, _gfx->_texturePixelFormat);
surface->fillRect(_fullscreenViewArea, color);
- if (isSpectrum() || isCPC() || isAmiga()) {
+ if (isSpectrum() || isCPC() || isC64() || isAmiga()) {
if (_language == Common::ES_ESP) {
// No accent in "prÃncipe" since it is not supported by the font
lines.push_back(centerAndPadString("*******************", 21));
@@ -2197,17 +2200,23 @@ void CastleEngine::selectCharacterScreen() {
CursorMan.showMouse(true);
// Calculate tap/click rectangles from actual rendered text positions.
- // lines[5] = prince, lines[6] = princess for ZX/CPC.
+ // lines[5] = prince, lines[6] = princess for 8-bit text menus.
// For DOS, use riddle text line positions.
Common::Rect princeSelector, princessSelector;
- if (isSpectrum() || isCPC() || isAmiga()) {
+ if (isSpectrum() || isCPC() || isC64() || isAmiga()) {
int x = _viewArea.left + 3;
int lineHeight = 12; // Castle Master line spacing in drawStringsInSurface
int princeY = _viewArea.top + 3 + 5 * lineHeight;
int princessY = _viewArea.top + 3 + 6 * lineHeight;
- // Use the full padded line (what's actually rendered on screen)
- princeSelector = _font.getBoundingBox(lines[5], x, princeY);
- princessSelector = _font.getBoundingBox(lines[6], x, princessY);
+ if (_fontLoaded) {
+ // Use the full padded line (what's actually rendered on screen)
+ princeSelector = _font.getBoundingBox(lines[5], x, princeY);
+ princessSelector = _font.getBoundingBox(lines[6], x, princessY);
+ } else {
+ int selectorWidth = 21 * 9;
+ princeSelector = Common::Rect(x, princeY, x + selectorWidth, princeY + lineHeight);
+ princessSelector = Common::Rect(x, princessY, x + selectorWidth, princessY + lineHeight);
+ }
} else {
// DOS: text comes from _riddleList[21], calculate from actual riddle line positions
Common::Array<RiddleText> selectMessage = _riddleList[21]._lines;
@@ -2343,6 +2352,9 @@ void CastleEngine::drawDroppingGate(Graphics::Surface *surface) {
if (_droppingGateStartTicks <= 0)
return;
+ if (!_gameOverBackgroundFrame)
+ return;
+
uint32 keyColor = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0x24, 0xA5);
int duration = 60;
int ticks = _ticks - _droppingGateStartTicks;
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index 4cd85376d44..04b9224e26e 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -193,6 +193,8 @@ private:
Common::SeekableReadStream *decryptFile(const Common::Path &filename);
Common::SeekableReadStream *decompressAtari(const Common::Path &filename);
void loadRiddles(Common::SeekableReadStream *file, int offset, int number);
+ void loadMessagesC64(Common::SeekableReadStream *file, int offset, int number);
+ void loadRiddlesC64(Common::SeekableReadStream *file, int offset, int number);
void loadDOSFonts(Common::SeekableReadStream *file, int pos);
void drawFullscreenRiddleAndWait(uint16 riddle);
void drawFullscreenEndGameAndWait();
Commit: bb7693b60e2e78ce6a0b08e5a55412faf08cae99
https://github.com/scummvm/scummvm/commit/bb7693b60e2e78ce6a0b08e5a55412faf08cae99
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-25T14:08:39+02:00
Commit Message:
FREESCAPE: namespace/static cleanup
Changed paths:
engines/freescape/debugger.cpp
engines/freescape/detection.cpp
engines/freescape/games/castle/amiga.cpp
engines/freescape/games/castle/ay.music.cpp
engines/freescape/games/castle/castle.musicdata.h
engines/freescape/games/castle/opl.music.cpp
engines/freescape/games/dark/amiga.cpp
engines/freescape/games/dark/c64.cpp
engines/freescape/games/dark/c64.music.cpp
engines/freescape/games/dark/c64.sfx.cpp
engines/freescape/games/eclipse/atari.music.cpp
engines/freescape/games/eclipse/c64.music.cpp
engines/freescape/games/eclipse/c64.sfx.cpp
engines/freescape/games/eclipse/eclipse.cpp
engines/freescape/games/eclipse/eclipse.musicdata.h
engines/freescape/games/eclipse/opl.music.cpp
engines/freescape/gfx_opengl_shaders.cpp
engines/freescape/loaders/8bitBinaryLoader.cpp
engines/freescape/metaengine.cpp
engines/freescape/wb.cpp
diff --git a/engines/freescape/debugger.cpp b/engines/freescape/debugger.cpp
index d9cac80416c..b2d6f402182 100644
--- a/engines/freescape/debugger.cpp
+++ b/engines/freescape/debugger.cpp
@@ -218,7 +218,7 @@ bool Debugger::cmdShowOcclusion(int argc, const char **argv) {
return true;
}
-static const char *objectTypeNames[] = {
+const char *const objectTypeNames[] = {
"Entrance",
"Cube",
"Sensor",
diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index cf29d171bdf..1504dc2b502 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -27,7 +27,7 @@
namespace Freescape {
-static const PlainGameDescriptor freescapeGames[] = {
+const PlainGameDescriptor freescapeGames[] = {
{"3dkit", "3D Kit Game"},
{"driller", "Driller"},
{"spacestationoblivion", "Space Station Oblivion"},
@@ -39,7 +39,7 @@ static const PlainGameDescriptor freescapeGames[] = {
{0, 0}
};
-static const ADGameDescription gameDescriptions[] = {
+const ADGameDescription gameDescriptions[] = {
// Original Freescape games
// Driller
{
@@ -1233,7 +1233,7 @@ static const ADGameDescription gameDescriptions[] = {
};
} // End of namespace Freescape
-static const DebugChannelDef debugFlagList[] = {
+const DebugChannelDef debugFlagList[] = {
{Freescape::kFreescapeDebugMove, "move", ""},
{Freescape::kFreescapeDebugParser, "parser", ""},
{Freescape::kFreescapeDebugCode, "code", ""},
diff --git a/engines/freescape/games/castle/amiga.cpp b/engines/freescape/games/castle/amiga.cpp
index 5b6bfaeda54..2f91fe9956d 100644
--- a/engines/freescape/games/castle/amiga.cpp
+++ b/engines/freescape/games/castle/amiga.cpp
@@ -84,7 +84,7 @@ struct CastleIntroLayout {
int palScale; // 0: palette channels are 4-bit (Amiga); 1: 3-bit (Atari ST), scale up
};
-static const CastleIntroLayout kAmigaIntroLayout = {
+const CastleIntroLayout kAmigaIntroLayout = {
0x247c, 0x2d64, 0x4664, // scrollPlaneA/B, staticPlane
0x5ba4, 0x5c0c, // fillBands, overlay
0x14ff4, 0x1ac74, // logo, foreground
@@ -96,7 +96,7 @@ static const CastleIntroLayout kAmigaIntroLayout = {
0
};
-static const CastleIntroLayout kAtariIntroLayout = {
+const CastleIntroLayout kAtariIntroLayout = {
0x22, 0x90a, 0x220a,
0x374a, 0x37b2,
0x12b9a, 0x1881a,
diff --git a/engines/freescape/games/castle/ay.music.cpp b/engines/freescape/games/castle/ay.music.cpp
index 3b609995404..803ba2b502e 100644
--- a/engines/freescape/games/castle/ay.music.cpp
+++ b/engines/freescape/games/castle/ay.music.cpp
@@ -30,8 +30,6 @@ using namespace Freescape::CastleMusicData;
namespace Freescape {
-namespace {
-
// AY-3-8910 period table (95 entries, same note numbering as the Castle OPL
// table and Total Eclipse AY player). AY clock = 1MHz.
const uint16 kAYPeriods[] = {
@@ -96,8 +94,6 @@ uint16 applySIDFrequencyOffset(uint16 basePeriod, int16 frequencyOffset) {
return CLIP<uint32>(period, 1, 4095);
}
-} // namespace
-
void CastleAYMusicPlayer::ChannelState::reset(const byte *channelOrderList) {
orderList = channelOrderList;
orderPosition = 0;
diff --git a/engines/freescape/games/castle/castle.musicdata.h b/engines/freescape/games/castle/castle.musicdata.h
index f79e4ed646b..d1e1913b16a 100644
--- a/engines/freescape/games/castle/castle.musicdata.h
+++ b/engines/freescape/games/castle/castle.musicdata.h
@@ -35,8 +35,8 @@
namespace Freescape {
namespace CastleMusicData {
-static const byte kOrderTranspose = 0x80;
-static const byte kOrderEnd = 0xFF;
+const byte kOrderTranspose = 0x80;
+const byte kOrderEnd = 0xFF;
struct InstrumentData {
byte arpeggio;
@@ -50,7 +50,7 @@ struct InstrumentData {
byte effect;
};
-static const InstrumentData kInstruments[] = {
+const InstrumentData kInstruments[] = {
{ 0x00, 0x05, 0x41, 0x0A, 0x0F, 0x02, 0x7F, 0x00, 0x00 },
{ 0x00, 0x05, 0x81, 0x25, 0x07, 0x02, 0x7F, 0x00, 0x05 },
{ 0x00, 0x05, 0x00, 0x1B, 0x07, 0x02, 0x7F, 0x00, 0x05 },
@@ -61,34 +61,34 @@ static const InstrumentData kInstruments[] = {
{ 0x00, 0x08, 0x41, 0x13, 0x0F, 0x02, 0x7F, 0x00, 0x05 }
};
-static const byte kOrderList0[] = {
+const byte kOrderList0[] = {
0x80, 0xFB, 0x00, 0x03, 0x03, 0x03, 0x03, 0x0A, 0x0A, 0x0D, 0x0D, 0x0D, 0x12, 0x12, 0x12, 0x12,
0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x1A, 0xFF
};
-static const byte kOrderList1[] = {
+const byte kOrderList1[] = {
0x80, 0xFB, 0x01, 0x04, 0x06, 0x08, 0x08, 0x0B, 0x0B, 0x0E, 0x11, 0x11, 0x13, 0x13, 0x16, 0x13,
0x13, 0x16, 0x18, 0x18, 0x16, 0x1A, 0x00, 0x01, 0x1B, 0xFF
};
-static const byte kOrderList2[] = {
+const byte kOrderList2[] = {
0x80, 0xFB, 0x02, 0x05, 0x07, 0x09, 0x09, 0x0C, 0x0C, 0x0F, 0x10, 0x10, 0x14, 0x15, 0x17, 0x14,
0x15, 0x17, 0x19, 0x19, 0x17, 0x1B, 0x01, 0x00, 0x1C, 0xFF
};
-static const byte *const kChannelOrderLists[] = {
+const byte *const kChannelOrderLists[] = {
kOrderList0,
kOrderList1,
kOrderList2
};
-static const uint16 kPatternOffsets[] = {
+const uint16 kPatternOffsets[] = {
0, 52, 104, 124, 158, 164, 170, 182, 196, 216, 238, 288,
311, 334, 390, 426, 460, 512, 553, 634, 690, 724, 778, 833,
873, 927, 937, 943, 949
};
-static const byte kPatternData[] = {
+const byte kPatternData[] = {
0x86, 0x1B, 0x18, 0x1E, 0x18, 0x19, 0x18, 0x1B, 0x18, 0x16, 0x18, 0x19, 0x18, 0x1D, 0x18, 0x19,
0x18, 0x86, 0x1B, 0x0C, 0x27, 0x0C, 0x20, 0x0C, 0x22, 0x0C, 0x23, 0x0C, 0x1E, 0x0C, 0x20, 0x0C,
0x22, 0x0C, 0x86, 0x1B, 0x0C, 0x27, 0x0C, 0x20, 0x0C, 0x22, 0x0C, 0x23, 0x0C, 0x1E, 0x0C, 0x20,
diff --git a/engines/freescape/games/castle/opl.music.cpp b/engines/freescape/games/castle/opl.music.cpp
index 1ead6651c6d..88f7a379673 100644
--- a/engines/freescape/games/castle/opl.music.cpp
+++ b/engines/freescape/games/castle/opl.music.cpp
@@ -30,9 +30,7 @@ using namespace Freescape::CastleMusicData;
namespace Freescape {
-namespace {
-
-struct OPLBasePatch {
+struct CastleOPLBasePatch {
byte modChar;
byte carChar;
byte modLevel;
@@ -80,7 +78,7 @@ const uint16 kOPLFreqs[] = {
const byte kOPLModOffset[] = { 0x00, 0x01, 0x02 };
const byte kOPLCarOffset[] = { 0x03, 0x04, 0x05 };
-const OPLBasePatch kOPLBasePatches[] = {
+const CastleOPLBasePatch kOPLBasePatches[] = {
{ 0x21, 0x21, 0x22, 0x03, 0xF2, 0xF3, 0x74, 0x56, 0x00, 0x00, 0x04 },
{ 0x02, 0x01, 0x1E, 0x00, 0xE4, 0xF2, 0x64, 0x46, 0x00, 0x01, 0x06 },
{ 0x31, 0x21, 0x26, 0x06, 0xC3, 0xE3, 0x64, 0x47, 0x00, 0x00, 0x02 },
@@ -101,8 +99,6 @@ uint16 scaleDuration(byte duration) {
return MAX<uint16>(1, duration);
}
-} // namespace
-
// ============================================================================
// ChannelState
// ============================================================================
@@ -203,7 +199,7 @@ void CastleOPLMusicPlayer::setOPLInstrument(int channel, byte instrument) {
if (!_opl)
return;
- const OPLBasePatch &patch = kOPLBasePatches[instrument % ARRAYSIZE(kOPLBasePatches)];
+ const CastleOPLBasePatch &patch = kOPLBasePatches[instrument % ARRAYSIZE(kOPLBasePatches)];
byte mod = kOPLModOffset[channel];
byte car = kOPLCarOffset[channel];
diff --git a/engines/freescape/games/dark/amiga.cpp b/engines/freescape/games/dark/amiga.cpp
index 95bef14ff53..4a2728682c8 100644
--- a/engines/freescape/games/dark/amiga.cpp
+++ b/engines/freescape/games/dark/amiga.cpp
@@ -29,8 +29,6 @@
namespace Freescape {
-namespace {
-
const int kAmigaGemdosHeaderSize = 0x1C;
int amigaProgToFile(int address) {
@@ -109,8 +107,6 @@ void decodeMaskedAmigaSprite(Common::SeekableReadStream *file, Graphics::Managed
}
}
-} // namespace
-
void DarkEngine::loadAssetsAmigaFullGame() {
Common::File file;
file.open("0.drk");
diff --git a/engines/freescape/games/dark/c64.cpp b/engines/freescape/games/dark/c64.cpp
index 25acf870909..656ce2ae407 100644
--- a/engines/freescape/games/dark/c64.cpp
+++ b/engines/freescape/games/dark/c64.cpp
@@ -30,10 +30,10 @@ namespace Freescape {
extern byte kC64Palette[16][3];
-static const byte kDarkC64CompassMask[] = {0xfe, 0xfa, 0x69, 0xf5, 0xea};
-static const byte kDarkC64CompassColor1[] = {0xfc, 0x1f, 0xf1, 0xcb, 0x3b};
-static const byte kDarkC64CompassColor2[] = {0x0b, 0x0c, 0x0c, 0x0f, 0x0c};
-static const byte kDarkC64ModeIndicatorPalette[2][3][4] = {
+const byte kDarkC64CompassMask[] = {0xfe, 0xfa, 0x69, 0xf5, 0xea};
+const byte kDarkC64CompassColor1[] = {0xfc, 0x1f, 0xf1, 0xcb, 0x3b};
+const byte kDarkC64CompassColor2[] = {0x0b, 0x0c, 0x0c, 0x0f, 0x0c};
+const byte kDarkC64ModeIndicatorPalette[2][3][4] = {
{{0, 11, 15, 1}, {0, 11, 15, 2}, {0, 15, 1, 11}},
{{0, 15, 12, 11}, {0, 15, 12, 11}, {0, 15, 2, 11}}
};
@@ -50,7 +50,7 @@ static int getDarkC64CompassTarget(float yaw) {
return target;
}
-static const byte *getDarkC64IndicatorSprite(const byte *spriteData, byte pointer) {
+const byte *getDarkC64IndicatorSprite(const byte *spriteData, byte pointer) {
assert(pointer >= 3 && pointer <= 10);
return spriteData + (pointer - 3) * 64;
}
diff --git a/engines/freescape/games/dark/c64.music.cpp b/engines/freescape/games/dark/c64.music.cpp
index 1943217b897..cfc1b81ceeb 100644
--- a/engines/freescape/games/dark/c64.music.cpp
+++ b/engines/freescape/games/dark/c64.music.cpp
@@ -32,7 +32,7 @@ namespace Freescape {
// Frequency table: hi bytes at $0F38, lo bytes at $0F97 (95 entries each)
// Index 0 = rest (freq 0), indices 1-94 = notes spanning 8 octaves
-static const uint8 kFreqHi[96] = {
+const uint8 kFreqHi[96] = {
0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x03, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x06, 0x06,
0x06, 0x07, 0x07, 0x08, 0x08, 0x09, 0x09, 0x0A, 0x0A, 0x0B, 0x0C, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
@@ -42,7 +42,7 @@ static const uint8 kFreqHi[96] = {
0x00 // safety padding
};
-static const uint8 kFreqLo[96] = {
+const uint8 kFreqLo[96] = {
0x00, 0x23, 0x34, 0x46, 0x5A, 0x6E, 0x84, 0x9B, 0xB3, 0xCD, 0xE9, 0x06, 0x25, 0x45, 0x68, 0x8C,
0xB3, 0xDC, 0x08, 0x36, 0x67, 0x9B, 0xD2, 0x0C, 0x49, 0x8B, 0xD0, 0x19, 0x67, 0xB9, 0x10, 0x6C,
0xCE, 0x35, 0xA3, 0x17, 0x93, 0x15, 0x9F, 0x3C, 0xCD, 0x72, 0x20, 0xD8, 0x9C, 0x6B, 0x46, 0x2F,
@@ -55,7 +55,7 @@ static const uint8 kFreqLo[96] = {
// Instrument table at $1010 (18 instruments x 8 bytes)
// Bytes: ctrl, AD, SR, initPW, vib/env mode, pwMod, autoFx, flags
// Instruments 16-17 are stored between $1090-$109F (past the nominal 16-entry table)
-static const uint8 kInstruments[18 * 8] = {
+const uint8 kInstruments[18 * 8] = {
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0: Pulse (silence)
0x41, 0x0C, 0xDC, 0x02, 0x00, 0x33, 0x00, 0x08, // 1: Pulse+G
0x41, 0xDC, 0xDD, 0x00, 0x00, 0x15, 0x00, 0x80, // 2: Pulse+G
@@ -77,56 +77,56 @@ static const uint8 kInstruments[18 * 8] = {
};
// Auxiliary envelope data tables ($154B and $156B, 16 entries each)
-static const uint8 kEnvData[2][16] = {
+const uint8 kEnvData[2][16] = {
{ 0xFA, 0x01, 0xFF, 0x20, 0x0A, 0x12, 0x04, 0x16, 0x0E, 0x0C, 0x0A, 0x08, 0x06, 0x04, 0x02, 0x00 },
{ 0x10, 0x0A, 0x06, 0x00, 0x04, 0x00, 0x00, 0x00, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00 },
};
// Envelope waveform control tables ($155B and $157B, 16 entries each)
-static const uint8 kEnvControl[2][16] = {
+const uint8 kEnvControl[2][16] = {
{ 0x81, 0x41, 0x81, 0x80, 0x80, 0x80, 0x80, 0x80, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10 },
{ 0x81, 0x41, 0x81, 0x80, 0x40, 0x40, 0x40, 0x40, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10 },
};
// Arpeggio semitone intervals ($1591)
-static const uint8 kArpIntervals[8] = { 3, 4, 5, 7, 8, 9, 10, 12 };
+const uint8 kArpIntervals[8] = { 3, 4, 5, 7, 8, 9, 10, 12 };
// SID register base offset per channel
-static const int kSIDOffset[3] = { 0, 7, 14 };
+const int kSIDOffset[3] = { 0, 7, 14 };
// ---- Pattern data (29 patterns from $122D-$1542) ----
-static const uint8 kPattern00[] = { 0xC0, 0xBF, 0x00, 0xFF };
-static const uint8 kPattern01[] = { 0xF5, 0xC2, 0xBF, 0x10, 0x10, 0xFF };
-static const uint8 kPattern02[] = { 0xC1, 0x8F, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0xFF };
-static const uint8 kPattern03[] = { 0xC4, 0x80, 0x34, 0x2F, 0x37, 0x36, 0x2F, 0x37, 0x34, 0x2F, 0x34, 0x2F, 0x37, 0x36, 0x2F, 0x37, 0x34, 0x2F, 0x34, 0x2F, 0x37, 0x36, 0x2F, 0x37, 0x34, 0x2F, 0x34, 0x2F, 0x37, 0x36, 0x2F, 0x37, 0x34, 0x2F, 0xFF };
-static const uint8 kPattern04[] = { 0xC6, 0x80, 0x7A, 0x08, 0x34, 0x36, 0x9D, 0x37, 0xFF };
-static const uint8 kPattern05[] = { 0x81, 0x34, 0x36, 0x34, 0x2F, 0x32, 0x30, 0x2B, 0x91, 0x2F, 0xFF };
-static const uint8 kPattern06[] = { 0x83, 0x34, 0x37, 0x34, 0x8F, 0x3C, 0x83, 0x3B, 0x8F, 0x3A, 0x8F, 0x36, 0xFF };
-static const uint8 kPattern07[] = { 0xC1, 0x81, 0x10, 0x10, 0x12, 0x10, 0x13, 0x10, 0x15, 0x13, 0xFF };
-static const uint8 kPattern08[] = { 0xC5, 0x9F, 0x7D, 0x89, 0x40, 0x7D, 0x91, 0x40, 0x7D, 0xA1, 0x40, 0x7D, 0x91, 0x40, 0xFF };
-static const uint8 kPattern09[] = { 0xC0, 0x9F, 0x00, 0xFF };
-static const uint8 kPattern10[] = { 0xCA, 0x9F, 0x7E, 0x0A, 0x7F, 0x4C, 0x83, 0x1C, 0x23, 0x28, 0x2F, 0x1C, 0x23, 0x28, 0x2F, 0x8F, 0x7F, 0x40, 0x7E, 0x10, 0x9F, 0x7B, 0x40, 0x3B, 0x21, 0x8F, 0x34, 0x40, 0x9F, 0x7B, 0x0D, 0x3B, 0x4C, 0x7F, 0x3C, 0xFF };
-static const uint8 kPattern11[] = { 0xC7, 0xFF };
-static const uint8 kPattern12[] = { 0xC2, 0xBF, 0x10, 0xFF };
-static const uint8 kPattern13[] = { 0xC8, 0x80, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0xFF };
-static const uint8 kPattern14[] = { 0x80, 0x28, 0x34, 0x34, 0x28, 0x34, 0x28, 0x28, 0x34, 0x28, 0x34, 0x34, 0x28, 0x28, 0x34, 0x28, 0x34, 0x28, 0x28, 0x34, 0x34, 0x28, 0x28, 0x34, 0x28, 0x28, 0x34, 0x34, 0x28, 0x34, 0x28, 0x28, 0x34, 0xFF };
-static const uint8 kPattern15[] = { 0xCB, 0x81, 0x54, 0x52, 0x50, 0xC0, 0x00, 0xCB, 0x54, 0x52, 0x50, 0xC0, 0x00, 0xCB, 0x54, 0x52, 0x50, 0xC0, 0x00, 0xCB, 0x54, 0x52, 0xFF };
-static const uint8 kPattern16[] = { 0xC1, 0x87, 0x10, 0x1C, 0x10, 0x1C, 0x10, 0x1C, 0x10, 0x83, 0x1C, 0x81, 0x10, 0x0E, 0x87, 0x0B, 0x17, 0x0B, 0x17, 0x0B, 0x17, 0x0B, 0x83, 0x17, 0x81, 0x0B, 0x0A, 0x87, 0x09, 0x15, 0x09, 0x15, 0x09, 0x15, 0x09, 0x83, 0x15, 0x81, 0x09, 0x0A, 0x87, 0x0B, 0x17, 0x0B, 0x17, 0x0B, 0x17, 0x0B, 0x83, 0x17, 0x81, 0x0B, 0x0E, 0xFF };
-static const uint8 kPattern17[] = { 0xCC, 0xBF, 0x7D, 0x89, 0x40, 0x7C, 0xA2, 0x3E, 0x3C, 0x7D, 0x91, 0x3F, 0xFF };
-static const uint8 kPattern18[] = { 0x83, 0x1C, 0xFF };
-static const uint8 kPattern19[] = { 0xC0, 0x93, 0x00, 0xC1, 0x81, 0x23, 0x83, 0x21, 0x81, 0x1F, 0x83, 0x1E, 0xFF };
-static const uint8 kPattern20[] = { 0xC1, 0x8B, 0x1C, 0x83, 0x23, 0x97, 0x23, 0x83, 0x21, 0x23, 0x21, 0x1F, 0x1E, 0x1F, 0x8B, 0x1E, 0x83, 0x23, 0x9B, 0x23, 0x81, 0x21, 0x23, 0x83, 0x21, 0x1F, 0x1E, 0x1F, 0x8B, 0x21, 0x83, 0x23, 0x93, 0x21, 0x83, 0x21, 0x23, 0x24, 0x26, 0x24, 0x23, 0x21, 0x9B, 0x1E, 0x83, 0x23, 0x93, 0x23, 0xD1, 0x81, 0x30, 0x30, 0xD0, 0x12, 0xD1, 0x30, 0x30, 0xD0, 0x7A, 0x18, 0x12, 0xFF };
-static const uint8 kPattern21[] = { 0xC8, 0xFF };
-static const uint8 kPattern22[] = { 0xC4, 0xFF };
-static const uint8 kPattern23[] = { 0xCF, 0xA3, 0x7B, 0x37, 0x02, 0x36, 0x83, 0x2F, 0x34, 0x36, 0x7B, 0x39, 0x02, 0x37, 0x37, 0x36, 0x34, 0x8B, 0x7B, 0x36, 0x02, 0x34, 0x83, 0x2F, 0x93, 0x2F, 0x83, 0x36, 0x36, 0x37, 0x7B, 0x39, 0x02, 0x37, 0x37, 0x36, 0x7B, 0x34, 0x04, 0x37, 0x8B, 0x34, 0x81, 0x36, 0x37, 0x93, 0x34, 0x83, 0x34, 0x36, 0x37, 0x39, 0x37, 0x36, 0x34, 0xA3, 0x7B, 0x36, 0x04, 0x34, 0xD1, 0x81, 0x30, 0x30, 0xD0, 0x12, 0xC0, 0x85, 0x00, 0xD1, 0x81, 0x30, 0x30, 0xD0, 0x12, 0xC0, 0x83, 0x00, 0xD1, 0x81, 0x30, 0x30, 0xD0, 0x12, 0xFF };
-static const uint8 kPattern24[] = { 0xC1, 0x87, 0x10, 0x85, 0x1C, 0x81, 0x10, 0x83, 0x10, 0x10, 0x81, 0x1C, 0x83, 0x10, 0x81, 0x1C, 0x87, 0x10, 0x85, 0x1C, 0x81, 0x10, 0x83, 0x10, 0x10, 0x81, 0x1C, 0x83, 0x10, 0x81, 0x1C, 0xFF };
-static const uint8 kPattern25[] = { 0xCF, 0x99, 0x7B, 0x34, 0x04, 0x32, 0x85, 0x7B, 0x32, 0x02, 0x34, 0x93, 0x7B, 0x34, 0x04, 0x32, 0x83, 0x7B, 0x37, 0x02, 0x36, 0x81, 0x37, 0x85, 0x7B, 0x32, 0x02, 0x34, 0x93, 0x32, 0x83, 0x32, 0x36, 0x7B, 0x36, 0x02, 0x37, 0x9B, 0x36, 0x83, 0x7B, 0x30, 0x03, 0x32, 0x30, 0x32, 0x81, 0x30, 0x85, 0x7B, 0x30, 0x02, 0x32, 0x93, 0x30, 0x83, 0x30, 0x32, 0x34, 0x8B, 0x7B, 0x36, 0x04, 0x37, 0x83, 0x34, 0x97, 0x7B, 0x36, 0x04, 0x34, 0x81, 0x36, 0x37, 0x36, 0x34, 0x99, 0x7B, 0x36, 0x04, 0x34, 0xD1, 0x81, 0x12, 0x83, 0x12, 0xFF };
-static const uint8 kPattern26[] = { 0xCC, 0xBF, 0x7D, 0x8A, 0x40, 0x7D, 0x91, 0x3F, 0x7D, 0x94, 0x3D, 0x7D, 0x91, 0x3F, 0xFF };
-static const uint8 kPattern27[] = { 0xCF, 0x83, 0x2F, 0x34, 0x91, 0x34, 0x85, 0x7B, 0x34, 0x12, 0x36, 0x2F, 0x81, 0x34, 0x34, 0x36, 0x8D, 0x34, 0x85, 0x7B, 0x33, 0x12, 0x34, 0x93, 0x33, 0x83, 0x33, 0x81, 0x33, 0x85, 0x7B, 0x33, 0x12, 0x34, 0x99, 0x33, 0x81, 0x2F, 0x83, 0x7B, 0x2D, 0x12, 0x2F, 0x8B, 0x2D, 0x83, 0x34, 0x34, 0x8B, 0x7B, 0x37, 0x22, 0x36, 0x93, 0x37, 0x87, 0x7B, 0x34, 0x04, 0x36, 0x83, 0x34, 0xA1, 0x7B, 0x36, 0x12, 0x34, 0xD1, 0x81, 0x1C, 0x8D, 0x1C, 0x81, 0x1C, 0x83, 0x1C, 0x1C, 0x81, 0x1C, 0x1C, 0xFF };
-static const uint8 kPattern28[] = { 0xCF, 0x80, 0x7A, 0x08, 0x34, 0x36, 0x9D, 0x37, 0xD0, 0x83, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0xCF, 0x80, 0x32, 0x34, 0x9D, 0x36, 0xD0, 0x81, 0x12, 0x83, 0x12, 0x12, 0x81, 0x0E, 0x83, 0x12, 0x1C, 0x85, 0x12, 0x81, 0x0E, 0x83, 0x12, 0xCF, 0x80, 0x30, 0x32, 0x9D, 0x34, 0xD1, 0x83, 0x1C, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0x81, 0x18, 0x1C, 0xCF, 0x87, 0x30, 0x80, 0x33, 0x34, 0x9D, 0x36, 0xC0, 0x9B, 0x00, 0xD1, 0x81, 0x18, 0x18, 0xFF };
-
-static const uint8 *kPatterns[29] = {
+const uint8 kPattern00[] = { 0xC0, 0xBF, 0x00, 0xFF };
+const uint8 kPattern01[] = { 0xF5, 0xC2, 0xBF, 0x10, 0x10, 0xFF };
+const uint8 kPattern02[] = { 0xC1, 0x8F, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0xFF };
+const uint8 kPattern03[] = { 0xC4, 0x80, 0x34, 0x2F, 0x37, 0x36, 0x2F, 0x37, 0x34, 0x2F, 0x34, 0x2F, 0x37, 0x36, 0x2F, 0x37, 0x34, 0x2F, 0x34, 0x2F, 0x37, 0x36, 0x2F, 0x37, 0x34, 0x2F, 0x34, 0x2F, 0x37, 0x36, 0x2F, 0x37, 0x34, 0x2F, 0xFF };
+const uint8 kPattern04[] = { 0xC6, 0x80, 0x7A, 0x08, 0x34, 0x36, 0x9D, 0x37, 0xFF };
+const uint8 kPattern05[] = { 0x81, 0x34, 0x36, 0x34, 0x2F, 0x32, 0x30, 0x2B, 0x91, 0x2F, 0xFF };
+const uint8 kPattern06[] = { 0x83, 0x34, 0x37, 0x34, 0x8F, 0x3C, 0x83, 0x3B, 0x8F, 0x3A, 0x8F, 0x36, 0xFF };
+const uint8 kPattern07[] = { 0xC1, 0x81, 0x10, 0x10, 0x12, 0x10, 0x13, 0x10, 0x15, 0x13, 0xFF };
+const uint8 kPattern08[] = { 0xC5, 0x9F, 0x7D, 0x89, 0x40, 0x7D, 0x91, 0x40, 0x7D, 0xA1, 0x40, 0x7D, 0x91, 0x40, 0xFF };
+const uint8 kPattern09[] = { 0xC0, 0x9F, 0x00, 0xFF };
+const uint8 kPattern10[] = { 0xCA, 0x9F, 0x7E, 0x0A, 0x7F, 0x4C, 0x83, 0x1C, 0x23, 0x28, 0x2F, 0x1C, 0x23, 0x28, 0x2F, 0x8F, 0x7F, 0x40, 0x7E, 0x10, 0x9F, 0x7B, 0x40, 0x3B, 0x21, 0x8F, 0x34, 0x40, 0x9F, 0x7B, 0x0D, 0x3B, 0x4C, 0x7F, 0x3C, 0xFF };
+const uint8 kPattern11[] = { 0xC7, 0xFF };
+const uint8 kPattern12[] = { 0xC2, 0xBF, 0x10, 0xFF };
+const uint8 kPattern13[] = { 0xC8, 0x80, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0xFF };
+const uint8 kPattern14[] = { 0x80, 0x28, 0x34, 0x34, 0x28, 0x34, 0x28, 0x28, 0x34, 0x28, 0x34, 0x34, 0x28, 0x28, 0x34, 0x28, 0x34, 0x28, 0x28, 0x34, 0x34, 0x28, 0x28, 0x34, 0x28, 0x28, 0x34, 0x34, 0x28, 0x34, 0x28, 0x28, 0x34, 0xFF };
+const uint8 kPattern15[] = { 0xCB, 0x81, 0x54, 0x52, 0x50, 0xC0, 0x00, 0xCB, 0x54, 0x52, 0x50, 0xC0, 0x00, 0xCB, 0x54, 0x52, 0x50, 0xC0, 0x00, 0xCB, 0x54, 0x52, 0xFF };
+const uint8 kPattern16[] = { 0xC1, 0x87, 0x10, 0x1C, 0x10, 0x1C, 0x10, 0x1C, 0x10, 0x83, 0x1C, 0x81, 0x10, 0x0E, 0x87, 0x0B, 0x17, 0x0B, 0x17, 0x0B, 0x17, 0x0B, 0x83, 0x17, 0x81, 0x0B, 0x0A, 0x87, 0x09, 0x15, 0x09, 0x15, 0x09, 0x15, 0x09, 0x83, 0x15, 0x81, 0x09, 0x0A, 0x87, 0x0B, 0x17, 0x0B, 0x17, 0x0B, 0x17, 0x0B, 0x83, 0x17, 0x81, 0x0B, 0x0E, 0xFF };
+const uint8 kPattern17[] = { 0xCC, 0xBF, 0x7D, 0x89, 0x40, 0x7C, 0xA2, 0x3E, 0x3C, 0x7D, 0x91, 0x3F, 0xFF };
+const uint8 kPattern18[] = { 0x83, 0x1C, 0xFF };
+const uint8 kPattern19[] = { 0xC0, 0x93, 0x00, 0xC1, 0x81, 0x23, 0x83, 0x21, 0x81, 0x1F, 0x83, 0x1E, 0xFF };
+const uint8 kPattern20[] = { 0xC1, 0x8B, 0x1C, 0x83, 0x23, 0x97, 0x23, 0x83, 0x21, 0x23, 0x21, 0x1F, 0x1E, 0x1F, 0x8B, 0x1E, 0x83, 0x23, 0x9B, 0x23, 0x81, 0x21, 0x23, 0x83, 0x21, 0x1F, 0x1E, 0x1F, 0x8B, 0x21, 0x83, 0x23, 0x93, 0x21, 0x83, 0x21, 0x23, 0x24, 0x26, 0x24, 0x23, 0x21, 0x9B, 0x1E, 0x83, 0x23, 0x93, 0x23, 0xD1, 0x81, 0x30, 0x30, 0xD0, 0x12, 0xD1, 0x30, 0x30, 0xD0, 0x7A, 0x18, 0x12, 0xFF };
+const uint8 kPattern21[] = { 0xC8, 0xFF };
+const uint8 kPattern22[] = { 0xC4, 0xFF };
+const uint8 kPattern23[] = { 0xCF, 0xA3, 0x7B, 0x37, 0x02, 0x36, 0x83, 0x2F, 0x34, 0x36, 0x7B, 0x39, 0x02, 0x37, 0x37, 0x36, 0x34, 0x8B, 0x7B, 0x36, 0x02, 0x34, 0x83, 0x2F, 0x93, 0x2F, 0x83, 0x36, 0x36, 0x37, 0x7B, 0x39, 0x02, 0x37, 0x37, 0x36, 0x7B, 0x34, 0x04, 0x37, 0x8B, 0x34, 0x81, 0x36, 0x37, 0x93, 0x34, 0x83, 0x34, 0x36, 0x37, 0x39, 0x37, 0x36, 0x34, 0xA3, 0x7B, 0x36, 0x04, 0x34, 0xD1, 0x81, 0x30, 0x30, 0xD0, 0x12, 0xC0, 0x85, 0x00, 0xD1, 0x81, 0x30, 0x30, 0xD0, 0x12, 0xC0, 0x83, 0x00, 0xD1, 0x81, 0x30, 0x30, 0xD0, 0x12, 0xFF };
+const uint8 kPattern24[] = { 0xC1, 0x87, 0x10, 0x85, 0x1C, 0x81, 0x10, 0x83, 0x10, 0x10, 0x81, 0x1C, 0x83, 0x10, 0x81, 0x1C, 0x87, 0x10, 0x85, 0x1C, 0x81, 0x10, 0x83, 0x10, 0x10, 0x81, 0x1C, 0x83, 0x10, 0x81, 0x1C, 0xFF };
+const uint8 kPattern25[] = { 0xCF, 0x99, 0x7B, 0x34, 0x04, 0x32, 0x85, 0x7B, 0x32, 0x02, 0x34, 0x93, 0x7B, 0x34, 0x04, 0x32, 0x83, 0x7B, 0x37, 0x02, 0x36, 0x81, 0x37, 0x85, 0x7B, 0x32, 0x02, 0x34, 0x93, 0x32, 0x83, 0x32, 0x36, 0x7B, 0x36, 0x02, 0x37, 0x9B, 0x36, 0x83, 0x7B, 0x30, 0x03, 0x32, 0x30, 0x32, 0x81, 0x30, 0x85, 0x7B, 0x30, 0x02, 0x32, 0x93, 0x30, 0x83, 0x30, 0x32, 0x34, 0x8B, 0x7B, 0x36, 0x04, 0x37, 0x83, 0x34, 0x97, 0x7B, 0x36, 0x04, 0x34, 0x81, 0x36, 0x37, 0x36, 0x34, 0x99, 0x7B, 0x36, 0x04, 0x34, 0xD1, 0x81, 0x12, 0x83, 0x12, 0xFF };
+const uint8 kPattern26[] = { 0xCC, 0xBF, 0x7D, 0x8A, 0x40, 0x7D, 0x91, 0x3F, 0x7D, 0x94, 0x3D, 0x7D, 0x91, 0x3F, 0xFF };
+const uint8 kPattern27[] = { 0xCF, 0x83, 0x2F, 0x34, 0x91, 0x34, 0x85, 0x7B, 0x34, 0x12, 0x36, 0x2F, 0x81, 0x34, 0x34, 0x36, 0x8D, 0x34, 0x85, 0x7B, 0x33, 0x12, 0x34, 0x93, 0x33, 0x83, 0x33, 0x81, 0x33, 0x85, 0x7B, 0x33, 0x12, 0x34, 0x99, 0x33, 0x81, 0x2F, 0x83, 0x7B, 0x2D, 0x12, 0x2F, 0x8B, 0x2D, 0x83, 0x34, 0x34, 0x8B, 0x7B, 0x37, 0x22, 0x36, 0x93, 0x37, 0x87, 0x7B, 0x34, 0x04, 0x36, 0x83, 0x34, 0xA1, 0x7B, 0x36, 0x12, 0x34, 0xD1, 0x81, 0x1C, 0x8D, 0x1C, 0x81, 0x1C, 0x83, 0x1C, 0x1C, 0x81, 0x1C, 0x1C, 0xFF };
+const uint8 kPattern28[] = { 0xCF, 0x80, 0x7A, 0x08, 0x34, 0x36, 0x9D, 0x37, 0xD0, 0x83, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0xCF, 0x80, 0x32, 0x34, 0x9D, 0x36, 0xD0, 0x81, 0x12, 0x83, 0x12, 0x12, 0x81, 0x0E, 0x83, 0x12, 0x1C, 0x85, 0x12, 0x81, 0x0E, 0x83, 0x12, 0xCF, 0x80, 0x30, 0x32, 0x9D, 0x34, 0xD1, 0x83, 0x1C, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0x81, 0x18, 0x1C, 0xCF, 0x87, 0x30, 0x80, 0x33, 0x34, 0x9D, 0x36, 0xC0, 0x9B, 0x00, 0xD1, 0x81, 0x18, 0x18, 0xFF };
+
+const uint8 *const kPatterns[29] = {
kPattern00, kPattern01, kPattern02, kPattern03, kPattern04,
kPattern05, kPattern06, kPattern07, kPattern08, kPattern09,
kPattern10, kPattern11, kPattern12, kPattern13, kPattern14,
@@ -137,7 +137,7 @@ static const uint8 *kPatterns[29] = {
// ---- Order lists (song 0, 3 channels) ----
-static const uint8 kOrderList0[] = {
+const uint8 kOrderList0[] = {
0xC2, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x02, 0x02, 0x01, 0x10, 0x10, 0x10, 0x10, 0x18, 0xC9, 0x18, 0xC7, 0x18, 0xC9,
0x18, 0xC2, 0x18, 0xC9, 0x18, 0xC7, 0x18, 0xC9, 0x18, 0xC2, 0x18, 0xC9, 0x18, 0xC7, 0x18,
@@ -146,7 +146,7 @@ static const uint8 kOrderList0[] = {
0x01, 0xFF
};
-static const uint8 kOrderList1[] = {
+const uint8 kOrderList1[] = {
0xCE, 0x09, 0x01, 0x0C, 0x09, 0x01, 0x01, 0x01, 0x09, 0x01, 0x01, 0x09, 0x00, 0x03, 0x03,
0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0xC2,
0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
@@ -157,7 +157,7 @@ static const uint8 kOrderList1[] = {
0xFF
};
-static const uint8 kOrderList2[] = {
+const uint8 kOrderList2[] = {
0xC2, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x09, 0x04, 0x05, 0x09, 0x04, 0x06, 0x09, 0x08, 0x08,
0x08, 0x08, 0x0B, 0x05, 0x06, 0x09, 0xAA, 0x04, 0xC2, 0x09, 0xB6, 0x04, 0xC2, 0x09, 0x0D,
0x09, 0xC4, 0x0D, 0xC2, 0x09, 0xC5, 0x0D, 0xC2, 0x09, 0xC7, 0x0D, 0xC2, 0x09, 0xCE, 0x0D,
@@ -171,7 +171,7 @@ static const uint8 kOrderList2[] = {
0x0E, 0xC2, 0x1C, 0x02, 0x02, 0xFF
};
-static const uint8 *kOrderLists[3] = { kOrderList0, kOrderList1, kOrderList2 };
+const uint8 *const kOrderLists[3] = { kOrderList0, kOrderList1, kOrderList2 };
// ============================================================
// Implementation
diff --git a/engines/freescape/games/dark/c64.sfx.cpp b/engines/freescape/games/dark/c64.sfx.cpp
index 06e22df9b20..0be1ad19ef2 100644
--- a/engines/freescape/games/dark/c64.sfx.cpp
+++ b/engines/freescape/games/dark/c64.sfx.cpp
@@ -30,7 +30,7 @@ namespace Freescape {
// 25 SFX entries extracted from dark2.prg at $C802 (address $C802-$CBEA).
// Each entry is 40 bytes in the original 6502 format.
// See SOUND_ANALYSIS.md for full documentation.
-static const C64SFXData kC64SFXData[25] = {
+const C64SFXData kC64SFXData[25] = {
// SFX #1: Shoot (Noise, highâsilence)
{2, 1, 0,
{0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
diff --git a/engines/freescape/games/eclipse/atari.music.cpp b/engines/freescape/games/eclipse/atari.music.cpp
index ca6d0dee517..2efab460e70 100644
--- a/engines/freescape/games/eclipse/atari.music.cpp
+++ b/engines/freescape/games/eclipse/atari.music.cpp
@@ -48,16 +48,16 @@
namespace Freescape {
// TEXT-relative offsets for data tables within TEMUSIC.ST
-static const uint32 kTEPeriodTableOffset = 0x0B24; // 96 x uint16 BE
-static const uint32 kTEArpeggioIntervalsOffset = 0x0CC8; // 8 bytes
-static const uint32 kTEInstrumentTableOffset = 0x0D60; // 12 x 8 bytes
-static const uint32 kTESongTableOffset = 0x0DC0; // 2 songs x 3 ch x uint32 BE
-static const uint32 kTEPatternPtrTableOffset = 0x0DCC; // up to 31 x uint32 BE
-
-static const int kTENumChannels = 3;
-static const int kTENumPeriods = 96;
-static const int kTENumInstruments = 12;
-static const int kTEMaxPatterns = 31;
+const uint32 kTEPeriodTableOffset = 0x0B24; // 96 x uint16 BE
+const uint32 kTEArpeggioIntervalsOffset = 0x0CC8; // 8 bytes
+const uint32 kTEInstrumentTableOffset = 0x0D60; // 12 x 8 bytes
+const uint32 kTESongTableOffset = 0x0DC0; // 2 songs x 3 ch x uint32 BE
+const uint32 kTEPatternPtrTableOffset = 0x0DCC; // up to 31 x uint32 BE
+
+const int kTENumChannels = 3;
+const int kTENumPeriods = 96;
+const int kTENumInstruments = 12;
+const int kTEMaxPatterns = 31;
class EclipseAtariMusicPlayer : public MusicPlayer {
public:
diff --git a/engines/freescape/games/eclipse/c64.music.cpp b/engines/freescape/games/eclipse/c64.music.cpp
index 09acca9092c..98de75c5604 100644
--- a/engines/freescape/games/eclipse/c64.music.cpp
+++ b/engines/freescape/games/eclipse/c64.music.cpp
@@ -26,7 +26,7 @@
namespace Freescape {
-static const int kSIDOffset[] = {0, 7, 14};
+const int kSIDOffset[] = {0, 7, 14};
void EclipseC64MusicPlayer::ChannelState::reset() {
orderAddr = 0;
diff --git a/engines/freescape/games/eclipse/c64.sfx.cpp b/engines/freescape/games/eclipse/c64.sfx.cpp
index e16208289af..64a55e003cd 100644
--- a/engines/freescape/games/eclipse/c64.sfx.cpp
+++ b/engines/freescape/games/eclipse/c64.sfx.cpp
@@ -29,7 +29,7 @@ namespace Freescape {
// 21 SFX entries extracted from totec2.prg at $C802 (address $C802-$CB49).
// Each entry is 40 bytes. Same descriptor format as Dark Side C64.
-static const C64SFXData kEclipseSFXData[21] = {
+const C64SFXData kEclipseSFXData[21] = {
// SFX #1: Noise, 2 notes, repeat 1
{2, 1, 0,
{0x00, 0x44, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
diff --git a/engines/freescape/games/eclipse/eclipse.cpp b/engines/freescape/games/eclipse/eclipse.cpp
index 3262c69883b..95c756865f8 100644
--- a/engines/freescape/games/eclipse/eclipse.cpp
+++ b/engines/freescape/games/eclipse/eclipse.cpp
@@ -42,7 +42,7 @@
namespace Freescape {
// Wally Beben table offsets for Total Eclipse Amiga TEMUSIC.AM
-static const WBTableOffsets kEclipseAmigaMusicOffsets = {
+const WBTableOffsets kEclipseAmigaMusicOffsets = {
0x0ACA, // periodTable
0x0C5E, // samplePtrTable
0x0CA6, // instrumentTable
diff --git a/engines/freescape/games/eclipse/eclipse.musicdata.h b/engines/freescape/games/eclipse/eclipse.musicdata.h
index 788f99a1cba..54db8b2e11f 100644
--- a/engines/freescape/games/eclipse/eclipse.musicdata.h
+++ b/engines/freescape/games/eclipse/eclipse.musicdata.h
@@ -36,7 +36,7 @@ namespace EclipseMusicData {
// 12 instruments, 6 bytes each (SID-only fields PW and PWM stripped)
// Format: ctrl, AD, SR, vibrato, autoArp, flags
-static const byte kInstruments[] = {
+const byte kInstruments[] = {
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // 0
0x41, 0x42, 0x24, 0x23, 0x00, 0x04, // 1
0x11, 0x8A, 0xAC, 0x64, 0x00, 0x00, // 2
@@ -50,34 +50,34 @@ static const byte kInstruments[] = {
0x21, 0x4A, 0x2A, 0x64, 0x00, 0x04, // 10
0x41, 0x6A, 0x6B, 0x00, 0x80, 0x04, // 11
};
-static const byte kInstrumentSize = 6;
-static const byte kInstrumentCount = 12;
+const byte kInstrumentSize = 6;
+const byte kInstrumentCount = 12;
// SID pulse-width data kept alongside the shared instruments so the OPL port
// can retain some of the original timbre motion.
-static const byte kPulseWidthInit[] = {
+const byte kPulseWidthInit[] = {
0x00, 0x02, 0x00, 0x64, 0x20, 0x20, 0x22, 0x43, 0x44, 0x30, 0x80, 0x41
};
-static const byte kPulseWidthMod[] = {
+const byte kPulseWidthMod[] = {
0x00, 0x0F, 0x0B, 0x2F, 0x08, 0x01, 0x00, 0x00, 0x22, 0x00, 0x22, 0x40
};
-static const byte kOrderList0[] = {
+const byte kOrderList0[] = {
0xE0, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x0B, 0x0B, 0x0B, 0x0B, 0x01, 0x01,
0x0B, 0x10, 0x0B, 0x0B, 0x01, 0x10, 0x01, 0x01, 0x13, 0x13, 0x13, 0x13, 0x0B, 0x0B, 0x13, 0x13,
0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x0B, 0x0B, 0x0B, 0x0B, 0x01, 0x01, 0x01, 0x01, 0x0B, 0x10,
0x0B, 0x0B, 0x01, 0x13, 0x01, 0x01, 0x13, 0x13, 0x13, 0x13, 0xFF
};
-static const byte kOrderList1[] = {
+const byte kOrderList1[] = {
0xE0, 0x00, 0x00, 0x00, 0x00, 0x06, 0x07, 0x07, 0x07, 0x07, 0x09, 0x09, 0x07, 0x07, 0xE0, 0x08,
0x08, 0x08, 0x08, 0x18, 0x0A, 0x0A, 0x18, 0x0C, 0x0D, 0x0C, 0x0D, 0x14, 0x14, 0x14, 0x14, 0x16,
0x17, 0x0C, 0x0D, 0xD4, 0x0C, 0x0D, 0xE0, 0x05, 0x05, 0x05, 0x05, 0x17, 0x0C, 0x0D, 0xD4, 0x0C,
0x0D, 0xEC, 0x08, 0x08, 0x08, 0x08, 0xFF
};
-static const byte kOrderList2[] = {
+const byte kOrderList2[] = {
0xE0, 0x05, 0x05, 0x05, 0x05, 0xEC, 0x08, 0xE0, 0x02, 0x02, 0x03, 0x02, 0x04, 0xEC, 0x04, 0xE0,
0x0E, 0x0F, 0x11, 0x12, 0xEC, 0x02, 0x02, 0x03, 0x02, 0xE0, 0x15, 0x1B, 0x19, 0x1B, 0x1A, 0x1C,
0x19, 0x1C, 0x1A, 0x1D, 0x1E, 0x07, 0x00, 0x07, 0x00, 0x04, 0xEC, 0x04, 0xE0, 0x0E, 0x0F, 0x11,
@@ -85,14 +85,14 @@ static const byte kOrderList2[] = {
};
// Pattern offset table (31 patterns into kPatternData)
-static const uint16 kPatternOffsets[] = {
+const uint16 kPatternOffsets[] = {
0, 4, 40, 65, 94, 122, 138, 148,
173, 178, 192, 200, 235, 249, 263, 295,
334, 369, 406, 450, 485, 490, 526, 562,
564, 566, 618, 670, 672, 674, 718
};
-static const byte kPatternData[] = {
+const byte kPatternData[] = {
0xC0, 0xBF, 0x00, 0xFF, 0xF3, 0xC1, 0x83, 0x1A, 0x21, 0x21, 0x1A, 0x1A, 0x21, 0x1A, 0x21, 0x1A,
0x21, 0x21, 0x1A, 0x1A, 0x21, 0x1A, 0x21, 0x1A, 0x21, 0x21, 0x1A, 0x1A, 0x21, 0x1A, 0x21, 0x1A,
0x21, 0x21, 0x1A, 0x1A, 0x21, 0x1A, 0x21, 0xFF, 0xC2, 0x97, 0x30, 0x87, 0x2D, 0x97, 0x30, 0x87,
@@ -143,7 +143,7 @@ static const byte kPatternData[] = {
0x9F, 0x39, 0xFF
};
-static const byte kArpeggioIntervals[] = { 0x03, 0x04, 0x05, 0x07, 0x08, 0x09, 0x0A, 0x0C };
+const byte kArpeggioIntervals[] = { 0x03, 0x04, 0x05, 0x07, 0x08, 0x09, 0x0A, 0x0C };
} // namespace EclipseMusicData
} // namespace Freescape
diff --git a/engines/freescape/games/eclipse/opl.music.cpp b/engines/freescape/games/eclipse/opl.music.cpp
index 7953f8afc1f..4e76d0957c8 100644
--- a/engines/freescape/games/eclipse/opl.music.cpp
+++ b/engines/freescape/games/eclipse/opl.music.cpp
@@ -30,9 +30,7 @@ using namespace Freescape::EclipseMusicData;
namespace Freescape {
-namespace {
-
-struct OPLBasePatch {
+struct EclipseOPLBasePatch {
byte modChar;
byte carChar;
byte modLevel;
@@ -85,7 +83,7 @@ const uint16 kOPLFreqs[] = {
const byte kOPLModOffset[] = { 0x00, 0x01, 0x02 };
const byte kOPLCarOffset[] = { 0x03, 0x04, 0x05 };
-const OPLBasePatch kOPLBasePatches[] = {
+const EclipseOPLBasePatch kOPLBasePatches[] = {
// 0: silent
{ 0x00, 0x00, 0x3F, 0x3F, 0x00, 0x00, 0x00 },
// 1: triangle - soft additive sine
@@ -129,8 +127,6 @@ uint16 decodePulseWidth(byte packed) {
return ((packed & 0x0F) << 8) | (packed & 0xF0);
}
-} // namespace
-
// ============================================================================
// ChannelState
// ============================================================================
@@ -265,7 +261,7 @@ void EclipseOPLMusicPlayer::setOPLInstrument(int channel, byte instrumentOffset)
patchIdx = 0;
byte ctrl = kInstruments[instrumentOffset + 0];
- const OPLBasePatch &patch = kOPLBasePatches[getWaveformFamily(ctrl)];
+ const EclipseOPLBasePatch &patch = kOPLBasePatches[getWaveformFamily(ctrl)];
byte mod = kOPLModOffset[channel];
byte car = kOPLCarOffset[channel];
diff --git a/engines/freescape/gfx_opengl_shaders.cpp b/engines/freescape/gfx_opengl_shaders.cpp
index bcb5aa16881..579a9cc2b4e 100644
--- a/engines/freescape/gfx_opengl_shaders.cpp
+++ b/engines/freescape/gfx_opengl_shaders.cpp
@@ -32,7 +32,7 @@
namespace Freescape {
-static const GLfloat bitmapVertices[] = {
+const GLfloat bitmapVertices[] = {
// XS YT
0.0, 0.0,
1.0, 0.0,
diff --git a/engines/freescape/loaders/8bitBinaryLoader.cpp b/engines/freescape/loaders/8bitBinaryLoader.cpp
index b6849a87146..0e21095df66 100644
--- a/engines/freescape/loaders/8bitBinaryLoader.cpp
+++ b/engines/freescape/loaders/8bitBinaryLoader.cpp
@@ -598,7 +598,7 @@ Object *FreescapeEngine::load8bitObject(Common::SeekableReadStream *file) {
// Unreachable
}
-static const char *eclipseRoomName[] = {
+const char *const eclipseRoomName[] = {
"* SAHARA",
"HORAKHTY",
"NEPHTHYS",
@@ -610,7 +610,7 @@ static const char *eclipseRoomName[] = {
"????????"
};
-static const char *eclipse2RoomName[] = {
+const char *const eclipse2RoomName[] = {
"\" SAHARA",
"ENTRANCE",
"\" SPHINX",
@@ -1251,7 +1251,6 @@ Common::SeekableReadStream *FreescapeEngine::decryptFileAmigaAtari(const Common:
}
-namespace {
// A simple implementation of memmem, which is a non-standard GNU extension.
const void *local_memmem(const void *haystack, size_t haystack_len, const void *needle, size_t needle_len) {
if (needle_len == 0) {
@@ -1268,7 +1267,6 @@ const void *local_memmem(const void *haystack, size_t haystack_len, const void *
}
return nullptr;
}
-} // namespace
Common::SeekableReadStream *FreescapeEngine::decryptFileAtariVirtualWorlds(const Common::Path &filename) {
Common::File file;
diff --git a/engines/freescape/metaengine.cpp b/engines/freescape/metaengine.cpp
index a3927d4b81f..5d08dc4d5ee 100644
--- a/engines/freescape/metaengine.cpp
+++ b/engines/freescape/metaengine.cpp
@@ -34,7 +34,7 @@
#include "freescape/detection.h"
-static const ADExtraGuiOptionsMap optionsList[] = {
+const ADExtraGuiOptionsMap optionsList[] = {
{
GAMEOPTION_PRERECORDED_SOUNDS,
{
diff --git a/engines/freescape/wb.cpp b/engines/freescape/wb.cpp
index 2e7d8dc8469..c3816cb5252 100644
--- a/engines/freescape/wb.cpp
+++ b/engines/freescape/wb.cpp
@@ -89,7 +89,7 @@ byte buildArpeggioTable(const byte intervals[8], byte mask, byte *outTable, byte
// Default TEXT-relative offsets for Dark Side HDSMUSIC.AM
// All addresses verified against disassembly of the 68000 code.
-static const WBTableOffsets kDarkSideOffsets = {
+const WBTableOffsets kDarkSideOffsets = {
0x0AAE, // periodTable: 48 x uint16 BE
0x0C42, // samplePtrTable: 16 x uint32 BE
0x0C82, // instrumentTable: 16 x 8 bytes
@@ -100,7 +100,7 @@ static const WBTableOffsets kDarkSideOffsets = {
16, 16, 10 // numSamples, numInstruments, numEnvelopes
};
-static const uint32 kMaxPatternEntries = 128;
+const uint32 kMaxPatternEntries = 128;
class WallyBebenStream : public Audio::Paula {
public:
Commit: 3402c7783913a6be91ef5eda181b4ba4a393ab25
https://github.com/scummvm/scummvm/commit/3402c7783913a6be91ef5eda181b4ba4a393ab25
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-25T14:08:39+02:00
Commit Message:
FREESCAPE: initial implemnetation of castle c64 music
Changed paths:
A engines/freescape/games/castle/c64.music.cpp
A engines/freescape/games/castle/c64.music.h
engines/freescape/games/castle/c64.cpp
engines/freescape/module.mk
diff --git a/engines/freescape/games/castle/c64.cpp b/engines/freescape/games/castle/c64.cpp
index d213c55b45e..89f5f0fb85b 100644
--- a/engines/freescape/games/castle/c64.cpp
+++ b/engines/freescape/games/castle/c64.cpp
@@ -23,6 +23,7 @@
#include "graphics/managed_surface.h"
#include "freescape/freescape.h"
+#include "freescape/games/castle/c64.music.h"
#include "freescape/games/castle/castle.h"
#include "freescape/language/8bitDetokeniser.h"
@@ -332,6 +333,8 @@ void CastleEngine::loadAssetsC64FullGame() {
_border->create(_screenW, _screenH, _gfx->_texturePixelFormat);
_border->fillRect(Common::Rect(0, 0, _screenW, _screenH), _gfx->_texturePixelFormat.ARGBToColor(0xff, 0, 0, 0));
+ _playerMusic = new CastleC64MusicPlayer();
+
// TODO: title screen is in BASIC loader (file 009) - not yet extracted
}
diff --git a/engines/freescape/games/castle/c64.music.cpp b/engines/freescape/games/castle/c64.music.cpp
new file mode 100644
index 00000000000..59323600fdd
--- /dev/null
+++ b/engines/freescape/games/castle/c64.music.cpp
@@ -0,0 +1,396 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "engines/freescape/games/castle/c64.music.h"
+
+#include "common/textconsole.h"
+#include "common/util.h"
+#include "freescape/freescape.h"
+#include "freescape/games/castle/castle.musicdata.h"
+#include "freescape/sid.h"
+
+using namespace Freescape::CastleMusicData;
+
+namespace Freescape {
+
+const int kCastleSIDVoiceOffset[] = { 0, 7, 14 };
+const int8 kCastleVibrato10[] = { 0, 20, 10, -10, -20, 15, 5, -20 };
+const int8 kCastleVibrato18[] = { 0, 3, -3, 3, -3, 3, -3, 3 };
+const int8 kCastleVibrato20[] = { 0, -100, -100, -100, -100, -100, -100, -100 };
+
+// Original Castle Master C64 SID frequency tables at $065A/$06BA.
+const byte kCastleSIDFreqLo[] = {
+ 0x16, 0x27, 0x38, 0x4B, 0x5F, 0x73, 0x8A, 0xA1, 0xBA, 0xD4, 0xF0, 0x0E, 0x2D, 0x4E, 0x71, 0x96,
+ 0xBD, 0xE7, 0x13, 0x42, 0x74, 0xA9, 0xE0, 0x1B, 0x5A, 0x9B, 0xE2, 0x2C, 0x7B, 0xCE, 0x27, 0x85,
+ 0xE8, 0x51, 0xC1, 0x37, 0xB4, 0x37, 0xC4, 0x57, 0xF5, 0x9C, 0x4E, 0x09, 0xD0, 0xA3, 0x82, 0x6E,
+ 0x68, 0x6E, 0x88, 0xAF, 0xEB, 0x39, 0x9C, 0x13, 0xA1, 0x46, 0x04, 0xDC, 0xD0, 0xDC, 0x10, 0x5E,
+ 0xD6, 0x72, 0x38, 0x26, 0x42, 0x8C, 0x08, 0xB8, 0xA0, 0xB8, 0x20, 0xBC, 0xAC, 0xE4, 0x70, 0x4C,
+ 0x84, 0x18, 0x10, 0x70, 0x40, 0x70, 0x40, 0x78, 0x58, 0xC8, 0xE0, 0x98, 0x08, 0x30, 0x20, 0x2E
+};
+
+const byte kCastleSIDFreqHi[] = {
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x03, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x06, 0x06,
+ 0x06, 0x07, 0x07, 0x08, 0x08, 0x09, 0x09, 0x0A, 0x0A, 0x0B, 0x0C, 0x0D, 0x0D, 0x0E, 0x0F, 0x10,
+ 0x11, 0x12, 0x13, 0x14, 0x15, 0x17, 0x18, 0x1A, 0x1B, 0x1D, 0x1F, 0x20, 0x22, 0x24, 0x27, 0x29,
+ 0x2B, 0x2E, 0x31, 0x34, 0x37, 0x3A, 0x3E, 0x41, 0x45, 0x49, 0x4E, 0x52, 0x57, 0x5C, 0x62, 0x68,
+ 0x6E, 0x75, 0x7C, 0x83, 0x8B, 0x93, 0x9C, 0xA5, 0xAF, 0xB9, 0xC4, 0xD0, 0xDD, 0xEA, 0xF8, 0xFD
+};
+
+const int8 *getCastleVibratoTable(byte vibrato) {
+ switch (vibrato) {
+ case 0x10:
+ return kCastleVibrato10;
+ case 0x18:
+ return kCastleVibrato18;
+ case 0x20:
+ return kCastleVibrato20;
+ default:
+ return nullptr;
+ }
+}
+
+void CastleC64MusicPlayer::ChannelState::reset(const byte *channelOrderList) {
+ orderList = channelOrderList;
+ orderPosition = 0;
+ patternIndex = 0;
+ patternDataOffset = 0;
+ patternOffset = 0;
+ delay = 0;
+ instrument = 0;
+ transpose = 0;
+ currentNote = 0;
+ baseFrequency = 0;
+ frequencyOffset = 0;
+ vibratoStep = 1;
+ vibratoReverse = false;
+ control = 0;
+ active = false;
+}
+
+CastleC64MusicPlayer::CastleC64MusicPlayer()
+ : _sid(nullptr),
+ _musicActive(false),
+ _tick(0) {
+}
+
+CastleC64MusicPlayer::~CastleC64MusicPlayer() {
+ stopMusic();
+}
+
+void CastleC64MusicPlayer::initSID() {
+ destroySID();
+
+ _sid = SID::Config::create(SID::Config::kSidPAL);
+ if (!_sid || !_sid->init()) {
+ warning("CastleC64MusicPlayer: Failed to create SID emulator");
+ destroySID();
+ return;
+ }
+
+ _sid->start(new Common::Functor0Mem<void, CastleC64MusicPlayer>(this, &CastleC64MusicPlayer::onTimer), 50);
+}
+
+void CastleC64MusicPlayer::destroySID() {
+ if (_sid) {
+ _sid->stop();
+ delete _sid;
+ _sid = nullptr;
+ }
+}
+
+void CastleC64MusicPlayer::sidWrite(int reg, byte data) {
+ if (_sid)
+ _sid->writeReg(reg, data);
+}
+
+void CastleC64MusicPlayer::startMusic() {
+ stopMusic();
+ initSID();
+ if (!_sid)
+ return;
+
+ setupSong();
+}
+
+void CastleC64MusicPlayer::stopMusic() {
+ _musicActive = false;
+ silenceAll();
+ destroySID();
+}
+
+bool CastleC64MusicPlayer::isPlaying() const {
+ return _musicActive;
+}
+
+void CastleC64MusicPlayer::silenceAll() {
+ for (int i = 0; i <= kSIDVolume; i++)
+ sidWrite(i, 0);
+}
+
+void CastleC64MusicPlayer::setupSong() {
+ silenceAll();
+ _tick = 0;
+
+ sidWrite(kSIDFilterLo, 0x00);
+ sidWrite(kSIDFilterHi, 0x00);
+ sidWrite(kSIDFilterCtrl, 0x00);
+ sidWrite(kSIDVolume, 0x0F);
+
+ for (int i = 0; i < kChannelCount; i++) {
+ _channels[i].reset(kChannelOrderLists[i]);
+ loadNextPattern(i);
+ }
+
+ _musicActive = true;
+}
+
+void CastleC64MusicPlayer::onTimer() {
+ if (!_musicActive)
+ return;
+
+ for (int i = 0; i < kChannelCount; i++) {
+ if (_channels[i].delay > 0) {
+ if (_channels[i].active) {
+ const InstrumentData &instrument = kInstruments[_channels[i].instrument % ARRAYSIZE(kInstruments)];
+ if (_channels[i].delay == instrument.gateOffTime)
+ gateOff(i);
+ else
+ applyFrameEffects(i);
+ }
+
+ _channels[i].delay--;
+ continue;
+ }
+
+ parseCommands(i);
+ }
+
+ _tick++;
+}
+
+void CastleC64MusicPlayer::loadNextPattern(int channel) {
+ ChannelState &c = _channels[channel];
+ int safety = 128;
+
+ while (safety-- > 0) {
+ byte value = c.orderList[c.orderPosition++];
+
+ if (value == kOrderEnd) {
+ c.orderPosition = 0;
+ continue;
+ }
+
+ if (value == kOrderTranspose) {
+ byte transpose = c.orderList[c.orderPosition++];
+ c.transpose = transpose >= 0x80 ? transpose - 0x100 : transpose;
+ continue;
+ }
+
+ if (value < ARRAYSIZE(kPatternOffsets)) {
+ c.patternIndex = value;
+ c.patternDataOffset = kPatternOffsets[value];
+ c.patternOffset = 0;
+ debugC(1, kFreescapeDebugMedia,
+ "Castle SID t=%u ch=%d order=%u PATTERN %u dataOffset=%u transpose=%d",
+ (uint)_tick, channel, c.orderPosition - 1, c.patternIndex, c.patternDataOffset, c.transpose);
+ return;
+ }
+ }
+
+ c.patternDataOffset = 0;
+ c.patternOffset = 0;
+}
+
+byte CastleC64MusicPlayer::readPatternByte(int channel) {
+ ChannelState &c = _channels[channel];
+ uint16 offset = c.patternDataOffset + c.patternOffset;
+ if (offset >= ARRAYSIZE(kPatternData)) {
+ loadNextPattern(channel);
+ offset = c.patternDataOffset + c.patternOffset;
+ }
+
+ c.patternOffset++;
+ return kPatternData[offset];
+}
+
+void CastleC64MusicPlayer::parseCommands(int channel) {
+ ChannelState &c = _channels[channel];
+ int safety = 128;
+
+ while (safety-- > 0) {
+ byte command = readPatternByte(channel);
+ uint16 commandOffset = c.patternOffset - 1;
+
+ if (command == 0xFF) {
+ loadNextPattern(channel);
+ continue;
+ }
+
+ if (command >= 0x80 && command < 0x90) {
+ c.instrument = command & 0x0F;
+ debugC(1, kFreescapeDebugMedia,
+ "Castle SID t=%u ch=%d pat=%u pos=%u INST %u",
+ (uint)_tick, channel, c.patternIndex, commandOffset, c.instrument);
+ continue;
+ }
+
+ if (command == 0xA0) {
+ byte duration = readPatternByte(channel);
+ debugC(1, kFreescapeDebugMedia,
+ "Castle SID t=%u ch=%d pat=%u pos=%u REST dur=%u inst=%u",
+ (uint)_tick, channel, c.patternIndex, commandOffset, duration, c.instrument);
+ rest(channel);
+ c.delay = MAX<uint16>(1, duration);
+ return;
+ }
+
+ if (command >= 0x90 && command < 0xC0) {
+ debugC(1, kFreescapeDebugMedia,
+ "Castle SID t=%u ch=%d pat=%u pos=%u EFFECT $%02x skipped inst=%u",
+ (uint)_tick, channel, c.patternIndex, commandOffset, command, c.instrument);
+ continue;
+ }
+
+ if (command >= 0xC0) {
+ byte slideDuration = readPatternByte(channel);
+ byte slideLo = readPatternByte(channel);
+ byte slideHi = readPatternByte(channel);
+ debugC(1, kFreescapeDebugMedia,
+ "Castle SID t=%u ch=%d pat=%u pos=%u SLIDE $%02x dur=%u delta=$%02x%02x skipped inst=%u",
+ (uint)_tick, channel, c.patternIndex, commandOffset, command, slideDuration, slideHi, slideLo, c.instrument);
+ continue;
+ }
+
+ byte duration = readPatternByte(channel);
+ if (command == 0) {
+ debugC(1, kFreescapeDebugMedia,
+ "Castle SID t=%u ch=%d pat=%u pos=%u OFF dur=%u inst=%u",
+ (uint)_tick, channel, c.patternIndex, commandOffset, duration, c.instrument);
+ rest(channel);
+ } else {
+ int effectiveNote = command + c.transpose + 20;
+ noteOn(channel, command);
+ debugC(1, kFreescapeDebugMedia,
+ "Castle SID t=%u ch=%d pat=%u pos=%u NOTE raw=$%02x effective=%d inst=%u transpose=%d dur=%u freq=$%04x ctrl=$%02x",
+ (uint)_tick, channel, c.patternIndex, commandOffset, command, effectiveNote, c.instrument,
+ c.transpose, duration, c.baseFrequency, c.control);
+ }
+ c.delay = MAX<uint16>(1, duration);
+ return;
+ }
+
+ debugC(1, kFreescapeDebugMedia,
+ "Castle SID t=%u ch=%d pat=%u parser safety stop inst=%u",
+ (uint)_tick, channel, c.patternIndex, c.instrument);
+ rest(channel);
+ c.delay = 12;
+}
+
+void CastleC64MusicPlayer::noteOn(int channel, byte note) {
+ ChannelState &c = _channels[channel];
+ const InstrumentData &instrument = kInstruments[c.instrument % ARRAYSIZE(kInstruments)];
+ int voiceOffset = kCastleSIDVoiceOffset[channel];
+
+ int effectiveNote = note + c.transpose + 20;
+ c.currentNote = CLIP<int>(effectiveNote, 0, kMaxNote);
+ c.baseFrequency = noteToSIDFrequency(effectiveNote);
+ c.frequencyOffset = 0;
+ c.vibratoStep = 1;
+ c.vibratoReverse = false;
+ c.control = sidControlForInstrument(c.instrument);
+ c.active = true;
+
+ sidWrite(voiceOffset + kSIDV1Ctrl, 0x00);
+ writeFrequency(channel, c.baseFrequency);
+ sidWrite(voiceOffset + kSIDV1PwLo, 0x00);
+ sidWrite(voiceOffset + kSIDV1PwHi, instrument.pulseWidth & 0x0F);
+ sidWrite(voiceOffset + kSIDV1AD, instrument.attackDecay);
+ sidWrite(voiceOffset + kSIDV1SR, instrument.sustainRelease);
+ sidWrite(voiceOffset + kSIDV1Ctrl, c.control);
+}
+
+void CastleC64MusicPlayer::rest(int channel) {
+ ChannelState &c = _channels[channel];
+ int voiceOffset = kCastleSIDVoiceOffset[channel];
+
+ c.active = false;
+ c.currentNote = 0;
+ c.baseFrequency = 0;
+ c.frequencyOffset = 0;
+ c.control = 0;
+ sidWrite(voiceOffset + kSIDV1Ctrl, 0x00);
+}
+
+void CastleC64MusicPlayer::gateOff(int channel) {
+ ChannelState &c = _channels[channel];
+ int voiceOffset = kCastleSIDVoiceOffset[channel];
+
+ c.control &= 0xFE;
+ sidWrite(voiceOffset + kSIDV1Ctrl, c.control);
+}
+
+void CastleC64MusicPlayer::writeFrequency(int channel, uint16 frequency) {
+ int voiceOffset = kCastleSIDVoiceOffset[channel];
+ sidWrite(voiceOffset + kSIDV1FreqLo, frequency & 0xFF);
+ sidWrite(voiceOffset + kSIDV1FreqHi, frequency >> 8);
+}
+
+uint16 CastleC64MusicPlayer::noteToSIDFrequency(int note) const {
+ note = CLIP<int>(note, 0, ARRAYSIZE(kCastleSIDFreqLo) - 1);
+ return kCastleSIDFreqLo[note] | (kCastleSIDFreqHi[note] << 8);
+}
+
+void CastleC64MusicPlayer::applyFrameEffects(int channel) {
+ ChannelState &c = _channels[channel];
+ if (!c.active || c.currentNote == 0 || c.baseFrequency == 0)
+ return;
+
+ const InstrumentData &instrument = kInstruments[c.instrument % ARRAYSIZE(kInstruments)];
+ const int8 *vibratoTable = getCastleVibratoTable(instrument.vibrato);
+ if (!vibratoTable)
+ return;
+
+ int8 sidDelta = vibratoTable[c.vibratoStep & 0x07];
+ c.frequencyOffset += c.vibratoReverse ? -sidDelta : sidDelta;
+ c.vibratoStep++;
+ if (c.vibratoStep >= 8) {
+ c.vibratoStep = 1;
+ c.vibratoReverse = !c.vibratoReverse;
+ }
+
+ int32 frequency = (int32)c.baseFrequency + c.frequencyOffset;
+ writeFrequency(channel, CLIP<int32>(frequency, 0, 0xFFFF));
+}
+
+byte CastleC64MusicPlayer::sidControlForInstrument(byte instrument) const {
+ const InstrumentData &instrumentData = kInstruments[instrument % ARRAYSIZE(kInstruments)];
+
+ // The original C64 driver's waveform sequence 5 immediately switches the
+ // audible waveform to triangle; using the raw noise/pulse byte here makes
+ // several default-title notes disappear.
+ if (instrumentData.effect == 0x05)
+ return 0x11;
+
+ return instrumentData.control;
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/games/castle/c64.music.h b/engines/freescape/games/castle/c64.music.h
new file mode 100644
index 00000000000..368eba5f71c
--- /dev/null
+++ b/engines/freescape/games/castle/c64.music.h
@@ -0,0 +1,90 @@
+/* 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_CASTLE_C64_MUSIC_H
+#define FREESCAPE_CASTLE_C64_MUSIC_H
+
+#include "audio/sid.h"
+#include "freescape/music.h"
+
+namespace Freescape {
+
+class CastleC64MusicPlayer : public MusicPlayer {
+public:
+ CastleC64MusicPlayer();
+ ~CastleC64MusicPlayer() override;
+
+ void startMusic() override;
+ void stopMusic() override;
+ bool isPlaying() const override;
+
+private:
+ enum {
+ kChannelCount = 3,
+ kMaxNote = 95
+ };
+
+ struct ChannelState {
+ const byte *orderList;
+ uint16 orderPosition;
+ byte patternIndex;
+ uint16 patternDataOffset;
+ uint16 patternOffset;
+ uint16 delay;
+ byte instrument;
+ int8 transpose;
+ byte currentNote;
+ uint16 baseFrequency;
+ int16 frequencyOffset;
+ byte vibratoStep;
+ bool vibratoReverse;
+ byte control;
+ bool active;
+
+ void reset(const byte *channelOrderList);
+ };
+
+ SID::SID *_sid;
+ bool _musicActive;
+ uint32 _tick;
+ ChannelState _channels[kChannelCount];
+
+ void initSID();
+ void destroySID();
+ void sidWrite(int reg, byte data);
+ void silenceAll();
+ void setupSong();
+ void onTimer();
+ void loadNextPattern(int channel);
+ byte readPatternByte(int channel);
+ void parseCommands(int channel);
+ void noteOn(int channel, byte note);
+ void rest(int channel);
+ void gateOff(int channel);
+ void writeFrequency(int channel, uint16 frequency);
+ uint16 noteToSIDFrequency(int note) const;
+ void applyFrameEffects(int channel);
+ byte sidControlForInstrument(byte instrument) const;
+};
+
+} // namespace Freescape
+
+#endif
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index ed7e0356e00..2704f0cdefd 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -14,6 +14,7 @@ MODULE_OBJS := \
games/castle/atari.o \
games/castle/ay.music.o \
games/castle/c64.o \
+ games/castle/c64.music.o \
games/castle/cpc.o \
games/castle/dos.o \
games/castle/opl.music.o \
Commit: 17341bc0f7270d7597b0b505730f6d3bfd6ebc3b
https://github.com/scummvm/scummvm/commit/17341bc0f7270d7597b0b505730f6d3bfd6ebc3b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-25T14:08:39+02:00
Commit Message:
FREESCAPE: refactored of castle music code
Changed paths:
engines/freescape/games/castle/ay.music.cpp
engines/freescape/games/castle/ay.music.h
engines/freescape/games/castle/c64.music.cpp
engines/freescape/games/castle/castle.musicdata.h
engines/freescape/games/castle/opl.music.cpp
engines/freescape/games/castle/opl.music.h
engines/freescape/module.mk
diff --git a/engines/freescape/games/castle/ay.music.cpp b/engines/freescape/games/castle/ay.music.cpp
index 803ba2b502e..742d0248ae1 100644
--- a/engines/freescape/games/castle/ay.music.cpp
+++ b/engines/freescape/games/castle/ay.music.cpp
@@ -30,23 +30,6 @@ using namespace Freescape::CastleMusicData;
namespace Freescape {
-// AY-3-8910 period table (95 entries, same note numbering as the Castle OPL
-// table and Total Eclipse AY player). AY clock = 1MHz.
-const uint16 kAYPeriods[] = {
- 0, 3657, 3455, 3265, 3076, 2908, 2743, 2589,
- 2447, 2309, 2176, 2055, 1939, 1832, 1728, 1632,
- 1540, 1454, 1371, 1295, 1222, 1153, 1088, 1027,
- 970, 915, 864, 816, 770, 726, 686, 647,
- 611, 577, 544, 514, 485, 458, 432, 406,
- 385, 363, 343, 324, 305, 288, 272, 257,
- 242, 229, 216, 204, 192, 182, 171, 162,
- 153, 144, 136, 128, 121, 114, 108, 102,
- 96, 91, 86, 81, 76, 72, 68, 64,
- 61, 57, 54, 51, 48, 45, 43, 40,
- 38, 36, 34, 32, 30, 29, 27, 25,
- 24, 23, 21, 20, 19, 18, 17
-};
-
const byte kAYInstrumentVolumes[] = {
13, 11, 10, 11, 13, 13, 14, 12
};
@@ -65,33 +48,18 @@ const uint16 kDecayReleaseRate[16] = {
0x001A, 0x0009, 0x0005, 0x0003
};
-const int8 kVibrato10[] = { 0, 20, 10, -10, -20, 15, 5, -20 };
-const int8 kVibrato18[] = { 0, 3, -3, 3, -3, 3, -3, 3 };
-const int8 kVibrato20[] = { 0, -100, -100, -100, -100, -100, -100, -100 };
-const uint32 kSIDToAYPeriodScale = 1079000;
-
-const int8 *getVibratoTable(byte vibrato) {
- switch (vibrato) {
- case 0x10:
- return kVibrato10;
- case 0x18:
- return kVibrato18;
- case 0x20:
- return kVibrato20;
- default:
- return nullptr;
- }
-}
+const uint32 kSIDToAYPeriodScale = 1064276;
-uint16 applySIDFrequencyOffset(uint16 basePeriod, int16 frequencyOffset) {
- if (basePeriod == 0 || frequencyOffset == 0)
- return basePeriod;
+uint16 sidFrequencyToAYPeriod(uint16 sidFrequency) {
+ if (sidFrequency == 0)
+ return 0;
- int32 sidFrequency = (kSIDToAYPeriodScale + (basePeriod / 2)) / basePeriod;
- sidFrequency = MAX<int32>(1, sidFrequency + frequencyOffset);
+ return CLIP<uint32>((kSIDToAYPeriodScale + (sidFrequency / 2)) / sidFrequency, 1, 4095);
+}
- uint32 period = (kSIDToAYPeriodScale + (sidFrequency / 2)) / sidFrequency;
- return CLIP<uint32>(period, 1, 4095);
+uint16 applySIDFrequencyOffset(uint16 baseSIDFrequency, int16 frequencyOffset) {
+ int32 sidFrequency = MAX<int32>(1, (int32)baseSIDFrequency + frequencyOffset);
+ return sidFrequencyToAYPeriod(sidFrequency);
}
void CastleAYMusicPlayer::ChannelState::reset(const byte *channelOrderList) {
@@ -104,6 +72,7 @@ void CastleAYMusicPlayer::ChannelState::reset(const byte *channelOrderList) {
instrument = 0;
transpose = 0;
currentNote = 0;
+ baseSIDFrequency = 0;
basePeriod = 0;
currentPeriod = 0;
adsrVolume = 0;
@@ -361,9 +330,8 @@ void CastleAYMusicPlayer::noteOn(int channel, byte note) {
}
int effectiveNote = note + c.transpose + 20;
- uint16 period = 0;
- noteToPeriod(effectiveNote, period);
- c.basePeriod = period;
+ c.baseSIDFrequency = getCastleSIDFrequency(effectiveNote);
+ c.basePeriod = sidFrequencyToAYPeriod(c.baseSIDFrequency);
c.sidFrequencyOffset = 0;
c.vibratoStep = 1;
c.vibratoReverse = false;
@@ -386,6 +354,7 @@ void CastleAYMusicPlayer::noteOff(int channel) {
c.adsrPhase = kPhaseOff;
c.adsrVolume = 0;
c.currentNote = 0;
+ c.baseSIDFrequency = 0;
c.basePeriod = 0;
c.sidFrequencyOffset = 0;
setReg(8 + channel, 0);
@@ -404,16 +373,16 @@ void CastleAYMusicPlayer::noteToPeriod(int note, uint16 &period) const {
if (note > kMaxNote)
note = kMaxNote;
- period = kAYPeriods[note];
+ period = sidFrequencyToAYPeriod(getCastleSIDFrequency(note));
}
void CastleAYMusicPlayer::applyFrameEffects(int channel) {
ChannelState &c = _channels[channel];
- if (c.adsrPhase == kPhaseOff || c.currentNote == 0 || c.basePeriod == 0)
+ if (c.adsrPhase == kPhaseOff || c.currentNote == 0 || c.baseSIDFrequency == 0)
return;
const InstrumentData &instrument = kInstruments[c.instrument % ARRAYSIZE(kInstruments)];
- const int8 *vibratoTable = getVibratoTable(instrument.vibrato);
+ const int8 *vibratoTable = getCastleVibratoTable(instrument.vibrato);
if (!vibratoTable)
return;
@@ -425,7 +394,7 @@ void CastleAYMusicPlayer::applyFrameEffects(int channel) {
c.vibratoReverse = !c.vibratoReverse;
}
- uint16 period = applySIDFrequencyOffset(c.basePeriod, c.sidFrequencyOffset);
+ uint16 period = applySIDFrequencyOffset(c.baseSIDFrequency, c.sidFrequencyOffset);
writeChannelPeriod(channel, period);
if (c.noiseEnabled)
writeNoisePeriod(period);
@@ -515,14 +484,7 @@ void CastleAYMusicPlayer::writeNoisePeriod(uint16 period) {
}
byte CastleAYMusicPlayer::sidControlForInstrument(byte instrument) const {
- const InstrumentData &instrumentData = kInstruments[instrument % ARRAYSIZE(kInstruments)];
-
- // Castle SID waveform sequence 5 switches the audible part of the note to
- // triangle; on AY this maps to the tone generator rather than noise.
- if (instrumentData.effect == 0x05)
- return 0x11;
-
- return instrumentData.control;
+ return getCastleSIDControlForInstrument(instrument);
}
byte CastleAYMusicPlayer::instrumentVolumeScale(byte instrument) const {
diff --git a/engines/freescape/games/castle/ay.music.h b/engines/freescape/games/castle/ay.music.h
index 4dd955e10b0..fa4af04c5e5 100644
--- a/engines/freescape/games/castle/ay.music.h
+++ b/engines/freescape/games/castle/ay.music.h
@@ -71,6 +71,7 @@ private:
byte instrument;
int8 transpose;
byte currentNote;
+ uint16 baseSIDFrequency;
uint16 basePeriod;
uint16 currentPeriod;
uint16 adsrVolume;
diff --git a/engines/freescape/games/castle/c64.music.cpp b/engines/freescape/games/castle/c64.music.cpp
index 59323600fdd..8d4b8e930fc 100644
--- a/engines/freescape/games/castle/c64.music.cpp
+++ b/engines/freescape/games/castle/c64.music.cpp
@@ -32,41 +32,6 @@ using namespace Freescape::CastleMusicData;
namespace Freescape {
const int kCastleSIDVoiceOffset[] = { 0, 7, 14 };
-const int8 kCastleVibrato10[] = { 0, 20, 10, -10, -20, 15, 5, -20 };
-const int8 kCastleVibrato18[] = { 0, 3, -3, 3, -3, 3, -3, 3 };
-const int8 kCastleVibrato20[] = { 0, -100, -100, -100, -100, -100, -100, -100 };
-
-// Original Castle Master C64 SID frequency tables at $065A/$06BA.
-const byte kCastleSIDFreqLo[] = {
- 0x16, 0x27, 0x38, 0x4B, 0x5F, 0x73, 0x8A, 0xA1, 0xBA, 0xD4, 0xF0, 0x0E, 0x2D, 0x4E, 0x71, 0x96,
- 0xBD, 0xE7, 0x13, 0x42, 0x74, 0xA9, 0xE0, 0x1B, 0x5A, 0x9B, 0xE2, 0x2C, 0x7B, 0xCE, 0x27, 0x85,
- 0xE8, 0x51, 0xC1, 0x37, 0xB4, 0x37, 0xC4, 0x57, 0xF5, 0x9C, 0x4E, 0x09, 0xD0, 0xA3, 0x82, 0x6E,
- 0x68, 0x6E, 0x88, 0xAF, 0xEB, 0x39, 0x9C, 0x13, 0xA1, 0x46, 0x04, 0xDC, 0xD0, 0xDC, 0x10, 0x5E,
- 0xD6, 0x72, 0x38, 0x26, 0x42, 0x8C, 0x08, 0xB8, 0xA0, 0xB8, 0x20, 0xBC, 0xAC, 0xE4, 0x70, 0x4C,
- 0x84, 0x18, 0x10, 0x70, 0x40, 0x70, 0x40, 0x78, 0x58, 0xC8, 0xE0, 0x98, 0x08, 0x30, 0x20, 0x2E
-};
-
-const byte kCastleSIDFreqHi[] = {
- 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02,
- 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x03, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x06, 0x06,
- 0x06, 0x07, 0x07, 0x08, 0x08, 0x09, 0x09, 0x0A, 0x0A, 0x0B, 0x0C, 0x0D, 0x0D, 0x0E, 0x0F, 0x10,
- 0x11, 0x12, 0x13, 0x14, 0x15, 0x17, 0x18, 0x1A, 0x1B, 0x1D, 0x1F, 0x20, 0x22, 0x24, 0x27, 0x29,
- 0x2B, 0x2E, 0x31, 0x34, 0x37, 0x3A, 0x3E, 0x41, 0x45, 0x49, 0x4E, 0x52, 0x57, 0x5C, 0x62, 0x68,
- 0x6E, 0x75, 0x7C, 0x83, 0x8B, 0x93, 0x9C, 0xA5, 0xAF, 0xB9, 0xC4, 0xD0, 0xDD, 0xEA, 0xF8, 0xFD
-};
-
-const int8 *getCastleVibratoTable(byte vibrato) {
- switch (vibrato) {
- case 0x10:
- return kCastleVibrato10;
- case 0x18:
- return kCastleVibrato18;
- case 0x20:
- return kCastleVibrato20;
- default:
- return nullptr;
- }
-}
void CastleC64MusicPlayer::ChannelState::reset(const byte *channelOrderList) {
orderList = channelOrderList;
@@ -355,8 +320,7 @@ void CastleC64MusicPlayer::writeFrequency(int channel, uint16 frequency) {
}
uint16 CastleC64MusicPlayer::noteToSIDFrequency(int note) const {
- note = CLIP<int>(note, 0, ARRAYSIZE(kCastleSIDFreqLo) - 1);
- return kCastleSIDFreqLo[note] | (kCastleSIDFreqHi[note] << 8);
+ return getCastleSIDFrequency(note);
}
void CastleC64MusicPlayer::applyFrameEffects(int channel) {
@@ -382,15 +346,7 @@ void CastleC64MusicPlayer::applyFrameEffects(int channel) {
}
byte CastleC64MusicPlayer::sidControlForInstrument(byte instrument) const {
- const InstrumentData &instrumentData = kInstruments[instrument % ARRAYSIZE(kInstruments)];
-
- // The original C64 driver's waveform sequence 5 immediately switches the
- // audible waveform to triangle; using the raw noise/pulse byte here makes
- // several default-title notes disappear.
- if (instrumentData.effect == 0x05)
- return 0x11;
-
- return instrumentData.control;
+ return getCastleSIDControlForInstrument(instrument);
}
} // namespace Freescape
diff --git a/engines/freescape/games/castle/castle.musicdata.h b/engines/freescape/games/castle/castle.musicdata.h
index d1e1913b16a..0dfe8a69bde 100644
--- a/engines/freescape/games/castle/castle.musicdata.h
+++ b/engines/freescape/games/castle/castle.musicdata.h
@@ -61,6 +61,13 @@ const InstrumentData kInstruments[] = {
{ 0x00, 0x08, 0x41, 0x13, 0x0F, 0x02, 0x7F, 0x00, 0x05 }
};
+extern const byte kCastleSIDFreqLo[96];
+extern const byte kCastleSIDFreqHi[96];
+
+uint16 getCastleSIDFrequency(int note);
+const int8 *getCastleVibratoTable(byte vibrato);
+byte getCastleSIDControlForInstrument(byte instrument);
+
const byte kOrderList0[] = {
0x80, 0xFB, 0x00, 0x03, 0x03, 0x03, 0x03, 0x0A, 0x0A, 0x0D, 0x0D, 0x0D, 0x12, 0x12, 0x12, 0x12,
0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x1A, 0xFF
diff --git a/engines/freescape/games/castle/opl.music.cpp b/engines/freescape/games/castle/opl.music.cpp
index 88f7a379673..4d3b6ac3f43 100644
--- a/engines/freescape/games/castle/opl.music.cpp
+++ b/engines/freescape/games/castle/opl.music.cpp
@@ -112,9 +112,17 @@ void CastleOPLMusicPlayer::ChannelState::reset(const byte *channelOrderList) {
delay = 0;
instrument = 0;
transpose = 0;
+ currentNote = 0;
+ baseSIDFrequency = 0;
+ sidFrequencyOffset = 0;
+ baseFrequencyFnum = 0;
+ baseFrequencyBlock = 0;
frequencyFnum = 0;
frequencyBlock = 0;
+ vibratoStep = 1;
+ vibratoReverse = false;
keyOn = false;
+ gateReleased = true;
}
// ============================================================================
@@ -224,8 +232,17 @@ void CastleOPLMusicPlayer::noteOn(int channel, byte note) {
uint16 fnum = 0;
byte block = 0;
- noteToFnumBlock(note + _channels[channel].transpose + 20, fnum, block);
+ int effectiveNote = note + _channels[channel].transpose + 20;
+ noteToFnumBlock(effectiveNote, fnum, block);
+ _channels[channel].currentNote = CLIP<int>(effectiveNote, 0, kMaxNote);
+ _channels[channel].baseSIDFrequency = getCastleSIDFrequency(effectiveNote);
+ _channels[channel].sidFrequencyOffset = 0;
+ _channels[channel].baseFrequencyFnum = fnum;
+ _channels[channel].baseFrequencyBlock = block;
+ _channels[channel].vibratoStep = 1;
+ _channels[channel].vibratoReverse = false;
_channels[channel].keyOn = true;
+ _channels[channel].gateReleased = false;
setFrequency(channel, fnum, block);
}
@@ -234,6 +251,19 @@ void CastleOPLMusicPlayer::noteOff(int channel) {
return;
_channels[channel].keyOn = false;
+ _channels[channel].gateReleased = true;
+ _channels[channel].currentNote = 0;
+ _channels[channel].baseSIDFrequency = 0;
+ _channels[channel].sidFrequencyOffset = 0;
+ writeFrequency(channel, _channels[channel].frequencyFnum, _channels[channel].frequencyBlock);
+}
+
+void CastleOPLMusicPlayer::gateOff(int channel) {
+ if (!_opl || _channels[channel].gateReleased)
+ return;
+
+ _channels[channel].keyOn = false;
+ _channels[channel].gateReleased = true;
writeFrequency(channel, _channels[channel].frequencyFnum, _channels[channel].frequencyBlock);
}
@@ -247,8 +277,10 @@ void CastleOPLMusicPlayer::onTimer() {
for (int i = 0; i < kChannelCount; i++) {
if (_channels[i].delay > 0) {
- // The SID release tail covers its early gate-off; a hard OPL key-off
- // here creates audible gaps, so hold until the next note or rest.
+ const InstrumentData &instrument = kInstruments[_channels[i].instrument % ARRAYSIZE(kInstruments)];
+ if (_channels[i].currentNote != 0 && _channels[i].delay == instrument.gateOffTime)
+ gateOff(i);
+ applyFrameEffects(i);
_channels[i].delay--;
continue;
}
@@ -259,6 +291,36 @@ void CastleOPLMusicPlayer::onTimer() {
_tick++;
}
+void CastleOPLMusicPlayer::applyFrameEffects(int channel) {
+ ChannelState &c = _channels[channel];
+ if (c.currentNote == 0 || c.baseSIDFrequency == 0 || c.baseFrequencyFnum == 0)
+ return;
+
+ const InstrumentData &instrument = kInstruments[c.instrument % ARRAYSIZE(kInstruments)];
+ const int8 *vibratoTable = getCastleVibratoTable(instrument.vibrato);
+ if (!vibratoTable)
+ return;
+
+ int8 sidDelta = vibratoTable[c.vibratoStep & 0x07];
+ c.sidFrequencyOffset += c.vibratoReverse ? -sidDelta : sidDelta;
+ c.vibratoStep++;
+ if (c.vibratoStep >= 8) {
+ c.vibratoStep = 1;
+ c.vibratoReverse = !c.vibratoReverse;
+ }
+
+ int32 sidFrequency = MAX<int32>(1, (int32)c.baseSIDFrequency + c.sidFrequencyOffset);
+ uint32 scaledFnum = ((uint32)c.baseFrequencyFnum * sidFrequency + (c.baseSIDFrequency / 2)) / c.baseSIDFrequency;
+ byte block = c.baseFrequencyBlock;
+
+ while (scaledFnum > 0x3FF && block < 7) {
+ scaledFnum = (scaledFnum + 1) >> 1;
+ block++;
+ }
+
+ setFrequency(channel, MIN<uint32>(scaledFnum, 0x3FF), block);
+}
+
// ============================================================================
// Song setup
// ============================================================================
diff --git a/engines/freescape/games/castle/opl.music.h b/engines/freescape/games/castle/opl.music.h
index 1c86806391f..12929a3c914 100644
--- a/engines/freescape/games/castle/opl.music.h
+++ b/engines/freescape/games/castle/opl.music.h
@@ -59,9 +59,17 @@ private:
uint16 delay;
byte instrument;
int8 transpose;
+ byte currentNote;
+ uint16 baseSIDFrequency;
+ int16 sidFrequencyOffset;
+ uint16 baseFrequencyFnum;
+ byte baseFrequencyBlock;
uint16 frequencyFnum;
byte frequencyBlock;
+ byte vibratoStep;
+ bool vibratoReverse;
bool keyOn;
+ bool gateReleased;
void reset(const byte *channelOrderList);
};
@@ -80,6 +88,8 @@ private:
void setOPLInstrument(int channel, byte instrument);
void noteOn(int channel, byte note);
void noteOff(int channel);
+ void gateOff(int channel);
+ void applyFrameEffects(int channel);
void setFrequency(int channel, uint16 fnum, byte block);
void writeFrequency(int channel, uint16 fnum, byte block);
void noteToFnumBlock(int note, uint16 &fnum, byte &block) const;
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index 2704f0cdefd..ed47cb0019e 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -10,6 +10,7 @@ MODULE_OBJS := \
font.o \
freescape.o \
games/castle/castle.o \
+ games/castle/castle.musicdata.o \
games/castle/amiga.o \
games/castle/atari.o \
games/castle/ay.music.o \
Commit: 5b4a8afee92e38c7a6eba605b60fbd86ce0e959d
https://github.com/scummvm/scummvm/commit/5b4a8afee92e38c7a6eba605b60fbd86ce0e959d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-25T14:08:39+02:00
Commit Message:
FREESCAPE: missing common .cpp for castle music
Changed paths:
A engines/freescape/games/castle/castle.musicdata.cpp
diff --git a/engines/freescape/games/castle/castle.musicdata.cpp b/engines/freescape/games/castle/castle.musicdata.cpp
new file mode 100644
index 00000000000..b295ae95bb6
--- /dev/null
+++ b/engines/freescape/games/castle/castle.musicdata.cpp
@@ -0,0 +1,80 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "engines/freescape/games/castle/castle.musicdata.h"
+
+#include "common/util.h"
+
+namespace Freescape {
+namespace CastleMusicData {
+
+// Original Castle Master C64 SID frequency tables at $065A/$06BA.
+extern const byte kCastleSIDFreqLo[96] = {
+ 0x16, 0x27, 0x38, 0x4B, 0x5F, 0x73, 0x8A, 0xA1, 0xBA, 0xD4, 0xF0, 0x0E, 0x2D, 0x4E, 0x71, 0x96,
+ 0xBD, 0xE7, 0x13, 0x42, 0x74, 0xA9, 0xE0, 0x1B, 0x5A, 0x9B, 0xE2, 0x2C, 0x7B, 0xCE, 0x27, 0x85,
+ 0xE8, 0x51, 0xC1, 0x37, 0xB4, 0x37, 0xC4, 0x57, 0xF5, 0x9C, 0x4E, 0x09, 0xD0, 0xA3, 0x82, 0x6E,
+ 0x68, 0x6E, 0x88, 0xAF, 0xEB, 0x39, 0x9C, 0x13, 0xA1, 0x46, 0x04, 0xDC, 0xD0, 0xDC, 0x10, 0x5E,
+ 0xD6, 0x72, 0x38, 0x26, 0x42, 0x8C, 0x08, 0xB8, 0xA0, 0xB8, 0x20, 0xBC, 0xAC, 0xE4, 0x70, 0x4C,
+ 0x84, 0x18, 0x10, 0x70, 0x40, 0x70, 0x40, 0x78, 0x58, 0xC8, 0xE0, 0x98, 0x08, 0x30, 0x20, 0x2E
+};
+
+extern const byte kCastleSIDFreqHi[96] = {
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x03, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x06, 0x06,
+ 0x06, 0x07, 0x07, 0x08, 0x08, 0x09, 0x09, 0x0A, 0x0A, 0x0B, 0x0C, 0x0D, 0x0D, 0x0E, 0x0F, 0x10,
+ 0x11, 0x12, 0x13, 0x14, 0x15, 0x17, 0x18, 0x1A, 0x1B, 0x1D, 0x1F, 0x20, 0x22, 0x24, 0x27, 0x29,
+ 0x2B, 0x2E, 0x31, 0x34, 0x37, 0x3A, 0x3E, 0x41, 0x45, 0x49, 0x4E, 0x52, 0x57, 0x5C, 0x62, 0x68,
+ 0x6E, 0x75, 0x7C, 0x83, 0x8B, 0x93, 0x9C, 0xA5, 0xAF, 0xB9, 0xC4, 0xD0, 0xDD, 0xEA, 0xF8, 0xFD
+};
+
+const int8 kCastleVibrato10[] = { 0, 20, 10, -10, -20, 15, 5, -20 };
+const int8 kCastleVibrato18[] = { 0, 3, -3, 3, -3, 3, -3, 3 };
+const int8 kCastleVibrato20[] = { 0, -100, -100, -100, -100, -100, -100, -100 };
+
+uint16 getCastleSIDFrequency(int note) {
+ note = CLIP<int>(note, 0, ARRAYSIZE(kCastleSIDFreqLo) - 1);
+ return kCastleSIDFreqLo[note] | (kCastleSIDFreqHi[note] << 8);
+}
+
+const int8 *getCastleVibratoTable(byte vibrato) {
+ switch (vibrato) {
+ case 0x10:
+ return kCastleVibrato10;
+ case 0x18:
+ return kCastleVibrato18;
+ case 0x20:
+ return kCastleVibrato20;
+ default:
+ return nullptr;
+ }
+}
+
+byte getCastleSIDControlForInstrument(byte instrument) {
+ const InstrumentData &instrumentData = kInstruments[instrument % ARRAYSIZE(kInstruments)];
+
+ if (instrumentData.effect == 0x05)
+ return 0x11;
+
+ return instrumentData.control;
+}
+
+} // namespace CastleMusicData
+} // namespace Freescape
Commit: cfe7df270563953ac0146dd832a8003d7e7552ff
https://github.com/scummvm/scummvm/commit/cfe7df270563953ac0146dd832a8003d7e7552ff
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-25T14:08:39+02:00
Commit Message:
FREESCAPE: more function UI for castle c64
Changed paths:
dists/engine-data/freescape.dat
engines/freescape/games/castle/c64.cpp
engines/freescape/games/castle/castle.cpp
diff --git a/dists/engine-data/freescape.dat b/dists/engine-data/freescape.dat
index 8ac681b3a8c..9ead4c51247 100644
Binary files a/dists/engine-data/freescape.dat and b/dists/engine-data/freescape.dat differ
diff --git a/engines/freescape/games/castle/c64.cpp b/engines/freescape/games/castle/c64.cpp
index 89f5f0fb85b..a65848797e6 100644
--- a/engines/freescape/games/castle/c64.cpp
+++ b/engines/freescape/games/castle/c64.cpp
@@ -37,7 +37,13 @@ enum {
kCastleC64CompactAreaTableOffset = 0x4b,
kCastleC64BackgroundColor = 0x00,
kCastleC64ScreenHighNibbleColor = 0x0c,
- kCastleC64ColorRamColor = 0x01
+ kCastleC64ColorRamColor = 0x01,
+ kCastleC64BorderCropLeft = 32,
+ kCastleC64BorderCropTop = 35,
+ kCastleC64MessageLeft = 118,
+ kCastleC64MessageRight = 280,
+ kCastleC64MessageX = 120,
+ kCastleC64MessageY = 182
};
struct CastleC64Repeat {
@@ -66,6 +72,105 @@ const CastleC64Repeat kCastleC64DatabaseRepeats[] = {
{ 0x2255, 113, 0x00 }
};
+static const byte kCastleC64FontData[] = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x18, 0x00,
+ 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x66, 0x66, 0xff, 0x66, 0xff, 0x66, 0x66, 0x00,
+ 0x18, 0x3e, 0x58, 0x3c, 0x1a, 0x7c, 0x18, 0x00,
+ 0x62, 0x66, 0x0c, 0x18, 0x30, 0x66, 0x46, 0x00,
+ 0x3c, 0x66, 0x3c, 0x38, 0x67, 0x66, 0x3f, 0x00,
+ 0x06, 0x0c, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x0c, 0x18, 0x30, 0x30, 0x30, 0x18, 0x0c, 0x00,
+ 0x30, 0x18, 0x0c, 0x0c, 0x0c, 0x18, 0x30, 0x00,
+ 0x00, 0x66, 0x3c, 0xff, 0x3c, 0x66, 0x00, 0x00,
+ 0x00, 0x18, 0x18, 0x7e, 0x18, 0x18, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30,
+ 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00,
+ 0x00, 0x03, 0x06, 0x0c, 0x18, 0x30, 0x60, 0x00,
+ 0x3c, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3c, 0x00,
+ 0x18, 0x18, 0x38, 0x18, 0x18, 0x18, 0x7e, 0x00,
+ 0x3c, 0x66, 0x06, 0x0c, 0x30, 0x60, 0x7e, 0x00,
+ 0x3c, 0x66, 0x06, 0x1c, 0x06, 0x66, 0x3c, 0x00,
+ 0x06, 0x0e, 0x1e, 0x66, 0x7f, 0x06, 0x06, 0x00,
+ 0x7e, 0x60, 0x7c, 0x06, 0x06, 0x66, 0x3c, 0x00,
+ 0x3c, 0x66, 0x60, 0x7c, 0x66, 0x66, 0x3c, 0x00,
+ 0x7e, 0x66, 0x0c, 0x18, 0x18, 0x18, 0x18, 0x00,
+ 0x3c, 0x66, 0x66, 0x3c, 0x66, 0x66, 0x3c, 0x00,
+ 0x3c, 0x66, 0x66, 0x3e, 0x06, 0x66, 0x3c, 0x00,
+ 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x00, 0x00,
+ 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x18, 0x30,
+ 0x0e, 0x18, 0x30, 0x60, 0x30, 0x18, 0x0e, 0x00,
+ 0x00, 0x00, 0x7e, 0x00, 0x7e, 0x00, 0x00, 0x00,
+ 0x70, 0x18, 0x0c, 0x06, 0x0c, 0x18, 0x70, 0x00,
+ 0x3c, 0x66, 0x06, 0x0c, 0x18, 0x00, 0x18, 0x00,
+ 0x3c, 0x66, 0x6e, 0x6e, 0x60, 0x62, 0x3c, 0x00,
+ 0x18, 0x3c, 0x66, 0x7e, 0x66, 0x66, 0x66, 0x00,
+ 0x7c, 0x66, 0x66, 0x7c, 0x66, 0x66, 0x7c, 0x00,
+ 0x3c, 0x66, 0x60, 0x60, 0x60, 0x66, 0x3c, 0x00,
+ 0x78, 0x6c, 0x66, 0x66, 0x66, 0x6c, 0x78, 0x00,
+ 0x7e, 0x60, 0x60, 0x78, 0x60, 0x60, 0x7e, 0x00,
+ 0x7e, 0x60, 0x60, 0x78, 0x60, 0x60, 0x60, 0x00,
+ 0x3c, 0x66, 0x60, 0x6e, 0x66, 0x66, 0x3c, 0x00,
+ 0x66, 0x66, 0x66, 0x7e, 0x66, 0x66, 0x66, 0x00,
+ 0x3c, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3c, 0x00,
+ 0x1e, 0x0c, 0x0c, 0x0c, 0x0c, 0x6c, 0x38, 0x00,
+ 0x66, 0x6c, 0x78, 0x70, 0x78, 0x6c, 0x66, 0x00,
+ 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x7e, 0x00,
+ 0x63, 0x77, 0x7f, 0x6b, 0x63, 0x63, 0x63, 0x00,
+ 0x66, 0x76, 0x7e, 0x7e, 0x6e, 0x66, 0x66, 0x00,
+ 0x3c, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3c, 0x00,
+ 0x7c, 0x66, 0x66, 0x7c, 0x60, 0x60, 0x60, 0x00,
+ 0x3c, 0x66, 0x66, 0x66, 0x66, 0x3c, 0x0e, 0x00,
+ 0x7c, 0x66, 0x66, 0x7c, 0x78, 0x6c, 0x66, 0x00,
+ 0x3c, 0x66, 0x60, 0x3c, 0x06, 0x66, 0x3c, 0x00,
+ 0x7e, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00,
+ 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3c, 0x00,
+ 0x66, 0x66, 0x66, 0x66, 0x66, 0x3c, 0x18, 0x00,
+ 0x63, 0x63, 0x63, 0x6b, 0x7f, 0x77, 0x63, 0x00,
+ 0x66, 0x66, 0x3c, 0x18, 0x3c, 0x66, 0x66, 0x00,
+ 0x66, 0x66, 0x66, 0x3c, 0x18, 0x18, 0x18, 0x00,
+ 0x7e, 0x06, 0x0c, 0x18, 0x30, 0x60, 0x7e, 0x00,
+ 0x3c, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3c, 0x00,
+ 0x00, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x03, 0x00,
+ 0x3c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x3c, 0x00,
+ 0x18, 0x3c, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00,
+ 0x18, 0x0c, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x3c, 0x06, 0x3e, 0x66, 0x3e, 0x00,
+ 0x00, 0x60, 0x60, 0x7c, 0x66, 0x66, 0x7c, 0x00,
+ 0x00, 0x00, 0x3c, 0x60, 0x60, 0x60, 0x3c, 0x00,
+ 0x00, 0x06, 0x06, 0x3e, 0x66, 0x66, 0x3e, 0x00,
+ 0x00, 0x00, 0x3c, 0x66, 0x7e, 0x60, 0x3c, 0x00,
+ 0x00, 0x0e, 0x18, 0x3e, 0x18, 0x18, 0x18, 0x00,
+ 0x00, 0x00, 0x3e, 0x66, 0x66, 0x3e, 0x06, 0x7c,
+ 0x00, 0x60, 0x60, 0x7c, 0x66, 0x66, 0x66, 0x00,
+ 0x00, 0x18, 0x00, 0x38, 0x18, 0x18, 0x3c, 0x00,
+ 0x00, 0x06, 0x00, 0x06, 0x06, 0x06, 0x06, 0x3c,
+ 0x00, 0x60, 0x60, 0x6c, 0x78, 0x6c, 0x66, 0x00,
+ 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x3c, 0x00,
+ 0x00, 0x00, 0x66, 0x7f, 0x7f, 0x6b, 0x63, 0x00,
+ 0x00, 0x00, 0x7c, 0x66, 0x66, 0x66, 0x66, 0x00,
+ 0x00, 0x00, 0x3c, 0x66, 0x66, 0x66, 0x3c, 0x00,
+ 0x00, 0x00, 0x7c, 0x66, 0x66, 0x7c, 0x60, 0x60,
+ 0x00, 0x00, 0x3e, 0x66, 0x66, 0x3e, 0x06, 0x06,
+ 0x00, 0x00, 0x7c, 0x66, 0x60, 0x60, 0x60, 0x00,
+ 0x00, 0x00, 0x3e, 0x60, 0x3c, 0x06, 0x7c, 0x00,
+ 0x00, 0x18, 0x7e, 0x18, 0x18, 0x18, 0x0e, 0x00,
+ 0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x3e, 0x00,
+ 0x00, 0x00, 0x66, 0x66, 0x66, 0x3c, 0x18, 0x00,
+ 0x00, 0x00, 0x63, 0x6b, 0x7f, 0x3e, 0x36, 0x00,
+ 0x00, 0x00, 0x66, 0x3c, 0x18, 0x3c, 0x66, 0x00,
+ 0x00, 0x00, 0x66, 0x66, 0x66, 0x3e, 0x0c, 0x78,
+ 0x00, 0x00, 0x7e, 0x0c, 0x18, 0x30, 0x7e, 0x00,
+ 0x0e, 0x18, 0x18, 0x70, 0x18, 0x18, 0x0e, 0x00,
+ 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00,
+ 0x70, 0x18, 0x18, 0x0e, 0x18, 0x18, 0x70, 0x00,
+ 0x31, 0x6b, 0x46, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
+};
+
uint16 readCastleC64Uint16LE(const Common::Array<byte> &data, uint32 offset) {
if (offset + 1 >= data.size())
error("Castle C64 database pointer read out of range at 0x%x", offset);
@@ -127,6 +232,28 @@ Common::Array<byte> normalizeCastleC64Database(Common::SeekableReadStream *file)
return decoded;
}
+Common::Array<Graphics::ManagedSurface *> loadCastleC64Font() {
+ Common::Array<Graphics::ManagedSurface *> chars;
+
+ for (uint chr = 0; chr < ARRAYSIZE(kCastleC64FontData) / 8; chr++) {
+ Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+ surface->create(8, 8, Graphics::PixelFormat::createFormatCLUT8());
+ surface->clear(0);
+
+ for (int y = 0; y < 8; y++) {
+ byte row = kCastleC64FontData[chr * 8 + y];
+ for (int x = 0; x < 8; x++) {
+ if (row & (0x80 >> x))
+ surface->setPixel(x, y, 1);
+ }
+ }
+
+ chars.push_back(surface);
+ }
+
+ return chars;
+}
+
class CastleC64DatabaseReadStream : public Common::SeekableReadStream {
public:
CastleC64DatabaseReadStream(const Common::Array<byte> &data) : _data(data), _pos(0), _eos(false), _colorMapRead(false) {
@@ -229,7 +356,7 @@ private:
};
void CastleEngine::initC64() {
- _viewArea = Common::Rect(32, 32, 288, 136);
+ _viewArea = Common::Rect(40, 32, 280, 152);
}
extern byte kC64Palette[16][3];
@@ -303,6 +430,14 @@ void CastleEngine::loadAssetsC64FullGame() {
if (!file.isOpen())
error("Failed to open castlemaster.c64.data");
+ // The original tape loader preloads display support into high RAM before
+ // this main program image starts; castlemaster.c64.data has no standalone
+ // font block like the other C64 Freescape games.
+ Common::Array<Graphics::ManagedSurface *> chars = loadCastleC64Font();
+ _font = Font(chars);
+ _font.setCharWidth(8);
+ _fontLoaded = true;
+
loadMessagesC64(&file, 0x13a9, 75);
loadRiddlesC64(&file, 0x1823, 9);
Common::Array<byte> database = normalizeCastleC64Database(&file);
@@ -329,9 +464,21 @@ void CastleEngine::loadAssetsC64FullGame() {
debugC(1, kFreescapeDebugParser, "Castle C64 area %d colors: background=%d screen1=%d screen2=%d colorRAM=%d", areaID, area->_usualBackgroundColor, area->_underFireBackgroundColor, area->_paperColor, area->_inkColor);
}
+ Graphics::Surface *surf = loadBundledImage("castle_border");
+ surf->convertToInPlace(_gfx->_texturePixelFormat);
_border = new Graphics::ManagedSurface();
- _border->create(_screenW, _screenH, _gfx->_texturePixelFormat);
- _border->fillRect(Common::Rect(0, 0, _screenW, _screenH), _gfx->_texturePixelFormat.ARGBToColor(0xff, 0, 0, 0));
+ if (surf->w == _screenW && surf->h == _screenH) {
+ _border->copyFrom(*surf);
+ } else {
+ Common::Rect borderRect(kCastleC64BorderCropLeft, kCastleC64BorderCropTop, kCastleC64BorderCropLeft + _screenW, kCastleC64BorderCropTop + _screenH);
+ if (surf->w < borderRect.right || surf->h < borderRect.bottom)
+ error("Castle C64 border has unsupported dimensions %dx%d", surf->w, surf->h);
+
+ _border->create(_screenW, _screenH, _gfx->_texturePixelFormat);
+ _border->copyRectToSurface(*surf, 0, 0, borderRect);
+ }
+ surf->free();
+ delete surf;
_playerMusic = new CastleC64MusicPlayer();
@@ -339,6 +486,35 @@ void CastleEngine::loadAssetsC64FullGame() {
}
void CastleEngine::drawC64UI(Graphics::Surface *surface) {
+ uint32 front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x62, 0xD5, 0x32);
+
+ uint8 r, g, b;
+ _gfx->readFromPalette(0, r, g, b);
+ uint32 back = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
+
+ // The original loader leaves "CASTLE MASTER" in the bottom message strip.
+ // Clear the whole writable part of that strip before drawing runtime text.
+ Common::Rect backRect(kCastleC64MessageLeft, 181, kCastleC64MessageRight, 192);
+ surface->fillRect(backRect, back);
+
+ Common::String message;
+ int deadline = -1;
+ getLatestMessages(message, deadline);
+ if (deadline > 0 && deadline <= _countdown) {
+ drawStringInSurface(message, kCastleC64MessageX, kCastleC64MessageY, front, back, surface);
+ _temporaryMessages.push_back(message);
+ _temporaryMessageDeadlines.push_back(deadline);
+ } else if (_gameStateControl == kFreescapeGameStatePlaying) {
+ if (ghostInArea() && !_ghostInAreaMessage.empty()) {
+ drawStringInSurface(_ghostInAreaMessage, kCastleC64MessageX, kCastleC64MessageY, front, back, surface);
+ } else {
+ Common::String areaName = _currentArea->_name;
+ uint areaMessageIndex = 16 + _currentArea->getAreaID();
+ if (areaMessageIndex < _messagesList.size())
+ areaName = _messagesList[areaMessageIndex];
+ drawStringInSurface(areaName, kCastleC64MessageX, kCastleC64MessageY, front, back, surface);
+ }
+ }
}
} // End of namespace Freescape
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 5b315b9c117..d5b3c1a9ef1 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -2095,8 +2095,16 @@ void CastleEngine::borderScreen() {
delete surface;
}
- if (isC64())
- return;
+ if (isC64()) {
+ // The bundled C64 border preserves the original loader text in the
+ // view area; clear it before drawing ScummVM's character selection.
+ if (_border) {
+ _border->fillRect(_viewArea, _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0, 0, 0));
+ delete _borderTexture;
+ _borderTexture = nullptr;
+ loadBorder();
+ }
+ }
if (!isCastleMaster2())
selectCharacterScreen();
Commit: 421ca52e30226563cc3dad11fd79b2f13e4f110c
https://github.com/scummvm/scummvm/commit/421ca52e30226563cc3dad11fd79b2f13e4f110c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-25T14:08:39+02:00
Commit Message:
FREESCAPE: improved precision on the drilling for driller
Changed paths:
engines/freescape/games/driller/amiga.cpp
engines/freescape/games/driller/c64.cpp
engines/freescape/games/driller/cpc.cpp
engines/freescape/games/driller/dos.cpp
engines/freescape/games/driller/driller.cpp
engines/freescape/games/driller/driller.h
engines/freescape/games/driller/zx.cpp
diff --git a/engines/freescape/games/driller/amiga.cpp b/engines/freescape/games/driller/amiga.cpp
index cfd74afb0b4..902ecc97e68 100644
--- a/engines/freescape/games/driller/amiga.cpp
+++ b/engines/freescape/games/driller/amiga.cpp
@@ -543,9 +543,7 @@ void DrillerEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
// then scrolling N/E/S/W needle (SPRCOG) drawn on top one line below.
// Background at x=$32â48, y=$8E=142. Needle at y=$8E+1=143.
if (!_compassYawFrames.empty()) {
- float yaw = _yaw;
- if (yaw < 0) yaw += 360;
- if (yaw >= 360) yaw -= 360;
+ float yaw = compassYaw();
int rot = ((int)(yaw / 5.0f)) % 72;
surface->copyRectToSurfaceWithKey(*_compassYawFrames[rot], 49, 143,
Common::Rect(_compassYawFrames[rot]->w, _compassYawFrames[rot]->h), transparent);
diff --git a/engines/freescape/games/driller/c64.cpp b/engines/freescape/games/driller/c64.cpp
index 8f2bfd36c87..c58787b70fc 100644
--- a/engines/freescape/games/driller/c64.cpp
+++ b/engines/freescape/games/driller/c64.cpp
@@ -286,7 +286,7 @@ void DrillerEngine::drawC64UI(Graphics::Surface *surface) {
uint32 yellow = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
surface->fillRect(Common::Rect(87, 156, 104, 166), back);
- drawCompass(surface, 94, 156, _yaw - 30, 11, 75, yellow);
+ drawCompass(surface, 94, 156, compassYaw() - 30, 11, 75, yellow);
surface->fillRect(Common::Rect(224, 151, 235, 160), back);
drawCompass(surface, 223, 156, _pitch - 30, 12, 60, yellow);
}
diff --git a/engines/freescape/games/driller/cpc.cpp b/engines/freescape/games/driller/cpc.cpp
index 73a4436ebc2..72ec651dc48 100644
--- a/engines/freescape/games/driller/cpc.cpp
+++ b/engines/freescape/games/driller/cpc.cpp
@@ -235,7 +235,7 @@ void DrillerEngine::drawCPCUI(Graphics::Surface *surface) {
surface->fillRect(shieldBar, front);
}
- drawCompass(surface, 87, 156, _yaw - 30, 10, 75, front);
+ drawCompass(surface, 87, 156, compassYaw() - 30, 10, 75, front);
drawCompass(surface, 230, 156, _pitch - 30, 10, 60, front);
}
diff --git a/engines/freescape/games/driller/dos.cpp b/engines/freescape/games/driller/dos.cpp
index b3d4697b339..30ba3c74cec 100644
--- a/engines/freescape/games/driller/dos.cpp
+++ b/engines/freescape/games/driller/dos.cpp
@@ -437,7 +437,7 @@ void DrillerEngine::drawDOSUI(Graphics::Surface *surface) {
uint32 other = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
Common::Point compassYawPos = _renderMode == Common::kRenderHercG ? Common::Point(214, 264) : Common::Point(87, 156);
- drawCompass(surface, compassYawPos.x, compassYawPos.y, _yaw - 30, 10, 75, other);
+ drawCompass(surface, compassYawPos.x, compassYawPos.y, compassYaw() - 30, 10, 75, other);
Common::Point compassPitchPos = _renderMode == Common::kRenderHercG ? Common::Point(502, 264) : Common::Point(230, 156);
drawCompass(surface, compassPitchPos.x, compassPitchPos.y, _pitch - 30, 10, 60, other);
}
diff --git a/engines/freescape/games/driller/driller.cpp b/engines/freescape/games/driller/driller.cpp
index 4ca5fb81de8..62721ef68d6 100644
--- a/engines/freescape/games/driller/driller.cpp
+++ b/engines/freescape/games/driller/driller.cpp
@@ -541,43 +541,59 @@ void DrillerEngine::pressedKey(const int keycode) {
clearTemporalMessages();
Common::Point gasPocket = _currentArea->_gasPocketPosition;
uint32 gasPocketRadius = _currentArea->_gasPocketRadius;
+ debugC(1, kFreescapeDebugMove, "DRILL deploy requested area=%u name='%s' scale=%u fly=%d energy=%d automatic=%d",
+ _currentArea->getAreaID(), _currentArea->_name.c_str(), _currentArea->getScale(),
+ _flyMode ? 1 : 0, _gameStateVars[k8bitVariableEnergy], _useAutomaticDrilling ? 1 : 0);
+ debugC(1, kFreescapeDebugMove, "DRILL player world=(%.2f,%.2f,%.2f) ui=(X=%04d,T=%04d,Y=%04d) yaw=%.2f pitch=%.2f front=(%.4f,%.4f,%.4f)",
+ _position.x(), _position.y(), _position.z(), int(2 * _position.x()), int(2 * _position.z()), int(2 * _position.y()),
+ _yaw, _pitch, _cameraFront.x(), _cameraFront.y(), _cameraFront.z());
if (gasPocketRadius == 0) {
+ debugC(1, kFreescapeDebugMove, "DRILL rejected: area has no gas pocket");
insertTemporaryMessage(_messagesList[2], _countdown - 2);
return;
}
if (_flyMode) {
+ debugC(1, kFreescapeDebugMove, "DRILL rejected: player is in probe/flight mode");
insertTemporaryMessage(_messagesList[8], _countdown - 2);
return;
}
if (drillDeployed(_currentArea)) {
+ debugC(1, kFreescapeDebugMove, "DRILL rejected: rig already deployed in this area");
insertTemporaryMessage(_messagesList[12], _countdown - 2);
return;
}
if (_gameStateVars[k8bitVariableEnergy] < 5) {
+ debugC(1, kFreescapeDebugMove, "DRILL rejected: energy too low (%d)", _gameStateVars[k8bitVariableEnergy]);
insertTemporaryMessage(_messagesList[7], _countdown - 2);
return;
}
Math::Vector3d drill = drillPosition();
- debugC(1, kFreescapeDebugMove, "Current position at %f %f %f", _position.x(), _position.y(), _position.z());
- debugC(1, kFreescapeDebugMove, "Trying to adding drill at %f %f %f", drill.x(), drill.y(), drill.z());
- debugC(1, kFreescapeDebugMove, "with pitch: %f and yaw %f", _pitch, _yaw);
+ const Math::Vector3d drillCenter(drill.x(), drill.y(), drill.z() + 128.0f);
+ debugC(1, kFreescapeDebugMove, "DRILL render anchor world=(%.2f,%.2f,%.2f) ui=(X=%04d,T=%04d,Y=%04d) cell=(%d,%d) center=(%.2f,%.2f) centerCell=(%d,%d)",
+ drill.x(), drill.y(), drill.z(), int(2 * drill.x()), int(2 * drill.z()), int(2 * drill.y()),
+ int(drill.x() / 32.0f), int(drill.z() / 32.0f), drillCenter.x(), drillCenter.z(),
+ int(drillCenter.x() / 32.0f), int(drillCenter.z() / 32.0f));
if (!checkDrill(drill)) {
+ debugC(1, kFreescapeDebugMove, "DRILL rejected: placement check failed before gas distance was evaluated");
insertTemporaryMessage(_messagesList[4], _countdown - 2);
return;
}
_gameStateVars[k8bitVariableEnergy] = _gameStateVars[k8bitVariableEnergy] - 5;
- const Math::Vector3d gasPocket3D(gasPocket.x, drill.y(), gasPocket.y);
- float distanceToPocket = (gasPocket3D - drill).length();
- debugC(1, kFreescapeDebugMove, "Gas pocket position: %f %f %f", gasPocket3D.x(), gasPocket3D.y(), gasPocket3D.z());
- debugC(1, kFreescapeDebugMove, "Distance to gas pocket: %f", distanceToPocket);
-
- float success = _useAutomaticDrilling ? 100.0 : 100.0 * (1.0 - distanceToPocket / _currentArea->_gasPocketRadius);
+ const Math::Vector3d gasPocket3D(gasPocket.x, drillCenter.y(), gasPocket.y);
+ const float distanceToPocket = (gasPocket3D - drillCenter).length();
+ debugC(1, kFreescapeDebugMove, "DRILL gas pocket raw=(%d,%d,r=%u) world=(%d,%d) radius=%u",
+ gasPocket.x / 32, gasPocket.y / 32, gasPocketRadius / 32, gasPocket.x, gasPocket.y, gasPocketRadius);
+ debugC(1, kFreescapeDebugMove, "DRILL gas distance renderCenter=(%.2f,%.2f) gasWorld=(%d,%d) euclidean=%.2f worldRadius=%u",
+ drillCenter.x(), drillCenter.z(), gasPocket.x, gasPocket.y, distanceToPocket, gasPocketRadius);
+
+ float success = _useAutomaticDrilling ? 100.0f : 100.0f * (1.0f - distanceToPocket / gasPocketRadius);
+ debugC(1, kFreescapeDebugMove, "DRILL gas computed success=%.2f automatic=%d", success, _useAutomaticDrilling ? 1 : 0);
// Play the "processing" sound up front (matches BTF660 in the
// original Amiga code, where sound 5 starts before the
// RIG POSITIONED / NO GAS FOUND messages are displayed and
@@ -587,6 +603,7 @@ void DrillerEngine::pressedKey(const int keycode) {
insertTemporaryMessage(_messagesList[3], _countdown - 2);
addDrill(drill, success > 0);
if (success <= 0) {
+ debugC(1, kFreescapeDebugMove, "DRILL result: no gas found");
insertTemporaryMessage(_messagesList[9], _countdown - 4);
_drillStatusByArea[_currentArea->getAreaID()] = kDrillerRigNoGas;
return;
@@ -602,8 +619,9 @@ void DrillerEngine::pressedKey(const int keycode) {
insertTemporaryMessage(successMessage, _countdown - 6);
_drillSuccessByArea[_currentArea->getAreaID()] = uint32(success);
_gameStateVars[k8bitVariableScore] += uint32(maxScore * uint32(success)) / 100;
+ debugC(1, kFreescapeDebugMove, "DRILL result: gas found success=%.2f maxScore=%d score=%d", success, maxScore, _gameStateVars[k8bitVariableScore]);
- if (success >= 50.0) {
+ if (success >= 50.0f) {
_drillStatusByArea[_currentArea->getAreaID()] = kDrillerRigInPlace;
_gameStateVars[32]++;
} else
@@ -658,14 +676,28 @@ void DrillerEngine::pressedKey(const int keycode) {
Math::Vector3d DrillerEngine::drillPosition() {
Math::Vector3d position = _position;
position.setValue(1, position.y() - _playerHeight);
- position = position + 300 * getProjectionToPlane(_cameraFront, Math::Vector3d(0, 1, 0));
+ Math::Vector3d forward = getProjectionToPlane(_cameraFront, Math::Vector3d(0, 1, 0));
+ if (forward.length() > 0.0f)
+ forward.normalize();
Object *obj = (GeometricObject *)_areaMap[255]->objectWithID(255); // Drill base
assert(obj);
- position.setValue(2, position.z() - 128);
+ const Math::Vector3d center = position + forward * 192.0f;
+ position.setValue(0, center.x());
+ position.setValue(2, center.z() - obj->getSize().z() / 2.0f);
return position;
}
+float DrillerEngine::compassYaw() const {
+ float yaw = _yaw + 90.0f;
+ while (yaw < 0.0f)
+ yaw += 360.0f;
+ while (yaw >= 360.0f)
+ yaw -= 360.0f;
+
+ return yaw;
+}
+
bool DrillerEngine::drillDeployed(Area *area) {
return (area->objectWithID(255) != nullptr);
}
@@ -682,7 +714,8 @@ bool DrillerEngine::checkDrill(const Math::Vector3d position) {
origin.setValue(2, origin.z() + 128);
_drillBase->setOrigin(origin);
- if (_currentArea->checkCollisions(_drillBase->_boundingBox).empty())
+ ObjectArray collisions = _currentArea->checkCollisions(_drillBase->_boundingBox);
+ if (collisions.empty())
return false;
origin.setValue(0, origin.x() - 128);
@@ -696,13 +729,15 @@ bool DrillerEngine::checkDrill(const Math::Vector3d position) {
obj->setOrigin(origin);
// This bounding box is too large and can result in the drill to float next to a wall
- if (!_currentArea->checkCollisions(obj->_boundingBox).empty())
+ collisions = _currentArea->checkCollisions(obj->_boundingBox);
+ if (!collisions.empty())
return false;
origin.setValue(1, origin.y() + 15);
obj->setOrigin(origin);
- if (!_currentArea->checkCollisions(obj->_boundingBox).empty())
+ collisions = _currentArea->checkCollisions(obj->_boundingBox);
+ if (!collisions.empty())
return false;
origin.setValue(1, origin.y() - 10);
@@ -720,7 +755,8 @@ bool DrillerEngine::checkDrill(const Math::Vector3d position) {
obj = (GeometricObject *)obj->duplicate();
obj->setOrigin(origin);
- if (!_currentArea->checkCollisions(obj->_boundingBox).empty())
+ collisions = _currentArea->checkCollisions(obj->_boundingBox);
+ if (!collisions.empty())
return false;
// Undo offset
@@ -740,7 +776,8 @@ bool DrillerEngine::checkDrill(const Math::Vector3d position) {
origin.setValue(2, origin.z() + obj->getSize().z() / 5);
obj->setOrigin(origin);
- if (!_currentArea->checkCollisions(obj->_boundingBox).empty())
+ collisions = _currentArea->checkCollisions(obj->_boundingBox);
+ if (!collisions.empty())
return false;
// Undo offset
diff --git a/engines/freescape/games/driller/driller.h b/engines/freescape/games/driller/driller.h
index 6db575dc442..3041a23c5fe 100644
--- a/engines/freescape/games/driller/driller.h
+++ b/engines/freescape/games/driller/driller.h
@@ -84,6 +84,7 @@ private:
bool drillDeployed(Area *area);
GeometricObject *_drillBase;
Math::Vector3d drillPosition();
+ float compassYaw() const;
void addDrill(const Math::Vector3d position, bool gasFound);
bool checkDrill(const Math::Vector3d position);
void removeDrill(Area *area);
diff --git a/engines/freescape/games/driller/zx.cpp b/engines/freescape/games/driller/zx.cpp
index c0b7142dac4..ad89604a36e 100644
--- a/engines/freescape/games/driller/zx.cpp
+++ b/engines/freescape/games/driller/zx.cpp
@@ -161,7 +161,7 @@ void DrillerEngine::drawZXUI(Graphics::Surface *surface) {
surface->fillRect(shieldBar, front);
}
- drawCompass(surface, 103, 160, _yaw - 30, 10, 75, front);
+ drawCompass(surface, 103, 160, compassYaw() - 30, 10, 75, front);
drawCompass(surface, 220 - 3, 160, _pitch - 30, 10, 60, front);
}
More information about the Scummvm-git-logs
mailing list