[Scummvm-git-logs] scummvm master -> 04795e7633ec19cf515870c8eb69f55116cbd3d8
neuromancer
noreply at scummvm.org
Sat May 23 19:15:36 UTC 2026
This automated email contains information about 7 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
e9e31ca2d4 FREESCAPE: improve AABB with a case where BB overlaps the geometry
f2293e631a FREESCAPE: initial support for castle master (amiga)
2e7f792636 FREESCAPE: improve castle amiga endgame
6cef8e0f34 FREESCAPE: experimental support red/blue stereo 3d images for the zx render
05707392b9 FREESCAPE: removed buggy collision workaround
4c18772a98 FREESCAPE: sensor fixes, in particular for driller amiga/atari
04795e7633 FREESCAPE: make sure saving/loading keep the game state consistent specially when the player dies
Commit: e9e31ca2d469c9053ae9071027ad167b3f96a3a6
https://github.com/scummvm/scummvm/commit/e9e31ca2d469c9053ae9071027ad167b3f96a3a6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-23T20:56:50+02:00
Commit Message:
FREESCAPE: improve AABB with a case where BB overlaps the geometry
Changed paths:
engines/freescape/sweepAABB.cpp
diff --git a/engines/freescape/sweepAABB.cpp b/engines/freescape/sweepAABB.cpp
index 426c0e15c38..0055fd3b1e8 100644
--- a/engines/freescape/sweepAABB.cpp
+++ b/engines/freescape/sweepAABB.cpp
@@ -44,6 +44,37 @@ float sweepAABB(Math::AABB const &a, Math::AABB const &b, Math::Vector3d const &
Math::Vector3d m = b.getMin() - a.getMax();
Math::Vector3d mh = a.getSize() + b.getSize();
+ // Overlap-at-start: the AABBs already intersect on every axis. The
+ // plane sweep below requires s >= 0 for every test, so it would
+ // incorrectly return "no collision" and the caller would let the
+ // player walk straight through the geometry they are already inside.
+ // Detect that case here, pick the axis of smallest overlap, and emit
+ // a t=0 collision with a unit normal pointing out along that axis.
+ // The caller's epsilon * normal push then pries the player out of
+ // the overlap incrementally over a few frames.
+ if (m.x() < 0 && m.x() + mh.x() > 0 &&
+ m.y() < 0 && m.y() + mh.y() > 0 &&
+ m.z() < 0 && m.z() + mh.z() > 0) {
+ // Overlap depth on the "push -axis" side vs the "push +axis" side
+ // (both values are positive while we are overlapping).
+ float ovNegX = -m.x();
+ float ovPosX = m.x() + mh.x();
+ float ovNegY = -m.y();
+ float ovPosY = m.y() + mh.y();
+ float ovNegZ = -m.z();
+ float ovPosZ = m.z() + mh.z();
+ float minX = MIN(ovNegX, ovPosX);
+ float minY = MIN(ovNegY, ovPosY);
+ float minZ = MIN(ovNegZ, ovPosZ);
+ if (minX <= minY && minX <= minZ)
+ normal = Math::Vector3d(ovNegX < ovPosX ? -1.0f : 1.0f, 0, 0);
+ else if (minY <= minZ)
+ normal = Math::Vector3d(0, ovNegY < ovPosY ? -1.0f : 1.0f, 0);
+ else
+ normal = Math::Vector3d(0, 0, ovNegZ < ovPosZ ? -1.0f : 1.0f);
+ return 0.0f;
+ }
+
float h = 1.0;
float s = 0.0;
Math::Vector3d zero;
Commit: f2293e631a10d5a8e3bd19d1e19cfe26c8adb783
https://github.com/scummvm/scummvm/commit/f2293e631a10d5a8e3bd19d1e19cfe26c8adb783
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-23T20:56:50+02:00
Commit Message:
FREESCAPE: initial support for castle master (amiga)
Changed paths:
engines/freescape/detection.cpp
engines/freescape/freescape.h
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/sound/amiga.cpp
diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index 23d98d3b388..798e4376b59 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -880,6 +880,20 @@ static const ADGameDescription gameDescriptions[] = {
ADGF_DEMO,
GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERAMIGA, GAMEOPTION_WASD_CONTROLS)
},
+ // Full Castle Master, Amiga
+ {
+ "castlemaster",
+ "",
+ {
+ {"cm", 0, "67dcb3f62fe15f18ecc31380ffaf5c4e", 1112},
+ {"x", 0, "afc245de66ef8eb1b5fd061c6bbd602e", 349975},
+ AD_LISTEND
+ },
+ Common::EN_ANY,
+ Common::kPlatformAmiga,
+ ADGF_UNSTABLE,
+ GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERAMIGA, GAMEOPTION_WASD_CONTROLS)
+ },
{
"castlemaster",
"",
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index f63766a0255..a7ada28eba7 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -534,7 +534,7 @@ public:
Common::Array<byte> _soundsCPCEnvelopeTable;
Common::Array<byte> _soundsCPCSoundDefTable;
- void loadSoundsAmigaDemo(Common::SeekableReadStream *file, int offset, int numSounds);
+ void loadSoundsAmigaDemo(Common::SeekableReadStream *file, int offset, int numSounds, int modOffset = 0x3D5A6);
void playSoundAmiga(int index, Audio::SoundHandle &handle);
Common::Array<AmigaSfxEntry> _amigaSfxTable;
Common::Array<AmigaDmaSample> _amigaDmaSamples;
diff --git a/engines/freescape/games/castle/amiga.cpp b/engines/freescape/games/castle/amiga.cpp
index 6e6795138a4..6ef8248c9e1 100644
--- a/engines/freescape/games/castle/amiga.cpp
+++ b/engines/freescape/games/castle/amiga.cpp
@@ -69,6 +69,914 @@ byte kAmigaCastleRiddlePalette[16][3] = {
{0xee, 0xcc, 0x66},
};
+class CastleAmigaIntroPlayer {
+public:
+ CastleAmigaIntroPlayer(CastleEngine *engine, const Common::Array<byte> &introText)
+ : _engine(engine), _data(introText) {
+ _screen[0].resize(kScreenBytes);
+ _screen[1].resize(kScreenBytes);
+ reset();
+ }
+
+ bool run(bool &selectedPrincess) {
+ selectedPrincess = false;
+
+ clearDrawBuffer();
+ drawBaseScreen();
+ drawStaticLogo();
+ drawStaticForeground();
+ fadePalette(0x1dc6);
+ pageFlip();
+ displayFrame(false, false);
+ fadePalette(0x1de6);
+
+ int key = 0;
+ while (!_aborted) {
+ clearDrawBuffer();
+ drawBaseScreen();
+ drawStaticForeground();
+ drawAnimatedObject();
+ drawStaticLogo();
+ key = displayFrame(false, true);
+ if (key || _phaseDone)
+ break;
+ pageFlip();
+ displayFrame(false, false);
+ updateAnimation();
+ _frameCounter++;
+ }
+
+ if (_aborted)
+ return false;
+
+ _phaseDone = 0;
+ _renderMode = 2;
+ _motionPtr = 0x2054;
+ updateAnimation();
+
+ clearDrawBuffer();
+ drawBaseScreen();
+ drawMovingCharacters();
+ drawOverlay();
+ drawLanguageText();
+ fadePalette(0x1dc6);
+ pageFlip();
+ displayFrame(false, false);
+ displayFrame(false, false);
+ fadePalette(0x1e06);
+
+ setSelectionMouseEnabled(true);
+ while (!_aborted) {
+ clearDrawBuffer();
+ drawBaseScreen();
+ drawMovingCharacters();
+ drawOverlay();
+ if (_choiceState == 0)
+ drawLanguageText();
+
+ key = displayFrame(true, false);
+ if (key == 1 || key == 2)
+ acceptSelection(key);
+
+ if (_phaseDone == 2 && _frameCounter > 5)
+ break;
+
+ pageFlip();
+ displayFrame(false, false);
+ updateAnimation();
+ _frameCounter++;
+ }
+ setSelectionMouseEnabled(false);
+
+ if (_aborted)
+ return false;
+
+ selectedPrincess = (_choice == 2);
+
+ _renderMode = 3;
+ _choiceState = 0;
+ _phaseDone = 0;
+ clearDrawBuffer();
+ _motionPtr = 0x2256;
+ updateAnimation();
+ drawBaseScreen();
+ drawStaticForeground();
+ drawAnimatedObject();
+ drawStaticLogo();
+ fadePalette(0x1dc6);
+ pageFlip();
+ displayFrame(false, false);
+ displayFrame(false, false);
+ fadePalette(0x1de6);
+
+ int guard = 0;
+ while (!_aborted && !_phaseDone && guard++ < 600) {
+ clearDrawBuffer();
+ drawBaseScreen();
+ drawStaticForeground();
+ drawAnimatedObject();
+ drawStaticLogo();
+ displayFrame(false, false);
+ pageFlip();
+ displayFrame(false, false);
+ updateAnimation();
+ _frameCounter++;
+ }
+
+ return !_aborted;
+ }
+
+private:
+ static const int kWidth = 320;
+ static const int kHeight = 200;
+ static const int kPlaneBytes = 0x1f40;
+ static const int kRowBytes = 40;
+ static const int kScreenBytes = 0x9c40;
+ static const int kVbiDelayMillis = 20;
+
+ CastleEngine *_engine;
+ const Common::Array<byte> &_data;
+ Common::Array<byte> _screen[2];
+ uint16 _palette[32];
+ int _displayBuffer;
+ int _drawBuffer;
+ bool _pageFlag;
+ bool _aborted;
+
+ int32 _scrollA;
+ int32 _scrollB;
+ int16 _sprite1Frame;
+ int16 _sprite1X;
+ int16 _sprite1Y;
+ int16 _sprite2Frame;
+ int16 _sprite2X;
+ int16 _sprite2Y;
+ int16 _choiceState;
+ int16 _frameCounter;
+ int16 _selectionLift;
+ int16 _selectionX;
+ int16 _selectionY;
+ int16 _objectFrame;
+ int16 _objectX;
+ int16 _objectY;
+ int16 _phaseDone;
+ int16 _renderMode;
+ int16 _arrowFrame;
+ int16 _choice;
+ uint32 _character1Ptr;
+ uint32 _character2Ptr;
+ uint32 _motionPtr;
+
+ void reset() {
+ memset(_screen[0].data(), 0, kScreenBytes);
+ memset(_screen[1].data(), 0, kScreenBytes);
+ memset(_palette, 0, sizeof(_palette));
+
+ _displayBuffer = 0;
+ _drawBuffer = 1;
+ _pageFlag = false;
+ _aborted = false;
+ _scrollA = 0;
+ _scrollB = 0;
+ _sprite1Frame = 0;
+ _sprite1X = 40;
+ _sprite1Y = 75;
+ _sprite2Frame = 0;
+ _sprite2X = 124;
+ _sprite2Y = 86;
+ _choiceState = 0;
+ _frameCounter = 0;
+ _selectionLift = 0;
+ _selectionX = 0;
+ _selectionY = -250;
+ _objectFrame = 0;
+ _objectX = 160;
+ _objectY = 100;
+ _phaseDone = 0;
+ _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);
+ }
+
+ static int16 highWord(int32 value) {
+ return (int16)((uint32)value >> 16);
+ }
+
+ uint16 screenWord(int offset) const {
+ if (offset < 0 || offset + 1 >= kScreenBytes)
+ return 0;
+ const Common::Array<byte> &buf = _screen[_drawBuffer];
+ return (buf[offset] << 8) | buf[offset + 1];
+ }
+
+ void setScreenWord(int offset, uint16 value) {
+ if (offset < 0 || offset + 1 >= kScreenBytes)
+ return;
+ Common::Array<byte> &buf = _screen[_drawBuffer];
+ buf[offset] = value >> 8;
+ buf[offset + 1] = value & 0xff;
+ }
+
+ void setScreenLong(int offset, uint32 value) {
+ if (offset < 0 || offset + 3 >= kScreenBytes)
+ return;
+ Common::Array<byte> &buf = _screen[_drawBuffer];
+ WRITE_BE_UINT32(&buf[offset], value);
+ }
+
+ void andScreenWord(int offset, uint16 value) {
+ setScreenWord(offset, screenWord(offset) & value);
+ }
+
+ void orScreenWord(int offset, uint16 value) {
+ setScreenWord(offset, screenWord(offset) | value);
+ }
+
+ static uint16 shiftedWord(uint32 value, int shift) {
+ return (uint16)((value << shift) >> 16);
+ }
+
+ void clearDrawBuffer() {
+ memset(_screen[_drawBuffer].data(), 0, kScreenBytes);
+ }
+
+ void drawBaseScreen() {
+ int dst = 38 * kRowBytes;
+ dst = drawScrollingPlane(0x247c, _scrollA, 57, dst);
+ dst = drawScrollingFourPlane(0x2d64, _scrollB, 40, dst);
+ dst = drawStaticFourPlane(0x4664, 33, dst);
+ drawFillBands(dst);
+ }
+
+ int drawScrollingPlane(int src, int32 scroll, int rows, int dst) {
+ int d7 = highWord(scroll);
+ int srcOffset = ((d7 >> 3) & 0x3e);
+ int shift = d7 & 0xf;
+ int skip = -(((srcOffset >> 1) - 0x12));
+ src += srcOffset;
+
+ for (int row = 0; row < rows; row++) {
+ int rowSrc = src + 0x28;
+ int run = skip;
+ int curSrc = src;
+ for (int col = 0; col < 20; col++) {
+ uint32 val;
+ if (run >= 0) {
+ val = READ_BE_UINT32(_data.data() + curSrc);
+ run--;
+ } else {
+ run = 0xff;
+ val = ((uint32)READ_BE_UINT16(_data.data() + curSrc) << 16) | READ_BE_UINT16(_data.data() + curSrc - 0x26);
+ curSrc -= 0x28;
+ }
+ uint16 word = shiftedWord(val, shift);
+ setScreenWord(dst + kPlaneBytes, 0);
+ setScreenWord(dst + kPlaneBytes * 2, 0);
+ setScreenWord(dst + kPlaneBytes * 3, 0);
+ setScreenWord(dst + kPlaneBytes * 4, 0);
+ setScreenWord(dst, word);
+ curSrc += 2;
+ dst += 2;
+ }
+ src = rowSrc;
+ }
+ return dst;
+ }
+
+ int drawScrollingFourPlane(int src, int32 scroll, int rows, int dst) {
+ int d7 = highWord(scroll);
+ int srcOffset = ((d7 >> 3) & 0x3e);
+ int shift = d7 & 0xf;
+ int skip = -(((srcOffset >> 1) - 0x12));
+ src += srcOffset;
+
+ for (int row = 0; row < rows; row++) {
+ int rowSrc = src + 0xa0;
+ int run = skip;
+ int curSrc = src;
+ for (int col = 0; col < 20; col++) {
+ uint32 p0, p1, p2, p3;
+ if (run >= 0) {
+ p0 = READ_BE_UINT32(_data.data() + curSrc);
+ p1 = READ_BE_UINT32(_data.data() + curSrc + 0x28);
+ p2 = READ_BE_UINT32(_data.data() + curSrc + 0x50);
+ p3 = READ_BE_UINT32(_data.data() + curSrc + 0x78);
+ run--;
+ } else {
+ run = 0xff;
+ p0 = ((uint32)READ_BE_UINT16(_data.data() + curSrc) << 16) | READ_BE_UINT16(_data.data() + curSrc - 0x26);
+ p1 = ((uint32)READ_BE_UINT16(_data.data() + curSrc + 0x28) << 16) | READ_BE_UINT16(_data.data() + curSrc + 0x02);
+ p2 = ((uint32)READ_BE_UINT16(_data.data() + curSrc + 0x50) << 16) | READ_BE_UINT16(_data.data() + curSrc + 0x2a);
+ p3 = ((uint32)READ_BE_UINT16(_data.data() + curSrc + 0x78) << 16) | READ_BE_UINT16(_data.data() + curSrc + 0x52);
+ curSrc -= 0x28;
+ }
+ setScreenLong(dst, p0 << shift);
+ setScreenLong(dst + kPlaneBytes, p1 << shift);
+ setScreenLong(dst + kPlaneBytes * 2, p2 << shift);
+ setScreenLong(dst + kPlaneBytes * 3, p3 << shift);
+ curSrc += 2;
+ dst += 2;
+ }
+ src = rowSrc;
+ }
+ return dst;
+ }
+
+ int drawStaticFourPlane(int src, int rows, int dst) {
+ for (int row = 0; row < rows; row++) {
+ for (int col = 0; col < 20; col++) {
+ setScreenWord(dst, READ_BE_UINT16(_data.data() + src));
+ setScreenWord(dst + kPlaneBytes, READ_BE_UINT16(_data.data() + src + 0x28));
+ setScreenWord(dst + kPlaneBytes * 2, READ_BE_UINT16(_data.data() + src + 0x50));
+ setScreenWord(dst + kPlaneBytes * 3, READ_BE_UINT16(_data.data() + src + 0x78));
+ src += 2;
+ dst += 2;
+ }
+ src += 0x78;
+ }
+ return dst;
+ }
+
+ void drawFillBands(int dst) {
+ int src = 0x5ba4;
+ for (int band = 0; band < 13; band++) {
+ int count = READ_BE_UINT16(_data.data() + src);
+ src += 2;
+ uint16 p0 = READ_BE_UINT16(_data.data() + src);
+ uint16 p1 = READ_BE_UINT16(_data.data() + src + 2);
+ uint16 p2 = READ_BE_UINT16(_data.data() + src + 4);
+ src += 6;
+ for (int repeat = 0; repeat <= count; repeat++) {
+ for (int col = 0; col < 20; col++) {
+ setScreenWord(dst, p0);
+ setScreenWord(dst + kPlaneBytes, p1);
+ setScreenWord(dst + kPlaneBytes * 2, p2);
+ setScreenWord(dst + kPlaneBytes * 3, 0);
+ dst += 2;
+ }
+ }
+ }
+ }
+
+ void drawOverlay() {
+ int src = 0x5c0c;
+ int dst = 0x1a18;
+ for (int row = 0; row < 30; row++) {
+ for (int col = 0; col < 20; col++) {
+ uint16 p0 = READ_BE_UINT16(_data.data() + src);
+ uint16 p1 = READ_BE_UINT16(_data.data() + src + 0x28);
+ uint16 p2 = READ_BE_UINT16(_data.data() + src + 0x50);
+ uint16 p3 = READ_BE_UINT16(_data.data() + src + 0x78);
+ uint16 mask = ~(p0 | p1 | p2 | p3);
+ andScreenWord(dst, mask);
+ orScreenWord(dst, p0);
+ andScreenWord(dst + kPlaneBytes, mask);
+ orScreenWord(dst + kPlaneBytes, p1);
+ andScreenWord(dst + kPlaneBytes * 2, mask);
+ orScreenWord(dst + kPlaneBytes * 2, p2);
+ andScreenWord(dst + kPlaneBytes * 3, mask);
+ orScreenWord(dst + kPlaneBytes * 3, p3);
+ src += 2;
+ dst += 2;
+ }
+ src += 0x78;
+ }
+ for (int i = 0; i < 60; i++) {
+ setScreenWord(dst, 0);
+ setScreenWord(dst + kPlaneBytes, 0);
+ setScreenWord(dst + kPlaneBytes * 2, 0);
+ setScreenWord(dst + kPlaneBytes * 3, 0);
+ dst += 2;
+ }
+ }
+
+ void drawMaskedBlock4(int src, int dst, int rows, int words) {
+ for (int row = 0; row < rows; row++) {
+ for (int col = 0; col < words; col++) {
+ uint16 p0 = READ_BE_UINT16(_data.data() + src);
+ uint16 p1 = READ_BE_UINT16(_data.data() + src + 2);
+ uint16 p2 = READ_BE_UINT16(_data.data() + src + 4);
+ uint16 p3 = READ_BE_UINT16(_data.data() + src + 6);
+ uint16 mask = ~(p0 | p1 | p2 | p3);
+ andScreenWord(dst, mask);
+ orScreenWord(dst, p0);
+ andScreenWord(dst + kPlaneBytes, mask);
+ orScreenWord(dst + kPlaneBytes, p1);
+ andScreenWord(dst + kPlaneBytes * 2, mask);
+ orScreenWord(dst + kPlaneBytes * 2, p2);
+ andScreenWord(dst + kPlaneBytes * 3, mask);
+ orScreenWord(dst + kPlaneBytes * 3, p3);
+ src += 8;
+ dst += 2;
+ }
+ }
+ }
+
+ void drawStaticLogo() {
+ drawMaskedBlock4(0x14ff4, 0x820, 0x94, 20);
+ }
+
+ void drawStaticForeground() {
+ drawMaskedBlock4(0x1ac74, 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);
+ if (_choiceState != 0 && _selectionLift == 0) {
+ drawLargeSprite4(0x108ac, 0, 0, _selectionX - 0x20, _selectionY - 0x76, 0x95, 0x40, 0x10, 8);
+ return;
+ }
+ drawAnimatedObject();
+ }
+
+ void drawAnimatedObject() {
+ bool objectVisible = true;
+ if (_objectY < 0 && -_objectY >= 13) {
+ _selectionLift = 0;
+ objectVisible = false;
+ } else {
+ drawSprite1(0x12dec + _objectFrame * 0x34, _objectX, _objectY, 13);
+ }
+ if (objectVisible && _renderMode == 3) {
+ int src = 0x136dc;
+ if (_choice != 2)
+ src += 0x36;
+ src += _arrowFrame * 18;
+ _arrowFrame++;
+ if (_arrowFrame > 2)
+ _arrowFrame = 0;
+ drawSmallSamePlane(src, _objectX + 8, _objectY + 8);
+ }
+ }
+
+ void drawLargeSprite4(int base, int frame, int frameSize, int x, int y, int rows, int rowStride, int planeStride, int maxWords) {
+ int src = base + frame * frameSize;
+ int rowCount = rows;
+ int dstY = y;
+ if (dstY < 0) {
+ int skip = -dstY;
+ if (skip >= rowCount)
+ return;
+ rowCount -= skip;
+ src += skip * rowStride;
+ dstY = 0;
+ }
+ if (dstY >= kHeight)
+ return;
+ if (dstY + rowCount > kHeight)
+ rowCount = kHeight - dstY;
+ drawSprite4(src, x, dstY, rowCount, rowStride, planeStride, maxWords);
+ }
+
+ void drawSprite4(int src, int x, int y, int rows, int rowStride, int planeStride, int maxWords) {
+ if (x >= kWidth)
+ return;
+ int dstBase = y * kRowBytes + ((x >> 3) & 0xfffe);
+ int visible = kWidth - x;
+ if (visible <= 0)
+ return;
+ int d0 = visible >> 4;
+ if (d0 > maxWords)
+ d0 = maxWords;
+ int shift = (16 - (x & 0xf)) & 0xf;
+ if (shift == 0)
+ d0--;
+ if (d0 < 0)
+ return;
+
+ for (int row = 0; row < rows; row++) {
+ int dst = dstBase + row * kRowBytes;
+ for (int col = 0; col <= d0; col++) {
+ uint32 values[4];
+ for (int plane = 0; plane < 4; plane++) {
+ int planeSrc = src + plane * planeStride;
+ if (shift && col == 0)
+ values[plane] = READ_BE_UINT16(_data.data() + planeSrc);
+ else {
+ values[plane] = READ_BE_UINT32(_data.data() + planeSrc + (shift ? (col - 1) * 2 : col * 2));
+ if (col == d0)
+ values[plane] &= 0xffff0000;
+ }
+ }
+
+ uint16 p0 = shiftedWord(values[0], shift);
+ uint16 p1 = shiftedWord(values[1], shift);
+ uint16 p2 = shiftedWord(values[2], shift);
+ uint16 p3 = shiftedWord(values[3], shift);
+ uint16 mask = ~(p0 | p1 | p2 | p3);
+ andScreenWord(dst, mask);
+ orScreenWord(dst, p0);
+ andScreenWord(dst + kPlaneBytes, mask);
+ orScreenWord(dst + kPlaneBytes, p1);
+ andScreenWord(dst + kPlaneBytes * 2, mask);
+ orScreenWord(dst + kPlaneBytes * 2, p2);
+ andScreenWord(dst + kPlaneBytes * 3, mask);
+ orScreenWord(dst + kPlaneBytes * 3, p3);
+ dst += 2;
+ }
+ src += rowStride;
+ }
+ }
+
+ void drawSprite1(int src, int x, int y, int rows) {
+ if (x >= kWidth || y >= kHeight)
+ return;
+ if (y < 0) {
+ int skip = -y;
+ if (skip >= rows)
+ return;
+ rows -= skip;
+ src += skip * 4;
+ y = 0;
+ }
+ if (y + rows > kHeight)
+ rows = kHeight - y;
+
+ int visible = kWidth - x;
+ if (visible <= 0)
+ return;
+ int d0 = visible >> 4;
+ if (d0 > 2)
+ d0 = 2;
+ int shift = (16 - (x & 0xf)) & 0xf;
+ if (shift == 0)
+ d0--;
+ if (d0 < 0)
+ return;
+
+ int dstBase = y * kRowBytes + ((x >> 3) & 0xfffe);
+ for (int row = 0; row < rows; row++) {
+ int dst = dstBase + row * kRowBytes;
+ for (int col = 0; col <= d0; col++) {
+ uint32 value;
+ if (shift && col == 0)
+ value = READ_BE_UINT16(_data.data() + src);
+ else {
+ value = READ_BE_UINT32(_data.data() + src + (shift ? (col - 1) * 2 : col * 2));
+ if (col == d0 && x <= 0x120)
+ value &= 0xffff0000;
+ }
+ uint16 p = shiftedWord(value, shift);
+ uint16 mask = ~p;
+ if (_renderMode == 2) {
+ andScreenWord(dst + kPlaneBytes, mask);
+ andScreenWord(dst + kPlaneBytes * 2, mask);
+ andScreenWord(dst + kPlaneBytes * 4, mask);
+ orScreenWord(dst + kPlaneBytes * 4, p);
+ andScreenWord(dst + kPlaneBytes * 3, mask);
+ orScreenWord(dst + kPlaneBytes * 3, p);
+ andScreenWord(dst, mask);
+ orScreenWord(dst, p);
+ } else {
+ andScreenWord(dst, mask);
+ orScreenWord(dst, p);
+ andScreenWord(dst + kPlaneBytes, mask);
+ orScreenWord(dst + kPlaneBytes, p);
+ andScreenWord(dst + kPlaneBytes * 2, mask);
+ orScreenWord(dst + kPlaneBytes * 2, p);
+ andScreenWord(dst + kPlaneBytes * 3, mask);
+ orScreenWord(dst + kPlaneBytes * 3, p);
+ }
+ dst += 2;
+ }
+ src += 4;
+ }
+ }
+
+ void drawSmallSamePlane(int src, int x, int y) {
+ int rows = 9;
+ if (y < 0) {
+ int skip = -y;
+ if (skip >= rows)
+ return;
+ rows -= skip;
+ src += skip * 2;
+ y = 0;
+ }
+ if (y >= kHeight || x >= kWidth)
+ return;
+ if (y + rows > kHeight)
+ rows = kHeight - y;
+
+ int dstBase = y * kRowBytes + ((x >> 3) & 0xfffe);
+ int shift = (16 - (x & 0xf)) & 0x1f;
+ int words = (kWidth - x) >> 4;
+ if (words > 1)
+ words = 1;
+ if (words < 0)
+ return;
+
+ for (int row = 0; row < rows; row++) {
+ uint32 value = (uint32)READ_BE_UINT16(_data.data() + src) << shift;
+ uint16 right = value & 0xffff;
+ uint16 left = (value >> 16) & 0xffff;
+ int dst = dstBase + row * kRowBytes;
+ if (words != 0)
+ orSamePlanes(dst + 2, right);
+ orSamePlanes(dst, left);
+ src += 2;
+ }
+ }
+
+ void orSamePlanes(int dst, uint16 bits) {
+ uint16 mask = ~bits;
+ for (int plane = 0; plane < 4; plane++) {
+ andScreenWord(dst + plane * kPlaneBytes, mask);
+ orScreenWord(dst + plane * kPlaneBytes, bits);
+ }
+ }
+
+ void drawLanguageText() {
+ int table = currentLanguageTextTable();
+
+ for (int i = 0; i < 5; i++) {
+ int src = READ_BE_UINT32(_data.data() + table);
+ int x = (int16)READ_BE_UINT16(_data.data() + table + 4);
+ int y = (int16)READ_BE_UINT16(_data.data() + table + 6);
+ int width = (int16)READ_BE_UINT16(_data.data() + table + 8) + 1;
+ int rows = (int16)READ_BE_UINT16(_data.data() + table + 10) + 1;
+ drawLanguageBlock(src, x, y, width, rows);
+ table += 12;
+ }
+ }
+
+ int currentLanguageTextTable() const {
+ if (_engine->_language == Common::FR_FRA)
+ return 0x1d2e;
+ if (_engine->_language == Common::DE_DEU)
+ return 0x1d6a;
+ return 0x1cf2;
+ }
+
+ int selectionSplitX() const {
+ int table = currentLanguageTextTable();
+ int princeEntry = table + 3 * 12;
+ int princessEntry = table + 4 * 12;
+ int princeX = (int16)READ_BE_UINT16(_data.data() + princeEntry + 4);
+ int princeWords = (int16)READ_BE_UINT16(_data.data() + princeEntry + 8) + 2;
+ int princessX = (int16)READ_BE_UINT16(_data.data() + princessEntry + 4);
+ if (princeWords <= 0 || princessX <= princeX)
+ return kWidth / 2;
+ return (princeX + princeWords * 16 + princessX) / 2;
+ }
+
+ void drawLanguageBlock(int src, int x, int y, int sourceWords, int rows) {
+ if (y >= kHeight || x >= kWidth)
+ return;
+ int dstBase = y * kRowBytes + ((x >> 3) & 0xfffe);
+ int shift = (16 - (x & 0xf)) & 0xf;
+ for (int row = 0; row < rows; row++) {
+ int dst = dstBase + row * kRowBytes;
+ uint16 prev0 = 0;
+ uint16 prev1 = 0;
+ uint16 prev2 = 0;
+ uint16 prev3 = 0;
+ for (int col = 0; col <= sourceWords; col++) {
+ uint32 p0;
+ uint32 p1;
+ uint32 p2;
+ uint32 p3;
+ if (col < sourceWords) {
+ uint16 cur0 = READ_BE_UINT16(_data.data() + src);
+ uint16 cur1 = READ_BE_UINT16(_data.data() + src + 2);
+ uint16 cur2 = READ_BE_UINT16(_data.data() + src + 4);
+ uint16 cur3 = READ_BE_UINT16(_data.data() + src + 6);
+ if (col == 0) {
+ p0 = cur0;
+ p1 = cur1;
+ p2 = cur2;
+ p3 = cur3;
+ } else {
+ p0 = ((uint32)prev0 << 16) | cur0;
+ p1 = ((uint32)prev1 << 16) | cur1;
+ p2 = ((uint32)prev2 << 16) | cur2;
+ p3 = ((uint32)prev3 << 16) | cur3;
+ }
+ prev0 = cur0;
+ prev1 = cur1;
+ prev2 = cur2;
+ prev3 = cur3;
+ src += 8;
+ } else {
+ p0 = 0;
+ p1 = 0;
+ p2 = 0;
+ p3 = 0;
+ }
+ uint16 w0 = shiftedWord(p0, shift);
+ uint16 w1 = shiftedWord(p1, shift);
+ uint16 w2 = shiftedWord(p2, shift);
+ uint16 w3 = shiftedWord(p3, shift);
+ uint16 mask = ~(w0 | w1 | w2 | w3);
+ andScreenWord(dst, mask);
+ orScreenWord(dst, w0);
+ andScreenWord(dst + kPlaneBytes, mask);
+ orScreenWord(dst + kPlaneBytes, w1);
+ andScreenWord(dst + kPlaneBytes * 2, mask);
+ orScreenWord(dst + kPlaneBytes * 2, w2);
+ andScreenWord(dst + kPlaneBytes * 3, mask);
+ orScreenWord(dst + kPlaneBytes * 3, w3);
+ dst += 2;
+ }
+ }
+ }
+
+ void fadePalette(int target) {
+ for (int threshold = 15; threshold > 0 && !_aborted; threshold--) {
+ for (int i = 0; i < 16; i++) {
+ uint16 dst = READ_BE_UINT16(_data.data() + target + i * 2);
+ 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};
+ for (int c = 0; c < 3; c++) {
+ int diff = targets[c] - channels[c];
+ if (ABS(diff) >= threshold && diff != 0)
+ channels[c] += diff > 0 ? 1 : -1;
+ }
+ _palette[i] = (channels[2] << 8) | (channels[1] << 4) | channels[0];
+ }
+ displayFrame(false, false, 4);
+ }
+ }
+
+ void pageFlip() {
+ _pageFlag = !_pageFlag;
+ if (_pageFlag) {
+ _displayBuffer = 0;
+ _drawBuffer = 1;
+ } else {
+ _displayBuffer = 1;
+ _drawBuffer = 0;
+ }
+ }
+
+ void updateAnimation() {
+ _scrollA += 0x8000;
+ if (highWord(_scrollA) >= 0x13f)
+ _scrollA -= 0x13f0000;
+ _scrollB += 0x10000;
+ if (highWord(_scrollB) >= 0x13f)
+ _scrollB -= 0x13f0000;
+
+ if (_choiceState == 1)
+ updateCharacterPath(_character1Ptr, _sprite1Frame, _sprite1X, _sprite1Y);
+ if (_choiceState == 2)
+ updateCharacterPath(_character2Ptr, _sprite2Frame, _sprite2X, _sprite2Y);
+
+ if (_choiceState < 0 && _selectionLift == 0) {
+ _selectionX = (_choiceState == -1) ? _sprite1X : _sprite2X;
+ _selectionY += 12;
+ if (_selectionY > 0x4b)
+ _choiceState = -_choiceState;
+ }
+
+ if (_renderMode == 1 && _frameCounter <= 10)
+ return;
+
+ if (READ_BE_UINT32(_data.data() + _motionPtr) == 0xffffffff) {
+ _phaseDone = 1;
+ _motionPtr = 0x21b0;
+ }
+ _objectFrame = (int16)READ_BE_UINT16(_data.data() + _motionPtr);
+ _objectX = (int16)READ_BE_UINT16(_data.data() + _motionPtr + 2);
+ _objectY = (int16)READ_BE_UINT16(_data.data() + _motionPtr + 4);
+ _motionPtr += 6;
+ if (_selectionLift != 0 && _renderMode == 2) {
+ _objectY -= _selectionLift;
+ _selectionLift += 2;
+ }
+ }
+
+ void updateCharacterPath(uint32 &ptr, int16 &frame, int16 &x, int16 &y) {
+ if (READ_BE_UINT32(_data.data() + ptr) & 0x80000000) {
+ if (_phaseDone != 2) {
+ _phaseDone = 2;
+ _frameCounter = 0;
+ }
+ return;
+ }
+ frame = (int16)READ_BE_UINT16(_data.data() + ptr);
+ x = (int16)READ_BE_UINT16(_data.data() + ptr + 2);
+ y = (int16)READ_BE_UINT16(_data.data() + ptr + 4);
+ _selectionX = x;
+ _selectionY = y;
+ ptr += 6;
+ }
+
+ void acceptSelection(int key) {
+ if (_choiceState != 0)
+ return;
+ _choice = key;
+ _selectionLift = 1;
+ _choiceState = (key == 1) ? -2 : -1;
+ }
+
+ void setSelectionMouseEnabled(bool enabled) {
+ if (enabled) {
+ CursorMan.setDefaultArrowCursor();
+ g_system->lockMouse(false);
+ CursorMan.showMouse(true);
+ } else {
+ g_system->lockMouse(true);
+ CursorMan.showMouse(false);
+ }
+ }
+
+ uint32 paletteColor(uint16 value) const {
+ byte r = ((value >> 8) & 0xf) * 0x11;
+ byte g = ((value >> 4) & 0xf) * 0x11;
+ byte b = (value & 0xf) * 0x11;
+ return _engine->_gfx->_texturePixelFormat.ARGBToColor(0xff, r, g, b);
+ }
+
+ int displayFrame(bool acceptSelection, bool acceptAnyKey, int ticks = 1) {
+ int key = pumpEvents(acceptSelection, acceptAnyKey);
+
+ Graphics::Surface surface;
+ surface.create(kWidth, kHeight, _engine->_gfx->_texturePixelFormat);
+ const Common::Array<byte> &buf = _screen[_displayBuffer];
+ uint32 colors[32];
+ for (int i = 0; i < 32; i++)
+ colors[i] = i < 16 ? paletteColor(_palette[i]) : paletteColor(0);
+
+ for (int y = 0; y < kHeight; y++) {
+ int row = y * kRowBytes;
+ for (int x = 0; x < kWidth; x++) {
+ int byteOffset = row + (x >> 3);
+ byte bit = 0x80 >> (x & 7);
+ byte color = 0;
+ for (int plane = 0; plane < 5; plane++) {
+ if (buf[byteOffset + plane * kPlaneBytes] & bit)
+ color |= 1 << plane;
+ }
+ surface.setPixel(x, y, colors[color]);
+ }
+ }
+
+ _engine->_gfx->clear(0, 0, 0, true);
+ _engine->drawFullscreenSurface(&surface);
+ _engine->_gfx->flipBuffer();
+ g_system->updateScreen();
+ g_system->delayMillis(kVbiDelayMillis * ticks);
+ surface.free();
+ return key;
+ }
+
+ int pumpEvents(bool acceptSelection, bool acceptAnyKey) {
+ int key = 0;
+ Common::Event event;
+ while (_engine->_eventManager->pollEvent(event)) {
+ switch (event.type) {
+ case Common::EVENT_QUIT:
+ case Common::EVENT_RETURN_TO_LAUNCHER:
+ _aborted = true;
+ _engine->quitGame();
+ break;
+ case Common::EVENT_SCREEN_CHANGED:
+ _engine->_gfx->computeScreenViewport();
+ _engine->_gfx->clear(0, 0, 0, true);
+ break;
+ case Common::EVENT_KEYDOWN:
+ if (event.kbd.keycode == Common::KEYCODE_1)
+ key = 1;
+ else if (event.kbd.keycode == Common::KEYCODE_2)
+ key = 2;
+ else if (acceptAnyKey)
+ key = 3;
+ break;
+ case Common::EVENT_LBUTTONDOWN:
+ case Common::EVENT_RBUTTONDOWN:
+ if (acceptSelection) {
+ int mouseX = kWidth * event.mouse.x / g_system->getWidth();
+ key = mouseX < selectionSplitX() ? 1 : 2;
+ } else if (acceptAnyKey) {
+ key = 3;
+ }
+ break;
+ case Common::EVENT_CUSTOM_ENGINE_ACTION_START:
+ if (event.customType == kActionSelectPrince)
+ key = 1;
+ else if (event.customType == kActionSelectPrincess)
+ key = 2;
+ else if (event.customType == kActionSkip && acceptAnyKey)
+ key = 3;
+ break;
+ default:
+ break;
+ }
+ }
+ if (!acceptSelection && (key == 1 || key == 2) && acceptAnyKey)
+ return key;
+ if (!acceptSelection && (key == 1 || key == 2))
+ return 0;
+ return key;
+ }
+};
+
Graphics::ManagedSurface *CastleEngine::loadFrameFromPlanesVertical(Common::SeekableReadStream *file, int widthInBytes, int height) {
Graphics::ManagedSurface *surface;
surface = new Graphics::ManagedSurface();
@@ -470,6 +1378,385 @@ void CastleEngine::loadAssetsAmigaDemo() {
it._value->addStructure(_areaMap[255]);
}
+void CastleEngine::loadAssetsAmigaFullGame() {
+ Common::File file;
+ file.open("x");
+ if (!file.isOpen())
+ error("Failed to open 'x' file");
+
+ _viewArea = Common::Rect(40, 29, 280, 154);
+ loadMessagesVariableSize(&file, 0x99ac, 178);
+ loadRiddles(&file, 0xa476, 19);
+
+ file.seek(0x11540);
+ 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);
+
+ load8bitBinary(&file, 0x158fa, 16);
+ for (int i = 0; i < 3; i++) {
+ debugC(1, kFreescapeDebugParser, "Continue to parse area index %d at offset %x", _areaMap.size() + i + 1, (int)file.pos());
+ 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, 0x147fa);
+
+ // COLOR15 cycling table: same format as the demo, terminated by 0xFFFF.
+ file.seek(0x998e);
+ while (true) {
+ uint16 val = file.readUint16BE();
+ if (val == 0xFFFF) break;
+ _gfx->_colorCyclingTable.push_back(val);
+ }
+
+ file.seek(0x2b4ea); // Area 255
+ _areaMap[255] = load8bitArea(&file, 16);
+
+ // Border NEO image (demo loaded at 0x2cf28 + 0x28 - 0x2 + 0x28 = 0x2cf76)
+ file.seek(0x2c5ca);
+ _border = loadFrameFromPlanesVertical(&file, 160, 200);
+ _border->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+
+ // Mountains panorama (63 words à 22 rows à 4 planes, interleaved).
+ file.seek(0x49c8);
+ _background = loadFrameFromPlanesInterleaved(&file, 63, 22);
+ _background->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+
+ // Info menu image (14 words à 116 rows).
+ file.seek(0x3473a);
+ _menu = loadFrameFromPlanesInterleaved(&file, 14, 116);
+ _menu->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+
+ // Additional 224Ã54 menu-related block.
+ file.seek(0x3620a);
+ _menuButtons = loadFrameFromPlanesInterleaved(&file, 14, 54);
+ _menuButtons->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+
+ file.seek(0x37fa6); // Spirit meter indicator background
+ _spiritsMeterIndicatorBackgroundFrame = loadFrameFromPlanesInterleaved(&file, 5, 10);
+ _spiritsMeterIndicatorBackgroundFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+
+ file.seek(0x38136); // Spirit meter indicator
+ _spiritsMeterIndicatorFrame = loadFrameFromPlanesInterleaved(&file, 1, 10);
+ _spiritsMeterIndicatorFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+
+ // Strength weight sprites: 4 frames à 1 word à 14 rows.
+ file.seek(0x38c46);
+ 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);
+ }
+
+ // Eye icon sprites: 12 frames à 1 word à 7 rows. Header at 0x3b6fe.
+ file.seek(0x3b706);
+ 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);
+ }
+
+ // Crawl/Walk/Run + Sound indicators: 5 frames à 3 words à 12 rows,
+ // preceded by a 6-byte header and a 6-byte mask (skipped here).
+ file.seek(0x379fa + 6 + 6);
+ {
+ _menuCrawlIndicator = loadFrameFromPlanesInterleaved(&file, 3, 12);
+ _menuCrawlIndicator->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+ _menuWalkIndicator = loadFrameFromPlanesInterleaved(&file, 3, 12);
+ _menuWalkIndicator->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+ _menuRunIndicator = loadFrameFromPlanesInterleaved(&file, 3, 12);
+ _menuRunIndicator->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+ _menuFxOffIndicator = loadFrameFromPlanesInterleaved(&file, 3, 12);
+ _menuFxOffIndicator->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+ _menuFxOnIndicator = loadFrameFromPlanesInterleaved(&file, 3, 12);
+ _menuFxOnIndicator->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+ }
+
+ // Mouse pointer and crosshair sprites (paired SPR0/SPR1). These live
+ // in the TEXT section at the very top of the binary and are at the
+ // same file offsets as the demo (no shift).
+ {
+ _cursorW = 16;
+ _cursorH = 16;
+ _cursorData = new byte[16 * 16];
+ memset(_cursorData, 0, 16 * 16);
+ file.seek(0x24A + 4); // SPR0 (bits 0-1)
+ for (int row = 0; row < 16; row++) {
+ uint16 p0 = file.readUint16BE();
+ uint16 p1 = file.readUint16BE();
+ for (int bit = 0; bit < 16; bit++) {
+ byte c = ((p0 >> (15 - bit)) & 1) | (((p1 >> (15 - bit)) & 1) << 1);
+ _cursorData[row * 16 + bit] = c;
+ }
+ }
+ file.seek(0x292 + 4); // SPR1 (bits 2-3)
+ for (int row = 0; row < 16; row++) {
+ uint16 p0 = file.readUint16BE();
+ uint16 p1 = file.readUint16BE();
+ for (int bit = 0; bit < 16; bit++) {
+ byte c = ((p0 >> (15 - bit)) & 1) | (((p1 >> (15 - bit)) & 1) << 1);
+ _cursorData[row * 16 + bit] |= (c << 2);
+ }
+ }
+ }
+ {
+ _crosshairData = new byte[16 * 16];
+ memset(_crosshairData, 0, 16 * 16);
+ file.seek(0x1BA + 4);
+ for (int row = 0; row < 16; row++) {
+ uint16 p0 = file.readUint16BE();
+ uint16 p1 = file.readUint16BE();
+ for (int bit = 0; bit < 16; bit++) {
+ byte c = ((p0 >> (15 - bit)) & 1) | (((p1 >> (15 - bit)) & 1) << 1);
+ _crosshairData[row * 16 + bit] = c;
+ }
+ }
+ file.seek(0x202 + 4);
+ for (int row = 0; row < 16; row++) {
+ uint16 p0 = file.readUint16BE();
+ uint16 p1 = file.readUint16BE();
+ for (int bit = 0; bit < 16; bit++) {
+ byte c = ((p0 >> (15 - bit)) & 1) | (((p1 >> (15 - bit)) & 1) << 1);
+ _crosshairData[row * 16 + bit] |= (c << 2);
+ }
+ }
+ }
+
+ // Flag animation: 5 frames à 2 words à 11 rows.
+ file.seek(0x3b9b0);
+ 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 mask + frames (see demo loader for layout details).
+ file.seek(0x3bd4a);
+ uint16 riddleMask[16];
+ for (int i = 0; i < 16; i++)
+ riddleMask[i] = file.readUint16BE();
+
+ file.seek(0x3bd6a);
+ _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 tiles (24 top rows + 19 bottom rows, see demo loader).
+ {
+ 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(0x39136);
+ file.read(pixelData, sizeof(pixelData));
+ file.seek(0x3a156);
+ 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++) {
+ int srcRow = r;
+ for (int col = 0; col < kColumnsPerRow; col++) {
+ uint16 mask = READ_BE_UINT16(&maskData[srcRow * kMaskBytesPerRow + col * 2]);
+ int pOff = srcRow * kPixelBytesPerRow + col * 6;
+ uint16 p0 = READ_BE_UINT16(&pixelData[pOff]);
+ uint16 p1 = READ_BE_UINT16(&pixelData[pOff + 2]);
+ uint16 p2 = READ_BE_UINT16(&pixelData[pOff + 4]);
+ for (int bit = 15; bit >= 0; bit--) {
+ if (!(mask & (1 << bit))) {
+ int color = ((p0 >> bit) & 1) | (((p1 >> bit) & 1) << 1) | (((p2 >> bit) & 1) << 2);
+ _gameOverBackgroundFrame->setPixel(col * 16 + (15 - bit), destRow, paletteColors[color]);
+ }
+ }
+ }
+ destRow++;
+ }
+ for (int block = 0; block < 4; block++) {
+ for (int r = 0; r < kTopRows; r++) {
+ int srcRow = r;
+ for (int col = 0; col < kColumnsPerRow; col++) {
+ uint16 mask = READ_BE_UINT16(&maskData[srcRow * kMaskBytesPerRow + col * 2]);
+ int pOff = srcRow * kPixelBytesPerRow + col * 6;
+ uint16 p0 = READ_BE_UINT16(&pixelData[pOff]);
+ uint16 p1 = READ_BE_UINT16(&pixelData[pOff + 2]);
+ uint16 p2 = READ_BE_UINT16(&pixelData[pOff + 4]);
+ for (int bit = 15; bit >= 0; bit--) {
+ if (!(mask & (1 << bit))) {
+ int color = ((p0 >> bit) & 1) | (((p1 >> bit) & 1) << 1) | (((p2 >> bit) & 1) << 2);
+ _gameOverBackgroundFrame->setPixel(col * 16 + (15 - bit), destRow, paletteColors[color]);
+ }
+ }
+ }
+ destRow++;
+ }
+ }
+ 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++;
+ }
+ }
+
+ // Sound effects command table (30 entries). Pass the full-game MOD
+ // offset so DMA sample extraction reads from the right place â the
+ // demo's 0x3D5A6 hardcoded default is wrong for the full binary.
+ loadSoundsAmigaDemo(&file, 0x13cf2, 30, 0x3cbfa);
+
+ // Embedded ProTracker module for background music.
+ static const int kModOffset = 0x3cbfa;
+ file.seek(0, SEEK_END);
+ int fileSize = file.pos();
+ int modSize = fileSize - kModOffset;
+ if (modSize > 0) {
+ file.seek(kModOffset);
+ _modData.resize(modSize);
+ file.read(_modData.data(), modSize);
+ }
+
+ file.close();
+
+ _areaMap[2]->_groundColor = 1;
+ for (auto &it : _areaMap)
+ it._value->addStructure(_areaMap[255]);
+}
+
+bool CastleEngine::playAmigaIntro() {
+ Common::File introFile;
+ if (!introFile.open("intro"))
+ return false;
+
+ introFile.seek(0, SEEK_END);
+ int introFileSize = introFile.pos();
+
+ Common::Array<byte> introText;
+ if (introFileSize > 0x1c) {
+ introFile.seek(0);
+ uint16 magic = introFile.readUint16BE();
+ uint32 textSize = introFile.readUint32BE();
+ if (magic == 0x601a) {
+ if (textSize == 0 || textSize + 0x1c > (uint32)introFileSize)
+ return false;
+ introText.resize(textSize);
+ introFile.seek(0x1c);
+ introFile.read(introText.data(), textSize);
+ }
+ }
+
+ if (introText.empty()) {
+ if (introFileSize <= 0)
+ return false;
+ introText.resize(introFileSize);
+ introFile.seek(0);
+ introFile.read(introText.data(), introFileSize);
+ }
+ introFile.close();
+
+ if (introText.size() < 0x1c00)
+ return false;
+
+ Common::Array<byte> introMusic;
+ Common::File introMusicFile;
+ if (introMusicFile.open("musicdat")) {
+ introMusicFile.seek(0, SEEK_END);
+ int introMusicSize = introMusicFile.pos();
+ introMusicFile.seek(0);
+ introMusic.resize(introMusicSize);
+ if (introMusicSize > 0)
+ introMusicFile.read(introMusic.data(), introMusicSize);
+ introMusicFile.close();
+ }
+
+ Audio::SoundHandle introMusicHandle;
+ if (!introMusic.empty()) {
+ Common::MemoryReadStream modStream(introMusic.data(), introMusic.size());
+ Audio::AudioStream *musicStream = Audio::makeProtrackerStream(&modStream);
+ if (musicStream)
+ _mixer->playStream(Audio::Mixer::kMusicSoundType, &introMusicHandle, musicStream);
+ }
+
+ bool selectedPrincess = false;
+ CastleAmigaIntroPlayer player(this, introText);
+ bool played = player.run(selectedPrincess);
+ if (played)
+ _selectedPrincess = selectedPrincess;
+
+ if (_mixer->isSoundHandleActive(introMusicHandle))
+ _mixer->stopHandle(introMusicHandle);
+
+ _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/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 06442915ee5..37ab361e444 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -121,6 +121,7 @@ CastleEngine::CastleEngine(OSystem *syst, const ADGameDescription *gd) : Freesca
_thunderTicks = 0;
_thunderFrameDuration = 0;
_thunderFrameIndex = 0;
+ _selectedPrincess = false;
}
CastleEngine::~CastleEngine() {
@@ -698,6 +699,8 @@ void CastleEngine::initGameState() {
_spiritsMeterPosition = _spiritsMeter * _spiritsToKill / _spiritsToKill;
_exploredAreas[_startArea] = true;
+ if (_selectedPrincess)
+ setGameBit(32);
if (_useRockTravel) // Enable cheat
setGameBit(k8bitGameBitTravelRock);
@@ -1999,6 +2002,12 @@ void CastleEngine::updateTimeVariables() {
void CastleEngine::borderScreen() {
if (isAmiga() && isDemo())
return; // Skip character selection
+ if (isAmiga()) {
+ if (playAmigaIntro())
+ return;
+ selectCharacterScreen();
+ return;
+ }
if (isSpectrum() || isCPC() || isC64())
FreescapeEngine::borderScreen();
@@ -2049,7 +2058,7 @@ void CastleEngine::selectCharacterScreen() {
surface->create(_screenW, _screenH, _gfx->_texturePixelFormat);
surface->fillRect(_fullscreenViewArea, color);
- if (isSpectrum() || isCPC()) {
+ if (isSpectrum() || isCPC() || isAmiga()) {
if (_language == Common::ES_ESP) {
// No accent in "prÃncipe" since it is not supported by the font
lines.push_back(centerAndPadString("*******************", 21));
@@ -2129,7 +2138,7 @@ void CastleEngine::selectCharacterScreen() {
// lines[5] = prince, lines[6] = princess for ZX/CPC.
// For DOS, use riddle text line positions.
Common::Rect princeSelector, princessSelector;
- if (isSpectrum() || isCPC()) {
+ if (isSpectrum() || isCPC() || isAmiga()) {
int x = _viewArea.left + 3;
int lineHeight = 12; // Castle Master line spacing in drawStringsInSurface
int princeY = _viewArea.top + 3 + 5 * lineHeight;
@@ -2180,10 +2189,10 @@ void CastleEngine::selectCharacterScreen() {
if (princeSelector.contains(mouse)) {
selected = true;
- // Nothing, since game bit should be already zero
+ _selectedPrincess = false;
} else if (princessSelector.contains(mouse)) {
selected = true;
- setGameBit(32);
+ _selectedPrincess = true;
}
break;
case Common::EVENT_SCREEN_CHANGED:
@@ -2196,11 +2205,11 @@ void CastleEngine::selectCharacterScreen() {
switch (event.customType) {
case kActionSelectPrince:
selected = true;
- // Nothing, since game bit should be already zero
+ _selectedPrincess = false;
break;
case kActionSelectPrincess:
selected = true;
- setGameBit(32);
+ _selectedPrincess = true;
break;
default:
break;
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index 51e390bafea..207ab0969a9 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -66,10 +66,12 @@ public:
void loadAssetsDOSFullGame() override;
void loadAssetsDOSDemo() override;
void loadAssetsAmigaDemo() override;
+ void loadAssetsAmigaFullGame() override;
void loadAssetsZXFullGame() override;
void loadAssetsCPCFullGame() override;
void borderScreen() override;
void selectCharacterScreen();
+ bool playAmigaIntro();
void drawOption();
void initZX();
@@ -180,6 +182,7 @@ public:
int _lastTenSeconds;
int _soundIndexStartFalling;
+ bool _selectedPrincess;
private:
Common::SeekableReadStream *decryptFile(const Common::Path &filename);
diff --git a/engines/freescape/loaders/8bitBinaryLoader.cpp b/engines/freescape/loaders/8bitBinaryLoader.cpp
index b63c6b4e231..8a36f73cd35 100644
--- a/engines/freescape/loaders/8bitBinaryLoader.cpp
+++ b/engines/freescape/loaders/8bitBinaryLoader.cpp
@@ -860,7 +860,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);
- if (isAmiga() && isCastle() && isDemo())
+ // 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())
numberOfAreas = 87;
debugC(1, kFreescapeDebugParser, "Number of areas: %d", numberOfAreas);
diff --git a/engines/freescape/sound/amiga.cpp b/engines/freescape/sound/amiga.cpp
index e03e8d8b0d7..cd9db335d1c 100644
--- a/engines/freescape/sound/amiga.cpp
+++ b/engines/freescape/sound/amiga.cpp
@@ -394,7 +394,7 @@ private:
}
};
-void FreescapeEngine::loadSoundsAmigaDemo(Common::SeekableReadStream *file, int offset, int numSounds) {
+void FreescapeEngine::loadSoundsAmigaDemo(Common::SeekableReadStream *file, int offset, int numSounds, int modOffset) {
file->seek(offset);
_amigaSfxTable.clear();
for (int i = 0; i < numSounds; i++) {
@@ -415,12 +415,11 @@ void FreescapeEngine::loadSoundsAmigaDemo(Common::SeekableReadStream *file, int
_amigaDmaSamples.clear();
_amigaDmaSamples.resize(12);
- static const int kModOffset = 0x3D5A6;
- if (file->size() > kModOffset + 1084) {
- int modSize = file->size() - kModOffset;
+ if (file->size() > modOffset + 1084) {
+ int modSize = file->size() - modOffset;
Common::Array<byte> modBytes;
modBytes.resize(modSize);
- file->seek(kModOffset);
+ file->seek(modOffset);
file->read(modBytes.data(), modSize);
Common::MemoryReadStream modStream(modBytes.data(), modBytes.size());
Commit: 2e7f792636428791a7507ecefd342dc1875718b8
https://github.com/scummvm/scummvm/commit/2e7f792636428791a7507ecefd342dc1875718b8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-23T20:56:51+02:00
Commit Message:
FREESCAPE: improve castle amiga endgame
Changed paths:
engines/freescape/detection.cpp
engines/freescape/games/castle/amiga.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 798e4376b59..e99ea19e535 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -886,6 +886,7 @@ static const ADGameDescription gameDescriptions[] = {
"",
{
{"cm", 0, "67dcb3f62fe15f18ecc31380ffaf5c4e", 1112},
+ {"w", 0, "63c770f1008a641c5fd5d0b9df2bcbb6", 32048},
{"x", 0, "afc245de66ef8eb1b5fd061c6bbd602e", 349975},
AD_LISTEND
},
diff --git a/engines/freescape/games/castle/amiga.cpp b/engines/freescape/games/castle/amiga.cpp
index 6ef8248c9e1..94f4802790d 100644
--- a/engines/freescape/games/castle/amiga.cpp
+++ b/engines/freescape/games/castle/amiga.cpp
@@ -1441,6 +1441,16 @@ void CastleEngine::loadAssetsAmigaFullGame() {
_border = loadFrameFromPlanesVertical(&file, 160, 200);
_border->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+ // End-game throne picture. The original executable opens "W" during the
+ // escaped ending and displays the first 114 rows as a 16-word-wide
+ // interleaved Amiga bitplane image.
+ Common::File endGameFile;
+ if (endGameFile.open("w")) {
+ _endGameBackgroundFrame = loadFrameFromPlanesInterleaved(&endGameFile, 16, 114);
+ _endGameBackgroundFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+ endGameFile.close();
+ }
+
// Mountains panorama (63 words à 22 rows à 4 planes, interleaved).
file.seek(0x49c8);
_background = loadFrameFromPlanesInterleaved(&file, 63, 22);
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 37ab361e444..38abf132082 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -761,10 +761,12 @@ void CastleEngine::endGame() {
_endGamePlayerEndArea = true;
if (hasEscaped()) {
- insertTemporaryMessage(_messagesList[5], INT_MIN);
+ insertTemporaryMessage(_messagesList[(isAmiga() || isAtariST()) ? 15 : 5], INT_MIN);
if (isDOS() && !isCastleMaster2()) {
drawFullscreenEndGameAndWait();
+ } else if (isAmiga() && !isDemo() && _endGameBackgroundFrame) {
+ drawFullscreenAmigaEndGameAndWait();
} else if (isCastleMaster2()) {
executeEscapeCameraSequence();
drawFullscreenGameOverAndWait();
@@ -1312,6 +1314,51 @@ void CastleEngine::drawFullscreenEndGameAndWait() {
delete surface;
}
+void CastleEngine::drawFullscreenAmigaEndGameAndWait() {
+ Graphics::Surface *surface = new Graphics::Surface();
+ surface->create(_screenW, _screenH, _gfx->_texturePixelFormat);
+ surface->fillRect(_fullscreenViewArea, _gfx->_texturePixelFormat.ARGBToColor(0x00, 0x00, 0x00, 0x00));
+ surface->fillRect(_viewArea, _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0x00, 0x00));
+
+ int dx = _viewArea.left + (_viewArea.width() - _endGameBackgroundFrame->w) / 2;
+ int dy = _viewArea.top + (_viewArea.height() - _endGameBackgroundFrame->h) / 2;
+ surface->copyRectToSurface(*_endGameBackgroundFrame, dx, dy,
+ Common::Rect(0, 0, _endGameBackgroundFrame->w, _endGameBackgroundFrame->h));
+
+ Common::Event event;
+ bool cont = true;
+ while (!shouldQuit() && cont) {
+ while (_eventManager->pollEvent(event)) {
+ switch (event.type) {
+ case Common::EVENT_LBUTTONDOWN:
+ cont = false;
+ break;
+ case Common::EVENT_CUSTOM_ENGINE_ACTION_START:
+ if (event.customType == kActionShoot || event.customType == kActionChangeMode || event.customType == kActionSkip)
+ cont = false;
+ break;
+ case Common::EVENT_SCREEN_CHANGED:
+ _gfx->computeScreenViewport();
+ break;
+ default:
+ break;
+ }
+ }
+ _gfx->clear(0, 0, 0, true);
+ drawBorder();
+ if (_currentArea)
+ drawUI();
+
+ drawFullscreenSurface(surface);
+ _gfx->flipBuffer();
+ g_system->updateScreen();
+ g_system->delayMillis(15); // try to target ~60 FPS
+ }
+
+ surface->free();
+ delete surface;
+}
+
void CastleEngine::drawFullscreenGameOverAndWait() {
Common::Event event;
bool cont = true;
@@ -1375,7 +1422,7 @@ void CastleEngine::drawFullscreenGameOverAndWait() {
}
if (!isDOS() && hasEscaped()) {
- insertTemporaryMessage(_messagesList[5], _countdown - 1);
+ insertTemporaryMessage(_messagesList[(isAmiga() || isAtariST()) ? 15 : 5], _countdown - 1);
}
while (!shouldQuit() && cont) {
@@ -1384,7 +1431,7 @@ void CastleEngine::drawFullscreenGameOverAndWait() {
insertTemporaryMessage(spiritsDestroyedString, _countdown - 4);
insertTemporaryMessage(keysCollectedString, _countdown - 6);
if (!isDOS() && hasEscaped()) {
- insertTemporaryMessage(_messagesList[5], _countdown - 8);
+ insertTemporaryMessage(_messagesList[(isAmiga() || isAtariST()) ? 15 : 5], _countdown - 8);
}
}
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index 207ab0969a9..9106fc7f739 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -190,6 +190,7 @@ private:
void loadDOSFonts(Common::SeekableReadStream *file, int pos);
void drawFullscreenRiddleAndWait(uint16 riddle);
void drawFullscreenEndGameAndWait();
+ void drawFullscreenAmigaEndGameAndWait();
void drawFullscreenGameOverAndWait();
void drawRiddle(uint16 riddle, uint32 front, uint32 back, Graphics::Surface *surface);
void tryToCollectKey();
Commit: 6cef8e0f346d97c162b05541a6bab17574567837
https://github.com/scummvm/scummvm/commit/6cef8e0f346d97c162b05541a6bab17574567837
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-23T20:56:51+02:00
Commit Message:
FREESCAPE: experimental support red/blue stereo 3d images for the zx render
Changed paths:
engines/freescape/area.cpp
engines/freescape/area.h
engines/freescape/freescape.cpp
engines/freescape/freescape.h
engines/freescape/gfx.cpp
engines/freescape/gfx.h
engines/freescape/gfx_opengl.cpp
engines/freescape/gfx_opengl.h
engines/freescape/gfx_opengl_shaders.cpp
engines/freescape/gfx_opengl_shaders.h
engines/freescape/movement.cpp
diff --git a/engines/freescape/area.cpp b/engines/freescape/area.cpp
index cfa95dd4fbc..35423f59446 100644
--- a/engines/freescape/area.cpp
+++ b/engines/freescape/area.cpp
@@ -86,6 +86,9 @@ Area::Area(uint16 areaID_, uint16 areaFlags_, ObjectMap *objectsByID_, ObjectMap
}
_lastTick = 0;
+ _lastDepthLayerTick = 0;
+ _lastRenderDepthLayer = kRenderDepthAll;
+ _lastForegroundDistance = 0.0f;
}
Area::~Area() {
@@ -224,6 +227,59 @@ void Area::resetArea() {
}
+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();
+ float nearest = FLT_MAX;
+ float farthest = -FLT_MAX;
+
+ for (int x = 0; x < 2; x++) {
+ for (int y = 0; y < 2; y++) {
+ for (int z = 0; z < 2; z++) {
+ Math::Vector3d corner(
+ x ? max.x() : min.x(),
+ y ? max.y() : min.y(),
+ z ? max.z() : min.z());
+ float depth = (corner - camera).dotProduct(direction);
+ nearest = MIN(nearest, depth);
+ farthest = MAX(farthest, depth);
+ }
+ }
+ }
+
+ return farthest < 0.0f ? FLT_MAX : MAX(0.0f, nearest);
+}
+
+static float objectNearestDepth(Object *obj, const Math::Vector3d &camera, const Math::Vector3d &direction) {
+ if (!obj || obj->isDestroyed() || obj->isInvisible())
+ return FLT_MAX;
+
+ if (obj->getType() == ObjectType::kGroupType) {
+ Group *group = (Group *)obj;
+ float nearest = FLT_MAX;
+ for (auto &child : group->_objects)
+ nearest = MIN(nearest, objectNearestDepth(child, camera, direction));
+ return nearest;
+ }
+
+ Math::AABB bounds = obj->_boundingBox;
+ if (!bounds.isValid()) {
+ bounds.expand(obj->_origin);
+ bounds.expand(obj->_origin + obj->_size);
+ }
+
+ return bounds.isValid() ? aabbNearestDepth(bounds, camera, direction) : FLT_MAX;
+}
+
+static bool objectInDepthLayer(Object *obj, const Math::Vector3d &camera, const Math::Vector3d &direction, Area::RenderDepthLayer depthLayer, float foregroundDistance) {
+ if (depthLayer == Area::kRenderDepthAll)
+ return true;
+
+ float nearestDepth = objectNearestDepth(obj, camera, direction);
+ bool foreground = nearestDepth <= foregroundDistance;
+ return depthLayer == Area::kRenderDepthForeground ? foreground : !foreground;
+}
+
void Area::draw(Freescape::Renderer *gfx, uint32 animationTicks, Math::Vector3d camera, Math::Vector3d direction, bool insideWait) {
bool runAnimation = animationTicks != _lastTick;
bool cameraChanged = camera != _lastCameraPosition;
@@ -397,6 +453,187 @@ void Area::draw(Freescape::Renderer *gfx, uint32 animationTicks, Math::Vector3d
_lastCameraPosition = camera;
}
+void Area::drawDepthLayer(Freescape::Renderer *gfx, uint32 animationTicks, Math::Vector3d camera, Math::Vector3d direction, bool insideWait, RenderDepthLayer depthLayer, float foregroundDistance) {
+ bool runAnimation = depthLayer != kRenderDepthBackground && animationTicks != _lastDepthLayerTick;
+ bool cameraChanged = camera != _lastDepthLayerCameraPosition;
+ bool layerChanged = depthLayer != _lastRenderDepthLayer || (depthLayer != kRenderDepthAll && ABS(foregroundDistance - _lastForegroundDistance) > 0.001f);
+ bool sort = runAnimation || cameraChanged || layerChanged || _depthLayerSortedObjects.empty();
+ Math::Vector3d normalizedDirection = direction.getNormalized();
+
+ assert(_drawableObjects.size() > 0);
+ if (sort)
+ _depthLayerSortedObjects.clear();
+
+ Object *floor = nullptr;
+
+ for (auto &obj : _drawableObjects) {
+ if (!obj->isDestroyed() && !obj->isInvisible()) {
+ if (!gfx->_debugHighlightObjectIDs.empty()) {
+ bool found = false;
+ for (auto id : gfx->_debugHighlightObjectIDs) {
+ if (obj->getObjectID() == id) {
+ found = true;
+ break;
+ }
+ }
+ // if this object is not in our list, skip it completely.
+ // it will not be sorted, and it will not be drawn.
+ if (!found)
+ continue;
+ }
+ if (obj->getObjectID() == 0 && _groundColor < 255 && _skyColor < 255) {
+ if (depthLayer != kRenderDepthForeground)
+ floor = obj;
+ continue;
+ }
+
+ if (obj->getType() == ObjectType::kGroupType) {
+ if (objectInDepthLayer(obj, camera, normalizedDirection, depthLayer, foregroundDistance))
+ drawGroup(gfx, (Group *)obj, runAnimation && !insideWait);
+ continue;
+ }
+
+ if (sort && objectInDepthLayer(obj, camera, normalizedDirection, depthLayer, foregroundDistance))
+ _depthLayerSortedObjects.push_back(obj);
+ }
+ }
+
+ if (floor) {
+ floor->draw(gfx);
+ }
+
+ // Corresponds to L9c66 in assembly (bounding_box_axis_loop)
+ auto checkAxis = [](float minA, float maxA, float minB, float maxB) -> int {
+ bool signMinA = minA >= 0;
+ bool signMaxA = maxA >= 0;
+ 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)
+ return 1; // A is closer
+ if (signMinB != signMaxB) // B covers 0 (L9cec_second_object_is_closer)
+ return 2; // B is closer
+
+ if (signMinA != signMinB) // Different sides (L9cf3_objects_incomparable_in_this_axis)
+ return 0;
+
+ // Same side
+ if (!signMinA) { // Negative side (sign bit set in asm)
+ if (minA > minB) return 1; // A closer
+ if (minA < minB) return 2; // B closer
+ if (maxA > maxB) return 1; // A closer
+ return 2; // B closer
+ } else { // Positive side (sign bit clear in asm)
+ if (minA < minB) return 1; // A closer
+ if (minA > minB) return 2; // B closer
+ if (maxA > maxB) return 2; // B closer
+ return 1; // A closer
+ }
+ } else if (minB >= maxA - 0.5f) { // B is clearly "greater" than A
+ if (signMinB != signMaxB) // B covers 0 (L9cec_second_object_is_closer)
+ return 2; // B is closer
+ if (signMinA != signMaxA) // A covers 0 (L9ce6_first_object_is_closer)
+ return 1; // A is closer
+
+ if (signMinA != signMinB) // Different sides (L9cf3_objects_incomparable_in_this_axis)
+ return 0;
+
+ // Same side
+ if (!signMinB) { // Negative side
+ if (minB > minA) return 2; // B closer
+ if (minB < minA) return 1; // A closer
+ if (maxB > maxA) return 2; // B closer
+ return 1; // A closer
+ } else { // Positive side
+ if (minB < minA) return 2; // B closer
+ if (minB > minA) return 1; // A closer
+ if (maxB > maxA) return 1; // A closer
+ return 2; // B closer
+ }
+ }
+ return 0; // Overlap (L9cf3_objects_incomparable_in_this_axis)
+ };
+
+ // Bubble sort as implemented in castlemaster2-annotated.asm (L9c2d_sort_objects_for_rendering)
+ // NOTE: The sorting is performed on unprojected world-space coordinates relative to the player (L847f).
+ // The rotation/view matrix (computed in L95de) is NOT applied to the bounding boxes used for sorting.
+ // It is only applied to the vertices during the projection phase (L850f/L9177).
+ int n = _depthLayerSortedObjects.size();
+ if (n > 1 && sort) {
+ // Pre-sort by distance from camera (furthest first) to provide a stable initial
+ // ordering for the non-transitive bubble sort below. The original game achieves
+ // this by culling off-screen objects via a rendering volume check (L8bb7/L845b)
+ // before sorting, which prevents distant off-screen objects from interfering with
+ // the depth ordering of visible objects through non-transitive comparisons.
+ Common::sort(_depthLayerSortedObjects.begin(), _depthLayerSortedObjects.end(),
+ [&camera](Object *a, Object *b) {
+ Math::Vector3d centerA = (a->_occlusionBox.getMin() + a->_occlusionBox.getMax()) * 0.5f;
+ Math::Vector3d centerB = (b->_occlusionBox.getMin() + b->_occlusionBox.getMax()) * 0.5f;
+ return (centerA - camera).getSquareMagnitude() > (centerB - camera).getSquareMagnitude();
+ });
+ for (int i = 0; i < n; i++) { // L9c31_whole_object_pass_loop
+ bool changed = false;
+ for (int j = 0; j < n - 1; j++) { // L9c45_objects_loop
+ Object *a = _depthLayerSortedObjects[j];
+ Object *b = _depthLayerSortedObjects[j + 1];
+
+ Math::AABB bboxA = a->_occlusionBox;
+ Math::AABB bboxB = b->_occlusionBox;
+ Math::Vector3d minA = bboxA.getMin() - camera;
+ Math::Vector3d maxA = bboxA.getMax() - camera;
+ Math::Vector3d minB = bboxB.getMin() - camera;
+ Math::Vector3d maxB = bboxB.getMax() - camera;
+
+ int result = 0;
+
+ // X axis
+ result = (result << 2) | checkAxis(minA.x(), maxA.x(), minB.x(), maxB.x());
+ // Y axis
+ result = (result << 2) | checkAxis(minA.y(), maxA.y(), minB.y(), maxB.y());
+ // Z axis
+ result = (result << 2) | checkAxis(minA.z(), maxA.z(), minB.z(), maxB.z());
+
+ bool keepOrder = false;
+ // If result indicates B is closer in at least one axis, AND A is NEVER closer in any axis, keep order (A before B)
+ // Codes where B is closer (2) and A is not (1):
+ // 2 (Z), 8 (Y), 32 (X) -> hex: 02, 08, 20
+ // 2+8=10 (0A), 2+32=34 (22), 8+32=40 (28)
+ // 2+8+32=42 (2A)
+ // L9d37_next_object (Keep order)
+ if (result == 0x02 || result == 0x08 || result == 0x20 ||
+ result == 0x0A || result == 0x22 || result == 0x28 || result == 0x2A)
+ keepOrder = true; // A before B
+
+ if (!keepOrder) {
+ // Swap objects (L9d2c_flip_objects_loop)
+ _depthLayerSortedObjects[j] = b;
+ _depthLayerSortedObjects[j + 1] = a;
+ changed = true;
+ }
+ }
+ if (!changed)
+ break;
+ }
+ }
+
+ for (auto &obj : _depthLayerSortedObjects) {
+ obj->draw(gfx);
+
+ // draw bounding boxes
+ if (gfx->_debugRenderBoundingBoxes)
+ gfx->drawAABB(obj->_boundingBox, 0, 255, 0);
+ if (gfx->_debugRenderOcclusionBoxes)
+ gfx->drawAABB(obj->_occlusionBox, 255, 0, 0);
+ }
+ if (depthLayer != kRenderDepthBackground)
+ _lastDepthLayerTick = animationTicks;
+ if (sort) {
+ _lastDepthLayerCameraPosition = camera;
+ _lastRenderDepthLayer = depthLayer;
+ _lastForegroundDistance = foregroundDistance;
+ }
+}
+
void Area::drawGroup(Freescape::Renderer *gfx, Group* group, bool runAnimation) {
if (runAnimation) {
group->run();
diff --git a/engines/freescape/area.h b/engines/freescape/area.h
index fba98d02378..51435e34b54 100644
--- a/engines/freescape/area.h
+++ b/engines/freescape/area.h
@@ -37,8 +37,15 @@ namespace Freescape {
typedef Common::HashMap<uint16, Object *> ObjectMap;
typedef Common::Array<Object *> ObjectArray;
+
class Area {
public:
+ enum RenderDepthLayer {
+ kRenderDepthAll,
+ kRenderDepthBackground,
+ kRenderDepthForeground
+ };
+
Area(uint16 areaID, uint16 areaFlags, ObjectMap *objectsByID, ObjectMap *entrancesByID, bool isCastle);
virtual ~Area();
@@ -58,6 +65,7 @@ public:
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 drawGroup(Renderer *gfx, Group *group, bool runAnimation);
void show();
@@ -108,6 +116,11 @@ public:
private:
Math::Vector3d _lastCameraPosition;
ObjectArray _sortedObjects;
+ ObjectArray _depthLayerSortedObjects;
+ Math::Vector3d _lastDepthLayerCameraPosition;
+ RenderDepthLayer _lastRenderDepthLayer;
+ float _lastForegroundDistance;
+ uint32 _lastDepthLayerTick;
uint16 _areaID;
uint16 _areaFlags;
diff --git a/engines/freescape/freescape.cpp b/engines/freescape/freescape.cpp
index afa566745c2..863c3579e37 100644
--- a/engines/freescape/freescape.cpp
+++ b/engines/freescape/freescape.cpp
@@ -191,6 +191,7 @@ FreescapeEngine::FreescapeEngine(OSystem *syst, const ADGameDescription *gd)
_strafeRight = false;
_moveUp = false;
_moveDown = false;
+ _stereoMode = false;
_playerWasCrushed = false;
_forceEndGame = false;
_syncSound = false;
@@ -503,12 +504,148 @@ void FreescapeEngine::drawBackground() {
_gfx->drawBackground(_currentArea->_skyColor);
}
+static bool rayAabbDistance(const Math::Vector3d &origin, const Math::Vector3d &direction, const Math::AABB &aabb, float &distance) {
+ const float originValues[3] = { origin.x(), origin.y(), origin.z() };
+ const float directionValues[3] = { direction.x(), direction.y(), direction.z() };
+ const float minValues[3] = { aabb.getMin().x(), aabb.getMin().y(), aabb.getMin().z() };
+ const float maxValues[3] = { aabb.getMax().x(), aabb.getMax().y(), aabb.getMax().z() };
+ float tMin = 0.0f;
+ float tMax = FLT_MAX;
+
+ for (int i = 0; i < 3; i++) {
+ if (ABS(directionValues[i]) < 0.0001f) {
+ if (originValues[i] < minValues[i] || originValues[i] > maxValues[i])
+ return false;
+ continue;
+ }
+
+ float t1 = (minValues[i] - originValues[i]) / directionValues[i];
+ float t2 = (maxValues[i] - originValues[i]) / directionValues[i];
+ if (t1 > t2)
+ SWAP(t1, t2);
+
+ tMin = MAX(tMin, t1);
+ tMax = MIN(tMax, t2);
+ if (tMin > tMax)
+ return false;
+ }
+
+ if (tMax < 0.0f)
+ return false;
+
+ distance = tMin > 0.0f ? tMin : tMax;
+ return true;
+}
+
+static float getStereoAreaSize(FreescapeEngine *engine) {
+ if (!engine->_currentArea)
+ return engine->_farClipPlane;
+
+ Math::AABB areaBounds;
+ ObjectMap *objectsByID = engine->_currentArea->getObjectsByID();
+ for (auto &it : *objectsByID) {
+ Object *obj = it._value;
+ if (!obj || obj->isDestroyed() || obj->isInvisible() || !obj->isGeometric() || !obj->_boundingBox.isValid())
+ continue;
+
+ areaBounds.expand(obj->_boundingBox.getMin());
+ areaBounds.expand(obj->_boundingBox.getMax());
+ }
+
+ const int areaScale = MAX<int>(engine->_currentArea->getScale(), 1);
+ if (!areaBounds.isValid())
+ return 800.0f / areaScale;
+
+ Math::Vector3d size = areaBounds.getSize();
+ float horizontalSize = MAX(size.x(), size.z());
+ return MAX(horizontalSize, size.length() * 0.5f);
+}
+
+static float getStereoConvergence(FreescapeEngine *engine) {
+ const int areaScale = MAX<int>(engine->_currentArea ? engine->_currentArea->getScale() : 1, 1);
+ float areaSize = getStereoAreaSize(engine);
+
+ return CLIP(areaSize * 0.55f, 120.0f / areaScale, 1200.0f / areaScale);
+}
+
+static float getStereoMaxSeparation(FreescapeEngine *engine, float convergence) {
+ const int areaScale = MAX<int>(engine->_currentArea ? engine->_currentArea->getScale() : 1, 1);
+ float maxSeparation = CLIP(2.0f / areaScale, 0.08f, 1.5f);
+
+ return MIN(maxSeparation, convergence * 0.02f);
+}
+
+static float getStereoForegroundDistance(FreescapeEngine *engine, float convergence) {
+ const int areaScale = MAX<int>(engine->_currentArea ? engine->_currentArea->getScale() : 1, 1);
+ float areaSize = getStereoAreaSize(engine);
+ float foregroundDistance = MIN(convergence * 0.7f, areaSize * 0.25f);
+
+ return CLIP(foregroundDistance, 60.0f / areaScale, 500.0f / areaScale);
+}
+
+static float getStereoViewDistance(FreescapeEngine *engine, float fov, float aspectRatio) {
+ if (!engine->_currentArea)
+ return engine->_farClipPlane;
+
+ float nearest = engine->_farClipPlane;
+ Math::Vector3d front = engine->_cameraFront.getNormalized();
+ Math::Vector3d up = engine->_upVector.getNormalized();
+ Math::Vector3d right = Math::Vector3d::crossProduct(front, up).getNormalized();
+ const float horizontalScale = tan(Math::deg2rad(fov) / 2);
+ const float verticalScale = horizontalScale / aspectRatio;
+ const float samples[][2] = {
+ { 0.0f, 0.0f },
+ { -0.75f, 0.0f },
+ { 0.75f, 0.0f },
+ { 0.0f, -0.65f },
+ { 0.0f, 0.65f },
+ { -0.55f, -0.45f },
+ { 0.55f, -0.45f },
+ { -0.55f, 0.45f },
+ { 0.55f, 0.45f }
+ };
+ ObjectMap *objectsByID = engine->_currentArea->getObjectsByID();
+ for (auto &it : *objectsByID) {
+ Object *obj = it._value;
+ if (!obj || obj->isDestroyed() || obj->isInvisible() || !obj->isGeometric() || !obj->_boundingBox.isValid())
+ continue;
+
+ for (uint i = 0; i < ARRAYSIZE(samples); i++) {
+ Math::Vector3d direction = (front + right * (samples[i][0] * horizontalScale) + up * (samples[i][1] * verticalScale)).getNormalized();
+ float distance = 0.0f;
+ if (rayAabbDistance(engine->_position, direction, obj->_boundingBox, distance))
+ nearest = MIN(nearest, distance);
+ }
+ }
+
+ return MAX(nearest, engine->_nearClipPlane);
+}
+
+static float getStereoSeparation(FreescapeEngine *engine, float fov, float aspectRatio, float convergence, float maxSeparation) {
+ const float maxDisparity = 6.0f;
+ float nearest = getStereoViewDistance(engine, fov, aspectRatio);
+ if (nearest >= convergence)
+ return maxSeparation;
+
+ float focalLength = (engine->_viewArea.width() * 0.5f) / tan(Math::deg2rad(fov) / 2);
+ float denominator = 2.0f * focalLength * (1.0f / nearest - 1.0f / convergence);
+ if (denominator <= 0.0f)
+ return maxSeparation;
+
+ return MIN(maxSeparation, maxDisparity / denominator);
+}
+
void FreescapeEngine::drawFrame() {
_gfx->updateColorCycling();
int farClipPlane = _farClipPlane;
if (_currentArea->isOutside())
farClipPlane *= 100;
+ if (_stereoMode) {
+ drawFrameStereo(farClipPlane);
+ return;
+ }
+
float aspectRatio = isCastle() ? 1.6 : 2.18;
_gfx->updateProjectionMatrix(75.0, aspectRatio, _nearClipPlane, farClipPlane);
_gfx->positionCamera(_position, _position + _cameraFront, _roll);
@@ -570,6 +707,94 @@ void FreescapeEngine::drawFrame() {
drawUI();
}
+void FreescapeEngine::drawFrameStereo(int farClipPlane) {
+ const float fov = 75.0f;
+ float aspectRatio = isCastle() ? 1.6 : 2.18;
+ const float stereoConvergence = getStereoConvergence(this);
+ const float stereoMaxSeparation = getStereoMaxSeparation(this, stereoConvergence);
+ const float stereoForegroundDistance = getStereoForegroundDistance(this, stereoConvergence);
+ _gfx->setStereoParameters(getStereoSeparation(this, fov, aspectRatio, stereoConvergence, stereoMaxSeparation), stereoConvergence);
+
+ if (_underFireFrames > 0) {
+ int underFireColor = _currentArea->_underFireBackgroundColor;
+
+ if (isDriller() && (isDOS() || isAmiga() || isAtariST()))
+ underFireColor = 1;
+ else if (isDark() && (isDOS() || isAmiga() || isAtariST())) {
+ if (_renderMode == Common::kRenderCGA)
+ underFireColor = 3;
+ else
+ underFireColor = 4;
+ }
+
+ _currentArea->remapColor(_currentArea->_usualBackgroundColor, underFireColor);
+ _currentArea->remapColor(_currentArea->_skyColor, underFireColor);
+ }
+
+ _gfx->setStereoEye(Renderer::kStereoEyeNone);
+ _gfx->clear(0, 0, 0, true);
+ _gfx->setStereoEye(Renderer::kStereoEyeFlatAnaglyph);
+ _gfx->updateProjectionMatrix(fov, aspectRatio, _nearClipPlane, farClipPlane);
+ _gfx->positionCamera(_position, _position + _cameraFront, _roll);
+
+ drawBackground();
+ if (_avoidRenderingFrames == 0)
+ _currentArea->drawDepthLayer(_gfx, _ticks / 10, _position, _cameraFront, false, Area::kRenderDepthBackground, stereoForegroundDistance);
+
+ for (int pass = 0; pass < 2; pass++) {
+ _gfx->setStereoEye(pass == 0 ? Renderer::kStereoEyeLeft : Renderer::kStereoEyeRight);
+ _gfx->updateProjectionMatrix(fov, aspectRatio, _nearClipPlane, farClipPlane);
+ _gfx->positionCamera(_position, _position + _cameraFront, _roll);
+
+ _gfx->clearDepthBuffer();
+
+ if (_avoidRenderingFrames == 0) // Avoid rendering inside objects
+ _currentArea->drawDepthLayer(_gfx, _ticks / 10, _position, _cameraFront, false, Area::kRenderDepthForeground, stereoForegroundDistance);
+
+ if (_underFireFrames > 0) {
+ for (auto &it : _sensors) {
+ Sensor *sensor = (Sensor *)it;
+ if (it->isDestroyed() || it->isInvisible())
+ continue;
+ if (isCastle() || sensor->isShooting())
+ drawSensorShoot(sensor);
+ }
+ }
+
+ if (_shootingFrames > 0) {
+ _gfx->setViewport(_fullscreenViewArea);
+ if (isDriller() || isDark())
+ _gfx->renderPlayerShootRay(0, _crossairPosition, _viewArea);
+ else
+ _gfx->renderPlayerShootBall(0, _crossairPosition, _shootingFrames, _viewArea);
+
+ _gfx->setViewport(_viewArea);
+ }
+ }
+ _gfx->setStereoEye(Renderer::kStereoEyeNone);
+
+ if (_avoidRenderingFrames == 0) {
+ if (_gameStateControl == kFreescapeGameStatePlaying &&
+ _currentArea->hasActiveGroups() && _ticks % 50 == 0)
+ executeMovementConditions();
+ } else
+ _avoidRenderingFrames--;
+
+ if (_underFireFrames > 0) {
+ _underFireFrames--;
+ if (_underFireFrames == 0) {
+ _currentArea->unremapColor(_currentArea->_usualBackgroundColor);
+ _currentArea->unremapColor(_currentArea->_skyColor);
+ }
+ }
+
+ if (_shootingFrames > 0)
+ _shootingFrames--;
+
+ drawBorder();
+ drawUI();
+}
+
void FreescapeEngine::pressedKey(const int keycode) {}
void FreescapeEngine::releasedKey(const int keycode) {}
@@ -670,6 +895,14 @@ void FreescapeEngine::processInput() {
_noClipMode = !_noClipMode;
_flyMode = _noClipMode;
break;
+ case kActionToggleStereoscopic:
+ // Limit this to the ZX Spectrum games for now; their restricted
+ // palette survives red/blue channel separation cleanly.
+ if (isSpectrum()) {
+ _stereoMode = !_stereoMode;
+ insertTemporaryMessage(_stereoMode ? "3D ON" : "3D OFF", _countdown - 2);
+ }
+ break;
case kActionEscape:
drawFrame();
_savedScreen = _gfx->getScreenshot();
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index a7ada28eba7..960019dc506 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -113,6 +113,7 @@ enum FreescapeAction {
kActionSelectPrincess,
kActionQuit,
kActionToggleFlashlight,
+ kActionToggleStereoscopic,
// Demo actions
kActionUnknownKey,
@@ -569,6 +570,10 @@ public:
int _shootingFrames;
GeometricObject *_delayedShootObject;
void drawFrame();
+ void drawFrameStereo(int farClipPlane);
+
+ // Red/blue anaglyph 3D ("two eyes") effect, toggled with the 3 key.
+ bool _stereoMode;
void flashScreen(int backgroundColor);
uint8 _colorNumber;
Math::Vector3d _scaleVector;
diff --git a/engines/freescape/gfx.cpp b/engines/freescape/gfx.cpp
index d8172b7ad87..532d325d4ae 100644
--- a/engines/freescape/gfx.cpp
+++ b/engines/freescape/gfx.cpp
@@ -59,6 +59,9 @@ Renderer::Renderer(int screenW, int screenH, Common::RenderMode renderMode, bool
_debugRenderWireframe = false;
_debugRenderNormals = false;
_authenticGraphics = authenticGraphics;
+ _stereoEye = kStereoEyeNone;
+ _stereoSeparation = 0.2f;
+ _stereoConvergence = 800.0f;
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 128; j++) {
@@ -70,6 +73,54 @@ Renderer::Renderer(int screenW, int screenH, Common::RenderMode renderMode, bool
_scale = 1;
}
+void Renderer::applyStereoTint(uint8 &r, uint8 &g, uint8 &b) const {
+ if (_stereoEye == kStereoEyeNone)
+ return;
+
+ uint8 lum = (uint8)((r * 77 + g * 150 + b * 29) >> 8);
+ if (_stereoEye == kStereoEyeLeft) {
+ r = lum;
+ g = 0;
+ b = 0;
+ } else if (_stereoEye == kStereoEyeRight) {
+ r = 0;
+ g = 0;
+ b = lum;
+ } else if (_stereoEye == kStereoEyeFlatAnaglyph) {
+ r = lum;
+ g = 0;
+ b = lum;
+ }
+}
+
+void Renderer::setStereoParameters(float separation, float convergence) {
+ _stereoSeparation = separation;
+ _stereoConvergence = convergence;
+}
+
+void Renderer::getStereoCamera(const Math::Vector3d &pos, const Math::Vector3d &interest, Math::Vector3d &eyePos, Math::Vector3d &eyeInterest) const {
+ eyePos = pos;
+ eyeInterest = interest;
+ if (_stereoEye != kStereoEyeLeft && _stereoEye != kStereoEyeRight)
+ return;
+
+ Math::Vector3d up(0, 1, 0);
+ Math::Vector3d front = (interest - pos).getNormalized();
+ Math::Vector3d right = Math::Vector3d::crossProduct(front, up).getNormalized();
+
+ Math::Vector3d eyeOffset = right * (-_stereoSeparation * _stereoEye);
+ eyePos = pos + eyeOffset;
+ eyeInterest = interest + eyeOffset;
+}
+
+float Renderer::getStereoFrustumOffset(float nearClipPlane, bool mirroredProjection) const {
+ if (_stereoEye != kStereoEyeLeft && _stereoEye != kStereoEyeRight)
+ return 0.0f;
+
+ float offset = (-_stereoSeparation * _stereoEye) * nearClipPlane / _stereoConvergence;
+ return mirroredProjection ? -offset : offset;
+}
+
Renderer::~Renderer() {}
byte getCPCStipple(byte cpc_byte, int back, int fore) {
diff --git a/engines/freescape/gfx.h b/engines/freescape/gfx.h
index 1986ea5db35..37432fb2ebd 100644
--- a/engines/freescape/gfx.h
+++ b/engines/freescape/gfx.h
@@ -76,6 +76,21 @@ public:
* Swap the buffers, making the drawn screen visible
*/
virtual void flipBuffer() {}
+
+ /**
+ * Select the current eye for the red/blue stereoscopic 3D effect.
+ * 0 = disabled (normal rendering), -1 = left eye (red), +1 = right eye (blue),
+ * 2 = mono anaglyph at screen depth (red + blue).
+ */
+ enum StereoEye {
+ kStereoEyeNone = 0,
+ kStereoEyeLeft = -1,
+ kStereoEyeRight = 1,
+ kStereoEyeFlatAnaglyph = 2
+ };
+ virtual void setStereoEye(StereoEye eye) { _stereoEye = eye; }
+ void setStereoParameters(float separation, float convergence);
+
virtual void useColor(uint8 r, uint8 g, uint8 b) = 0;
virtual void enableCulling(bool enabled) {};
virtual void polygonOffset(bool enabled) = 0;
@@ -100,6 +115,7 @@ public:
void setColorRemaps(ColorReMap *colorRemaps);
virtual void clear(uint8 r, uint8 g, uint8 b, bool ignoreViewport = false) = 0;
+ virtual void clearDepthBuffer(bool ignoreViewport = false) {}
virtual void drawFloor(uint8 color) = 0;
virtual void drawBackground(uint8 color);
@@ -343,6 +359,13 @@ protected:
Math::Frustum _frustum;
Math::Matrix4 makeProjectionMatrix(float fov, float nearClipPlane, float farClipPlane) const;
+ void applyStereoTint(uint8 &r, uint8 &g, uint8 &b) const;
+ void getStereoCamera(const Math::Vector3d &pos, const Math::Vector3d &interest, Math::Vector3d &eyePos, Math::Vector3d &eyeInterest) const;
+ float getStereoFrustumOffset(float nearClipPlane, bool mirroredProjection) const;
+
+ StereoEye _stereoEye;
+ float _stereoSeparation; // half of the inter-ocular distance, in world units
+ float _stereoConvergence; // distance to the zero-parallax plane, in world units
};
Graphics::RendererType determinateRenderType();
diff --git a/engines/freescape/gfx_opengl.cpp b/engines/freescape/gfx_opengl.cpp
index 7f11d3e750c..7d3adfe0ea4 100644
--- a/engines/freescape/gfx_opengl.cpp
+++ b/engines/freescape/gfx_opengl.cpp
@@ -255,7 +255,12 @@ void OpenGLRenderer::updateProjectionMatrix(float fov, float aspectRatio, float
float ymaxValue = xmaxValue / aspectRatio;
// Corrected glFrustum call
- glFrustum(-xmaxValue, xmaxValue, -ymaxValue, ymaxValue, nearClipPlane, farClipPlane);
+ if (_stereoEye == kStereoEyeLeft || _stereoEye == kStereoEyeRight) {
+ float stereoOffset = getStereoFrustumOffset(nearClipPlane, false);
+ glFrustum(-xmaxValue + stereoOffset, xmaxValue + stereoOffset, -ymaxValue, ymaxValue, nearClipPlane, farClipPlane);
+ } else {
+ glFrustum(-xmaxValue, xmaxValue, -ymaxValue, ymaxValue, nearClipPlane, farClipPlane);
+ }
glScalef(-1.0f, 1.0f, 1.0f);
glMatrixMode(GL_MODELVIEW);
@@ -265,10 +270,20 @@ void OpenGLRenderer::updateProjectionMatrix(float fov, float aspectRatio, float
void OpenGLRenderer::positionCamera(const Math::Vector3d &pos, const Math::Vector3d &interest, float rollAngle) {
Math::Vector3d up_vec(0, 1, 0);
- Math::Matrix4 lookMatrix = Math::makeLookAtMatrix(pos, interest, up_vec);
- glMultMatrixf(lookMatrix.getData());
- glRotatef(rollAngle, 0.0f, 0.0f, 1.0f);
- glTranslatef(-pos.x(), -pos.y(), -pos.z());
+ if (_stereoEye == kStereoEyeLeft || _stereoEye == kStereoEyeRight) {
+ Math::Vector3d eyePos;
+ Math::Vector3d eyeInterest;
+ getStereoCamera(pos, interest, eyePos, eyeInterest);
+ Math::Matrix4 lookMatrix = Math::makeLookAtMatrix(eyePos, eyeInterest, up_vec);
+ glMultMatrixf(lookMatrix.getData());
+ glRotatef(rollAngle, 0.0f, 0.0f, 1.0f);
+ glTranslatef(-eyePos.x(), -eyePos.y(), -eyePos.z());
+ } else {
+ Math::Matrix4 lookMatrix = Math::makeLookAtMatrix(pos, interest, up_vec);
+ glMultMatrixf(lookMatrix.getData());
+ glRotatef(rollAngle, 0.0f, 0.0f, 1.0f);
+ glTranslatef(-pos.x(), -pos.y(), -pos.z());
+ }
// Apply a 2D shake effect on the projection matrix.
// This avoids moving the camera in the 3D world, which could cause clipping issues.
@@ -602,11 +617,28 @@ void OpenGLRenderer::useStipple(bool enabled) {
}
}
+void OpenGLRenderer::setStereoEye(StereoEye eye) {
+ Renderer::setStereoEye(eye);
+ // Restrict stereo passes to the anaglyph channels they are allowed to touch.
+ if (eye == kStereoEyeNone)
+ glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+ else if (eye == kStereoEyeLeft)
+ glColorMask(GL_TRUE, GL_FALSE, GL_FALSE, GL_TRUE); // left eye -> red
+ else if (eye == kStereoEyeRight)
+ glColorMask(GL_FALSE, GL_FALSE, GL_TRUE, GL_TRUE); // right eye -> blue
+ else if (eye == kStereoEyeFlatAnaglyph)
+ glColorMask(GL_TRUE, GL_FALSE, GL_TRUE, GL_TRUE); // flat background -> red + blue
+}
+
void OpenGLRenderer::useColor(uint8 r, uint8 g, uint8 b) {
+ if (_stereoEye != kStereoEyeNone)
+ applyStereoTint(r, g, b);
glColor4ub(r, g, b, 255);
}
void OpenGLRenderer::clear(uint8 r, uint8 g, uint8 b, bool ignoreViewport) {
+ if (_stereoEye != kStereoEyeNone)
+ applyStereoTint(r, g, b);
if (ignoreViewport)
glDisable(GL_SCISSOR_TEST);
glClearColor(r / 255., g / 255., b / 255., 1.0);
@@ -615,10 +647,20 @@ void OpenGLRenderer::clear(uint8 r, uint8 g, uint8 b, bool ignoreViewport) {
glEnable(GL_SCISSOR_TEST);
}
+void OpenGLRenderer::clearDepthBuffer(bool ignoreViewport) {
+ if (ignoreViewport)
+ glDisable(GL_SCISSOR_TEST);
+ glClear(GL_DEPTH_BUFFER_BIT);
+ if (ignoreViewport)
+ glEnable(GL_SCISSOR_TEST);
+}
+
void OpenGLRenderer::drawFloor(uint8 color) {
uint8 r1, g1, b1, r2, g2, b2;
byte *stipple;
assert(getRGBAt(color, 0, r1, g1, b1, r2, g2, b2, stipple)); // TODO: move check inside this function
+ if (_stereoEye != kStereoEyeNone)
+ applyStereoTint(r1, g1, b1);
glColor4ub(r1, g1, b1, 255);
glEnableClientState(GL_VERTEX_ARRAY);
diff --git a/engines/freescape/gfx_opengl.h b/engines/freescape/gfx_opengl.h
index 691faa42222..eda2b72b43e 100644
--- a/engines/freescape/gfx_opengl.h
+++ b/engines/freescape/gfx_opengl.h
@@ -67,6 +67,7 @@ public:
virtual void init() override;
virtual void clear(uint8 r, uint8 g, uint8 b, bool ignoreViewport = false) override;
+ virtual void clearDepthBuffer(bool ignoreViewport = false) override;
virtual void setViewport(const Common::Rect &rect) override;
virtual Common::Point nativeResolution() override;
virtual void positionCamera(const Math::Vector3d &pos, const Math::Vector3d &interest, float rollAngle = 0.0) override;
@@ -78,6 +79,7 @@ public:
virtual void useStipple(bool enabled) override;
virtual void enableCulling(bool enabled) override;
+ virtual void setStereoEye(StereoEye eye) override;
Texture *createTexture(const Graphics::Surface *surface, bool is3D = false) override;
void freeTexture(Texture *texture) override;
diff --git a/engines/freescape/gfx_opengl_shaders.cpp b/engines/freescape/gfx_opengl_shaders.cpp
index 0effc9fb0ce..bcb5aa16881 100644
--- a/engines/freescape/gfx_opengl_shaders.cpp
+++ b/engines/freescape/gfx_opengl_shaders.cpp
@@ -286,15 +286,31 @@ void OpenGLShaderRenderer::drawThunder(Texture *texture, const Math::Vector3d po
void OpenGLShaderRenderer::updateProjectionMatrix(float fov, float aspectRatio, float nearClipPlane, float farClipPlane) {
float xmaxValue = nearClipPlane * tan(Math::deg2rad(fov) / 2);
float ymaxValue = xmaxValue / aspectRatio;
- _projectionMatrix = Math::makeFrustumMatrix(xmaxValue, -xmaxValue, -ymaxValue, ymaxValue, nearClipPlane, farClipPlane);
+ if (_stereoEye == kStereoEyeLeft || _stereoEye == kStereoEyeRight) {
+ float stereoOffset = getStereoFrustumOffset(nearClipPlane, true);
+ _projectionMatrix = Math::makeFrustumMatrix(xmaxValue + stereoOffset, -xmaxValue + stereoOffset, -ymaxValue, ymaxValue, nearClipPlane, farClipPlane);
+ } else {
+ _projectionMatrix = Math::makeFrustumMatrix(xmaxValue, -xmaxValue, -ymaxValue, ymaxValue, nearClipPlane, farClipPlane);
+ }
}
void OpenGLShaderRenderer::positionCamera(const Math::Vector3d &pos, const Math::Vector3d &interest, float rollAngle) {
Math::Vector3d up_vec(0, 1, 0);
- Math::Matrix4 lookMatrix = Math::makeLookAtMatrix(pos, interest, up_vec);
+ Math::Matrix4 lookMatrix;
+ Math::Vector3d viewPosition;
+ if (_stereoEye == kStereoEyeLeft || _stereoEye == kStereoEyeRight) {
+ Math::Vector3d eyePos;
+ Math::Vector3d eyeInterest;
+ getStereoCamera(pos, interest, eyePos, eyeInterest);
+ lookMatrix = Math::makeLookAtMatrix(eyePos, eyeInterest, up_vec);
+ viewPosition = eyePos;
+ } else {
+ lookMatrix = Math::makeLookAtMatrix(pos, interest, up_vec);
+ viewPosition = pos;
+ }
Math::Matrix4 viewMatrix;
- viewMatrix.translate(-pos);
+ viewMatrix.translate(-viewPosition);
viewMatrix.transpose();
// Roll around the camera's forward axis. The matrix is stored in the
@@ -810,13 +826,29 @@ void OpenGLShaderRenderer::useStipple(bool enabled) {
}
}
+void OpenGLShaderRenderer::setStereoEye(StereoEye eye) {
+ Renderer::setStereoEye(eye);
+ if (eye == kStereoEyeNone)
+ glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+ else if (eye == kStereoEyeLeft)
+ glColorMask(GL_TRUE, GL_FALSE, GL_FALSE, GL_TRUE);
+ else if (eye == kStereoEyeRight)
+ glColorMask(GL_FALSE, GL_FALSE, GL_TRUE, GL_TRUE);
+ else if (eye == kStereoEyeFlatAnaglyph)
+ glColorMask(GL_TRUE, GL_FALSE, GL_TRUE, GL_TRUE);
+}
+
void OpenGLShaderRenderer::useColor(uint8 r, uint8 g, uint8 b) {
+ if (_stereoEye != kStereoEyeNone)
+ applyStereoTint(r, g, b);
Math::Vector3d color(r / 256.0, g / 256.0, b / 256.0);
_triangleShader->use();
_triangleShader->setUniform("color", color);
}
void OpenGLShaderRenderer::clear(uint8 r, uint8 g, uint8 b, bool ignoreViewport) {
+ if (_stereoEye != kStereoEyeNone)
+ applyStereoTint(r, g, b);
if (ignoreViewport)
glDisable(GL_SCISSOR_TEST);
glClearColor(r / 255., g / 255., b / 255., 1.0);
@@ -825,6 +857,14 @@ void OpenGLShaderRenderer::clear(uint8 r, uint8 g, uint8 b, bool ignoreViewport)
glEnable(GL_SCISSOR_TEST);
}
+void OpenGLShaderRenderer::clearDepthBuffer(bool ignoreViewport) {
+ if (ignoreViewport)
+ glDisable(GL_SCISSOR_TEST);
+ glClear(GL_DEPTH_BUFFER_BIT);
+ if (ignoreViewport)
+ glEnable(GL_SCISSOR_TEST);
+}
+
void OpenGLShaderRenderer::drawFloor(uint8 color) {
/*uint8 r1, g1, b1, r2, g2, b2;
byte *stipple;
diff --git a/engines/freescape/gfx_opengl_shaders.h b/engines/freescape/gfx_opengl_shaders.h
index e6fb9d0affc..824f5672d00 100644
--- a/engines/freescape/gfx_opengl_shaders.h
+++ b/engines/freescape/gfx_opengl_shaders.h
@@ -75,6 +75,7 @@ public:
virtual void init() override;
virtual void drawAABB(const Math::AABB &aabb, uint8 r, uint8 g, uint8 b) override;
virtual void clear(uint8 r, uint8 g, uint8 b, bool ignoreViewport = false) override;
+ virtual void clearDepthBuffer(bool ignoreViewport = false) override;
virtual void setViewport(const Common::Rect &rect) override;
virtual Common::Point nativeResolution() override;
virtual void positionCamera(const Math::Vector3d &pos, const Math::Vector3d &interest, float rollAngle = 0.0f) override;
@@ -83,6 +84,7 @@ public:
virtual void useColor(uint8 r, uint8 g, uint8 b) override;
virtual void polygonOffset(bool enabled) override;
virtual void enableCulling(bool enabled) override;
+ virtual void setStereoEye(StereoEye eye) override;
virtual void setStippleData(byte *data) override;
virtual void useStipple(bool enabled) override;
diff --git a/engines/freescape/movement.cpp b/engines/freescape/movement.cpp
index 81d389cecb9..b694793a5bb 100644
--- a/engines/freescape/movement.cpp
+++ b/engines/freescape/movement.cpp
@@ -114,6 +114,12 @@ void FreescapeEngine::initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *
act->addDefaultInputMapping("i");
act->addDefaultInputMapping("JOY_GUIDE");
engineKeyMap->addAction(act);
+
+ // I18N: Toggles the red/blue stereoscopic 3D effect (anaglyph glasses).
+ act = new Common::Action("STEREO3D", _("Toggle red/blue 3D"));
+ act->setCustomEngineActionEvent(kActionToggleStereoscopic);
+ act->addDefaultInputMapping("3");
+ engineKeyMap->addAction(act);
}
Math::AABB createPlayerAABB(Math::Vector3d const position, int playerHeight, float reductionHeight = 0.0f) {
Commit: 05707392b982676f16135b7aea61e9633dcdeacf
https://github.com/scummvm/scummvm/commit/05707392b982676f16135b7aea61e9633dcdeacf
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-23T20:56:51+02:00
Commit Message:
FREESCAPE: removed buggy collision workaround
Changed paths:
engines/freescape/sweepAABB.cpp
diff --git a/engines/freescape/sweepAABB.cpp b/engines/freescape/sweepAABB.cpp
index 0055fd3b1e8..426c0e15c38 100644
--- a/engines/freescape/sweepAABB.cpp
+++ b/engines/freescape/sweepAABB.cpp
@@ -44,37 +44,6 @@ float sweepAABB(Math::AABB const &a, Math::AABB const &b, Math::Vector3d const &
Math::Vector3d m = b.getMin() - a.getMax();
Math::Vector3d mh = a.getSize() + b.getSize();
- // Overlap-at-start: the AABBs already intersect on every axis. The
- // plane sweep below requires s >= 0 for every test, so it would
- // incorrectly return "no collision" and the caller would let the
- // player walk straight through the geometry they are already inside.
- // Detect that case here, pick the axis of smallest overlap, and emit
- // a t=0 collision with a unit normal pointing out along that axis.
- // The caller's epsilon * normal push then pries the player out of
- // the overlap incrementally over a few frames.
- if (m.x() < 0 && m.x() + mh.x() > 0 &&
- m.y() < 0 && m.y() + mh.y() > 0 &&
- m.z() < 0 && m.z() + mh.z() > 0) {
- // Overlap depth on the "push -axis" side vs the "push +axis" side
- // (both values are positive while we are overlapping).
- float ovNegX = -m.x();
- float ovPosX = m.x() + mh.x();
- float ovNegY = -m.y();
- float ovPosY = m.y() + mh.y();
- float ovNegZ = -m.z();
- float ovPosZ = m.z() + mh.z();
- float minX = MIN(ovNegX, ovPosX);
- float minY = MIN(ovNegY, ovPosY);
- float minZ = MIN(ovNegZ, ovPosZ);
- if (minX <= minY && minX <= minZ)
- normal = Math::Vector3d(ovNegX < ovPosX ? -1.0f : 1.0f, 0, 0);
- else if (minY <= minZ)
- normal = Math::Vector3d(0, ovNegY < ovPosY ? -1.0f : 1.0f, 0);
- else
- normal = Math::Vector3d(0, 0, ovNegZ < ovPosZ ? -1.0f : 1.0f);
- return 0.0f;
- }
-
float h = 1.0;
float s = 0.0;
Math::Vector3d zero;
Commit: 4c18772a9818934854883774999485ce016f627e
https://github.com/scummvm/scummvm/commit/4c18772a9818934854883774999485ce016f627e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-23T20:56:51+02:00
Commit Message:
FREESCAPE: sensor fixes, in particular for driller amiga/atari
Changed paths:
engines/freescape/loaders/8bitBinaryLoader.cpp
engines/freescape/objects/sensor.cpp
diff --git a/engines/freescape/loaders/8bitBinaryLoader.cpp b/engines/freescape/loaders/8bitBinaryLoader.cpp
index 8a36f73cd35..838e05deec8 100644
--- a/engines/freescape/loaders/8bitBinaryLoader.cpp
+++ b/engines/freescape/loaders/8bitBinaryLoader.cpp
@@ -561,6 +561,10 @@ Object *FreescapeEngine::load8bitObject(Common::SeekableReadStream *file) {
assert(color > 0);
byte firingInterval = readField(file, 8);
uint16 firingRange = readPtr(file) / 2;
+ // Driller Amiga/ST stores sensor ranges in the original 64-units-per-cell
+ // coordinate space. ScummVM normalizes Driller geometry to 32 units per cell.
+ if (isDriller() && (isAmiga() || isAtariST()))
+ firingRange = firingRange / 2;
if (isDark())
firingRange = firingRange / 2;
byte sensorAxis = readField(file, 8);
diff --git a/engines/freescape/objects/sensor.cpp b/engines/freescape/objects/sensor.cpp
index 165018717d0..cb1788f1698 100644
--- a/engines/freescape/objects/sensor.cpp
+++ b/engines/freescape/objects/sensor.cpp
@@ -68,6 +68,7 @@ Sensor::Sensor(
void Sensor::scale(int factor) {
_origin = _origin / factor;
_size = _size / factor;
+ _firingRange = _firingRange / factor;
}
Object *Sensor::duplicate() {
@@ -105,7 +106,8 @@ bool Sensor::playerDetected(const Math::Vector3d &position, Area *area) {
}
if (detected) {
- detected = ABS(diff.x() + ABS(diff.y())) + ABS(diff.z()) <= _firingRange;
+ float distance = ABS(diff.x()) + ABS(diff.y()) + ABS(diff.z());
+ detected = distance < _firingRange;
}
return detected;
Commit: 04795e7633ec19cf515870c8eb69f55116cbd3d8
https://github.com/scummvm/scummvm/commit/04795e7633ec19cf515870c8eb69f55116cbd3d8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-23T20:56:51+02:00
Commit Message:
FREESCAPE: make sure saving/loading keep the game state consistent specially when the player dies
Changed paths:
engines/freescape/freescape.cpp
engines/freescape/freescape.h
diff --git a/engines/freescape/freescape.cpp b/engines/freescape/freescape.cpp
index 863c3579e37..43a3b06f69f 100644
--- a/engines/freescape/freescape.cpp
+++ b/engines/freescape/freescape.cpp
@@ -1321,6 +1321,7 @@ void FreescapeEngine::initGameState() {
_flyMode = false;
_noClipMode = false;
+ _hasFallen = false;
_playerWasCrushed = false;
_shootingFrames = 0;
_delayedShootObject = nullptr;
@@ -1432,6 +1433,14 @@ Common::Error FreescapeEngine::loadGameStream(Common::SeekableReadStream *stream
_flyMode = stream->readByte();
_noClipMode = false;
+ // Reset transient "player has fallen" state. Otherwise, having fallen out
+ // of the world before saving (or loading in-game right after a fall)
+ // persists and immediately ends the loaded game. Falling also tilts the
+ // camera (_roll) in some games and that value is not stored in the save,
+ // so reset it as well. Reported for Driller (Amiga), but applies to all games.
+ _hasFallen = false;
+ _roll = 0;
+ _avoidRenderingFrames = 0;
_playerHeightNumber = stream->readSint32LE();
_playerStepIndex = stream->readUint32LE();
_countdown = stream->readUint32LE();
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index 960019dc506..7aabd4adf40 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -39,6 +39,7 @@
#include "freescape/area.h"
#include "freescape/font.h"
#include "freescape/gfx.h"
+#include "freescape/language/8bitDetokeniser.h"
#include "freescape/objects/entrance.h"
#include "freescape/objects/geometricobject.h"
#include "freescape/objects/sensor.h"
@@ -643,7 +644,11 @@ public:
bool hasFeature(EngineFeature f) const override;
bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override { return true; }
bool canSaveAutosaveCurrently() override { return false; }
- bool canSaveGameStateCurrently(Common::U32String *msg = nullptr) override { return _gameStateControl == kFreescapeGameStatePlaying && _currentArea; }
+ // Disallow saving while the game is not in the playing state, or while the
+ // player is dead/not controllable (fallen out of the world, crushed, or out
+ // of health/shield). Otherwise that transient state would be captured into
+ // the savegame.
+ bool canSaveGameStateCurrently(Common::U32String *msg = nullptr) override { return _gameStateControl == kFreescapeGameStatePlaying && _currentArea && !_hasFallen && !_playerWasCrushed && _gameStateVars.getValOrDefault(k8bitVariableShield) > 0; }
Common::Error loadGameStream(Common::SeekableReadStream *stream) override;
Common::Error saveGameStream(Common::WriteStream *stream, bool isAutosave = false) override;
virtual Common::Error saveGameStreamExtended(Common::WriteStream *stream, bool isAutosave = false);
More information about the Scummvm-git-logs
mailing list