[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