[Scummvm-git-logs] scummvm master -> 241b138f5e385b5d8f2f25e3ab58564f7e70d5e9
neuromancer
noreply at scummvm.org
Mon Sep 7 05:34:14 UTC 2026
This automated email contains information about 15 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
6a20f330d8 SCUMM: RA1: better coordination when playing frames out of order during player path decisions
69dc0313b5 FREESCAPE: completed castle UI for C64
7ca4207a12 FREESCAPE: added gate parsing/rendering for castle c64
217eb9e715 FREESCAPE: refactored music and added sound effects for castle c64
81583b292c FREESCAPE: background and lighting in castle c64
1e6f0d0504 FREESCAPE: ghost attack effect implemented in castle c64
3d55dacb49 FREESCAPE: reworked painters algorithm to fix some artifacts
253f3b0ea9 FREESCAPE: implemented loading of simple geometry for 3DCK games
be883d1a56 FREESCAPE: allow to load geometry of the 3DCK sample game
f65035d8f0 FREESCAPE: initial implementation of 3DCK opcodes and scripting
c567de577a FREESCAPE: refactored instructions code into functions for the 3dck
effb13c821 FREESCAPE: mouselook unlocked for 3dck
01bb2300a0 FREESCAPE: added 8bit opcodes and scripting for 3dck
ce36579e2f FREESCAPE: refactored language scripting for freescape/3dck
241b138f5e FREESCAPE: refactored freescape script detokenizer to match other similar functions
Commit: 6a20f330d8ede2816283fdcc175eb77c440782d6
https://github.com/scummvm/scummvm/commit/6a20f330d8ede2816283fdcc175eb77c440782d6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-07T07:23:52+02:00
Commit Message:
SCUMM: RA1: better coordination when playing frames out of order during player path decisions
Changed paths:
engines/scumm/insane/rebel1/iact.cpp
engines/scumm/insane/rebel1/rebel.h
engines/scumm/insane/rebel1/render.cpp
engines/scumm/insane/rebel1/runlevels.cpp
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 1a799bbbbf7..683657b0946 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -762,8 +762,8 @@ void InsaneRebel1::checkDynamicLevelBranch(int32 curFrame) {
}
}
- // Level 8 owns its branch choice in updateLevel8WalkerState(), where the
- // choice variable. This function only performs the delayed route cutover.
+ // Level 8 schedules its branch in updateLevel8WalkerState() and commits it
+ // after rendering the following frame.
}
void InsaneRebel1::projectGameplayPoint(int16 &x, int16 &y) const {
@@ -1228,17 +1228,21 @@ void InsaneRebel1::updateShipPhysics() {
_damageFlags = 0;
- // After this point, drift goes strongly negative (pushing ship left for the hard path).
- if (_pathBranchEnabled && _gameCounter >= kPathBranchCounter) {
- if (_shipPosX > kRA1CenterX) {
- _rightPathSelected = true;
+ // The original chooses at frame 386, then keeps the source through frame
+ // 391. The right-hand clip resumes at local frame 1 after that shared frame.
+ if (_pathBranchEnabled && _currentSmushFrame >= kLevel1BranchDecisionFrame) {
+ if (!_rightPathSelected) {
+ _rightPathSelected = _shipPosX > kRA1CenterX;
+ if (!_rightPathSelected)
+ _pathBranchEnabled = false;
+ debugC(DEBUG_INSANE, "L1 path selected: right=%d localFrame=%d shipX=%d",
+ _rightPathSelected ? 1 : 0, (int)_currentSmushFrame, _shipPosX);
+ }
+ if (_rightPathSelected && _currentSmushFrame >= kLevel1BranchCutoverFrame) {
+ _pathBranchEnabled = false;
preserveInteractiveVideoAudioState();
_vm->_smushVideoShouldFinish = true;
- debugC(DEBUG_INSANE, "Right path selected (counter=%d, shipX=%d)", _gameCounter, _shipPosX);
- } else {
- debugC(DEBUG_INSANE, "Left path retained (counter=%d, shipX=%d)", _gameCounter, _shipPosX);
}
- _pathBranchEnabled = false;
}
if (_currentLevel != 6)
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 1b77886e368..40bd8709062 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -246,7 +246,7 @@ private:
void preserveInteractiveVideoAudioState();
void setupInteractiveVideoState(int32 startFrame);
void resolveSeek(const char *filename, int32 startFrame, int32 &videoOffset, int32 &videoStartFrame);
- void captureInteractiveVideoInput();
+ void captureInteractiveVideoInput(bool preserveInputState);
void releaseInteractiveVideoInput();
void playInteractiveVideoFile(const char *filename, int32 videoOffset, int32 videoStartFrame);
void enableIOSGamepadController();
@@ -555,7 +555,8 @@ private:
RebelTouchTapDetector _touchTapDetector;
// Path branching for levels with left/right alternative videos.
- static const int32 kPathBranchCounter = 394;
+ static const int32 kLevel1BranchDecisionFrame = 0x182;
+ static const int32 kLevel1BranchCutoverFrame = 0x187;
int32 _gameCounter;
bool _pathBranchEnabled;
bool _rightPathSelected;
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 957d149ec53..d10ba45825e 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -776,6 +776,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
if (_currentLevel == 7) {
updateLevel8WalkerState();
+ checkDynamicLevelBranch(curFrame);
const int viewportX = _player ? ra1Player()->_ra1ViewportOffsetX : 0;
const int viewportY = _player ? ra1Player()->_ra1ViewportOffsetY : 0;
renderLevel8Overlay(renderBitmap, pitch, width, height, viewportX, viewportY);
@@ -1855,10 +1856,11 @@ void InsaneRebel1::updateLevel8WalkerState() {
if (newRoute != 0) {
_pendingRouteIndex = newRoute;
- _pendingRouteCutoverFrame = _currentSmushFrame + 7;
- // The destination starts at frame 1, advanced by the source tail
- // already displayed. This also applies when repeating the same route.
- _pendingRouteStartFrame = 1 + (_pendingRouteCutoverFrame - _currentSmushFrame);
+ // The original makes this choice in the post-frame callback, after
+ // that frame's splice check. It renders one more source frame before
+ // switching, advancing the destination's frame-1 start by that frame.
+ _pendingRouteCutoverFrame = _currentSmushFrame + 1;
+ _pendingRouteStartFrame = 2;
debugC(DEBUG_INSANE, "L8 branch: route=%d -> %d at localFrame=%u shipX=%d resumeLocalFrame=%d cutoverFrame=%d",
route, newRoute, (unsigned)fc, _shipPosX,
(int)_pendingRouteStartFrame, (int)_pendingRouteCutoverFrame);
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 14ad794e5ec..c9a28805994 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -287,10 +287,10 @@ bool InsaneRebel1::runLevel1() {
if (shouldAbortGameFlow())
return false;
- if (_rightPathSelected && _health >= 0) {
+ if (_rightPathSelected && !_interactiveVideoCheatSkipped && _health >= 0) {
_pathBranchEnabled = false;
_flyControlMode = 1;
- playInteractiveVideo("LVL1/L1PLAY1R.ANM", 0x187);
+ playInteractiveVideo("LVL1/L1PLAY1R.ANM", 1);
if (shouldAbortGameFlow())
return false;
}
@@ -1543,19 +1543,20 @@ void InsaneRebel1::resolveSeek(const char *filename, int32 startFrame, int32 &vi
_levelRouteIndex, (int)_pendingRouteStartFrame,
(int)videoStartFrame, (unsigned)videoOffset);
}
- } else if (_currentLevel == 7 && resumingRoute) {
- // Walker routes restart at their own local frame, even when branching
- // back to the same ANM. Their embedded GAME counters are not seek targets.
+ } else if ((_currentLevel == 0 || _currentLevel == 7) && resumingRoute) {
+ // Route continuations use destination-local frames. In L1 this skips
+ // both the overlapping frame and the GAME reset at the start of the
+ // right-hand clip. L8 can also branch back to the same ANM.
videoStartFrame = startFrame;
videoOffset = findAnimFrameChunkOffset(_vm, filename, videoStartFrame);
if (videoOffset < 0) {
- debugC(DEBUG_INSANE, "L8 resume: route=%d localFrame=%d offset lookup failed",
- _levelRouteIndex, (int)videoStartFrame);
+ debugC(DEBUG_INSANE, "L%d resume: localFrame=%d offset lookup failed",
+ _currentLevel + 1, (int)videoStartFrame);
videoStartFrame = 0;
videoOffset = 0;
} else {
- debugC(DEBUG_INSANE, "L8 resume: route=%d localFrame=%d offset=0x%x",
- _levelRouteIndex, (int)videoStartFrame, (unsigned)videoOffset);
+ debugC(DEBUG_INSANE, "L%d resume: localFrame=%d offset=0x%x",
+ _currentLevel + 1, (int)videoStartFrame, (unsigned)videoOffset);
}
} else if (_currentLevel == 13 && resumingRoute) {
// L14PLY2B is already the continuation clip. Preserve state, but do not seek.
@@ -1564,12 +1565,7 @@ void InsaneRebel1::resolveSeek(const char *filename, int32 startFrame, int32 &vi
}
}
-void InsaneRebel1::captureInteractiveVideoInput() {
- const bool level7RouteSplice = (_currentLevel == 6 && _levelRouteIndex > 0);
- const bool walkerRouteContinuation = (_currentLevel == 7 &&
- (_walkerRoundReplay || _pendingRouteStartFrame > 0));
- const bool preserveInputState = _preserveInteractiveRuntimeState || level7RouteSplice || walkerRouteContinuation;
-
+void InsaneRebel1::captureInteractiveVideoInput(bool preserveInputState) {
enableIOSGamepadController();
// Center mouse, hide system cursor, and lock mouse to window.
@@ -1625,7 +1621,7 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
resetInteractiveVideoAudio();
setupInteractiveVideoState(startFrame);
resolveSeek(filename, startFrame, videoOffset, videoStartFrame);
- captureInteractiveVideoInput();
+ captureInteractiveVideoInput(preserveRuntimeState);
playInteractiveVideoFile(filename, videoOffset, videoStartFrame);
releaseInteractiveVideoInput();
_preserveInteractiveRuntimeState = false;
Commit: 69dc0313b574cd5d3e94a99009872e2ac35367b4
https://github.com/scummvm/scummvm/commit/69dc0313b574cd5d3e94a99009872e2ac35367b4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-07T07:23:52+02:00
Commit Message:
FREESCAPE: completed castle UI for C64
Changed paths:
engines/freescape/games/castle/c64.cpp
engines/freescape/games/castle/castle.cpp
engines/freescape/games/castle/castle.h
diff --git a/engines/freescape/games/castle/c64.cpp b/engines/freescape/games/castle/c64.cpp
index 7057c05b5b2..6ae46e4dca8 100644
--- a/engines/freescape/games/castle/c64.cpp
+++ b/engines/freescape/games/castle/c64.cpp
@@ -20,6 +20,7 @@
*/
#include "common/file.h"
+#include "common/memstream.h"
#include "graphics/managed_surface.h"
#include "freescape/freescape.h"
@@ -46,6 +47,93 @@ enum {
kCastleC64MessageY = 182
};
+// Match the colors of the bundled C64 border. The unused palette entries are
+// black; the HUD uses only these ten VIC colors.
+static const byte kCastleC64UIPalette[16][3] = {
+ {0, 0, 0}, {255, 255, 255}, {0, 0, 0}, {0, 0, 0},
+ {0, 0, 0}, {98, 213, 50}, {0, 0, 0}, {255, 255, 70},
+ {183, 99, 30}, {119, 83, 0}, {0, 0, 0}, {98, 98, 98},
+ {148, 148, 148}, {183, 255, 134}, {0, 0, 0}, {205, 205, 205}
+};
+
+static uint32 castleC64UIColor(const Graphics::PixelFormat &format, byte color) {
+ const byte *rgb = kCastleC64UIPalette[color];
+ return format.ARGBToColor(255, rgb[0], rgb[1], rgb[2]);
+}
+
+static Common::Array<byte> unpackCastleC64UI(Common::SeekableReadStream *file) {
+ // The startup relocates the packed stream from $0d50 to $2708, then
+ // expands it into $0200..$ffff. Decode only through the screen attributes
+ // at $c400..$c7e7: the later bitmap pages require the separate tape loader.
+ // Page flags at $09ff descend through memory, least significant bit first.
+ // A clear bit selects a page with its own escape byte and count/value runs.
+ Common::Array<byte> packed;
+ packed.resize(file->size());
+ file->seek(0);
+ if (packed.size() < 0x551 || file->read(packed.data(), packed.size()) != packed.size())
+ error("Unable to read Castle C64 UI data");
+
+ Common::Array<byte> data;
+ data.resize(0xc800);
+ uint32 source = 0x551; // $0d50, including the PRG load-address adjustment
+ int flagOffset = 0x200; // $09ff
+ byte flags = packed[flagOffset];
+ int bitsLeft = 6; // The first two pages are not part of the packed stream.
+ for (uint page = 2; page < 0xc8; page++) {
+ if (!bitsLeft) {
+ flags = packed[--flagOffset];
+ bitsLeft = 8;
+ }
+ bool raw = flags & 1;
+ flags >>= 1;
+ bitsLeft--;
+ uint end = (page + 1) * 256;
+ if (source >= packed.size())
+ error("Truncated Castle C64 UI page %x", page);
+ byte escape = raw ? 0 : packed[source++];
+ for (uint dest = page * 256; dest < end;) {
+ if (source >= packed.size())
+ error("Truncated Castle C64 UI page %x", page);
+ byte value = packed[source++];
+ uint count = 1;
+ if (!raw && value == escape) {
+ if (source + 2 > packed.size())
+ error("Truncated Castle C64 UI run");
+ count = packed[source++];
+ if (!count)
+ count = 256;
+ value = packed[source++];
+ }
+ if (count > end - dest)
+ error("Castle C64 UI run crosses a page boundary");
+ while (count--)
+ data[dest++] = value;
+ }
+ }
+ return data;
+}
+
+static void loadCastleC64Frame(const Common::Array<byte> &data, uint address, Graphics::ManagedSurface *surface, int frame = 0) {
+ // $7cf6 reads a five-byte header: byte width, height, final-byte mask,
+ // and frame size. These HUD frames all use the whole final byte.
+ if (address + 5 > data.size())
+ error("Missing Castle C64 UI frame header at %x", address);
+ uint width = data[address];
+ uint height = data[address + 1];
+ uint size = data[address + 3] | (data[address + 4] << 8);
+ uint pixels = address + 5 + frame * size;
+ if (!width || !height || data[address + 2] != 0xff || size != width * height || pixels + size > data.size())
+ error("Invalid Castle C64 UI frame at %x", address);
+ surface->create(width * 8, height, Graphics::PixelFormat::createFormatCLUT8());
+ for (uint y = 0; y < height; y++) {
+ for (uint x = 0; x < width * 8; x += 2) {
+ byte color = (data[pixels + y * width + x / 8] >> (6 - x % 8)) & 3;
+ surface->setPixel(x, y, color);
+ surface->setPixel(x + 1, y, color);
+ }
+ }
+}
+
struct CastleC64Repeat {
uint16 offset;
byte count;
@@ -72,105 +160,6 @@ const CastleC64Repeat kCastleC64DatabaseRepeats[] = {
{ 0x2255, 113, 0x00 }
};
-static const byte kCastleC64FontData[] = {
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x18, 0x00,
- 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x66, 0x66, 0xff, 0x66, 0xff, 0x66, 0x66, 0x00,
- 0x18, 0x3e, 0x58, 0x3c, 0x1a, 0x7c, 0x18, 0x00,
- 0x62, 0x66, 0x0c, 0x18, 0x30, 0x66, 0x46, 0x00,
- 0x3c, 0x66, 0x3c, 0x38, 0x67, 0x66, 0x3f, 0x00,
- 0x06, 0x0c, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x0c, 0x18, 0x30, 0x30, 0x30, 0x18, 0x0c, 0x00,
- 0x30, 0x18, 0x0c, 0x0c, 0x0c, 0x18, 0x30, 0x00,
- 0x00, 0x66, 0x3c, 0xff, 0x3c, 0x66, 0x00, 0x00,
- 0x00, 0x18, 0x18, 0x7e, 0x18, 0x18, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30,
- 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00,
- 0x00, 0x03, 0x06, 0x0c, 0x18, 0x30, 0x60, 0x00,
- 0x3c, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3c, 0x00,
- 0x18, 0x18, 0x38, 0x18, 0x18, 0x18, 0x7e, 0x00,
- 0x3c, 0x66, 0x06, 0x0c, 0x30, 0x60, 0x7e, 0x00,
- 0x3c, 0x66, 0x06, 0x1c, 0x06, 0x66, 0x3c, 0x00,
- 0x06, 0x0e, 0x1e, 0x66, 0x7f, 0x06, 0x06, 0x00,
- 0x7e, 0x60, 0x7c, 0x06, 0x06, 0x66, 0x3c, 0x00,
- 0x3c, 0x66, 0x60, 0x7c, 0x66, 0x66, 0x3c, 0x00,
- 0x7e, 0x66, 0x0c, 0x18, 0x18, 0x18, 0x18, 0x00,
- 0x3c, 0x66, 0x66, 0x3c, 0x66, 0x66, 0x3c, 0x00,
- 0x3c, 0x66, 0x66, 0x3e, 0x06, 0x66, 0x3c, 0x00,
- 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x00, 0x00,
- 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x18, 0x30,
- 0x0e, 0x18, 0x30, 0x60, 0x30, 0x18, 0x0e, 0x00,
- 0x00, 0x00, 0x7e, 0x00, 0x7e, 0x00, 0x00, 0x00,
- 0x70, 0x18, 0x0c, 0x06, 0x0c, 0x18, 0x70, 0x00,
- 0x3c, 0x66, 0x06, 0x0c, 0x18, 0x00, 0x18, 0x00,
- 0x3c, 0x66, 0x6e, 0x6e, 0x60, 0x62, 0x3c, 0x00,
- 0x18, 0x3c, 0x66, 0x7e, 0x66, 0x66, 0x66, 0x00,
- 0x7c, 0x66, 0x66, 0x7c, 0x66, 0x66, 0x7c, 0x00,
- 0x3c, 0x66, 0x60, 0x60, 0x60, 0x66, 0x3c, 0x00,
- 0x78, 0x6c, 0x66, 0x66, 0x66, 0x6c, 0x78, 0x00,
- 0x7e, 0x60, 0x60, 0x78, 0x60, 0x60, 0x7e, 0x00,
- 0x7e, 0x60, 0x60, 0x78, 0x60, 0x60, 0x60, 0x00,
- 0x3c, 0x66, 0x60, 0x6e, 0x66, 0x66, 0x3c, 0x00,
- 0x66, 0x66, 0x66, 0x7e, 0x66, 0x66, 0x66, 0x00,
- 0x3c, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3c, 0x00,
- 0x1e, 0x0c, 0x0c, 0x0c, 0x0c, 0x6c, 0x38, 0x00,
- 0x66, 0x6c, 0x78, 0x70, 0x78, 0x6c, 0x66, 0x00,
- 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x7e, 0x00,
- 0x63, 0x77, 0x7f, 0x6b, 0x63, 0x63, 0x63, 0x00,
- 0x66, 0x76, 0x7e, 0x7e, 0x6e, 0x66, 0x66, 0x00,
- 0x3c, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3c, 0x00,
- 0x7c, 0x66, 0x66, 0x7c, 0x60, 0x60, 0x60, 0x00,
- 0x3c, 0x66, 0x66, 0x66, 0x66, 0x3c, 0x0e, 0x00,
- 0x7c, 0x66, 0x66, 0x7c, 0x78, 0x6c, 0x66, 0x00,
- 0x3c, 0x66, 0x60, 0x3c, 0x06, 0x66, 0x3c, 0x00,
- 0x7e, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00,
- 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3c, 0x00,
- 0x66, 0x66, 0x66, 0x66, 0x66, 0x3c, 0x18, 0x00,
- 0x63, 0x63, 0x63, 0x6b, 0x7f, 0x77, 0x63, 0x00,
- 0x66, 0x66, 0x3c, 0x18, 0x3c, 0x66, 0x66, 0x00,
- 0x66, 0x66, 0x66, 0x3c, 0x18, 0x18, 0x18, 0x00,
- 0x7e, 0x06, 0x0c, 0x18, 0x30, 0x60, 0x7e, 0x00,
- 0x3c, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3c, 0x00,
- 0x00, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x03, 0x00,
- 0x3c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x3c, 0x00,
- 0x18, 0x3c, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00,
- 0x18, 0x0c, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x3c, 0x06, 0x3e, 0x66, 0x3e, 0x00,
- 0x00, 0x60, 0x60, 0x7c, 0x66, 0x66, 0x7c, 0x00,
- 0x00, 0x00, 0x3c, 0x60, 0x60, 0x60, 0x3c, 0x00,
- 0x00, 0x06, 0x06, 0x3e, 0x66, 0x66, 0x3e, 0x00,
- 0x00, 0x00, 0x3c, 0x66, 0x7e, 0x60, 0x3c, 0x00,
- 0x00, 0x0e, 0x18, 0x3e, 0x18, 0x18, 0x18, 0x00,
- 0x00, 0x00, 0x3e, 0x66, 0x66, 0x3e, 0x06, 0x7c,
- 0x00, 0x60, 0x60, 0x7c, 0x66, 0x66, 0x66, 0x00,
- 0x00, 0x18, 0x00, 0x38, 0x18, 0x18, 0x3c, 0x00,
- 0x00, 0x06, 0x00, 0x06, 0x06, 0x06, 0x06, 0x3c,
- 0x00, 0x60, 0x60, 0x6c, 0x78, 0x6c, 0x66, 0x00,
- 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x3c, 0x00,
- 0x00, 0x00, 0x66, 0x7f, 0x7f, 0x6b, 0x63, 0x00,
- 0x00, 0x00, 0x7c, 0x66, 0x66, 0x66, 0x66, 0x00,
- 0x00, 0x00, 0x3c, 0x66, 0x66, 0x66, 0x3c, 0x00,
- 0x00, 0x00, 0x7c, 0x66, 0x66, 0x7c, 0x60, 0x60,
- 0x00, 0x00, 0x3e, 0x66, 0x66, 0x3e, 0x06, 0x06,
- 0x00, 0x00, 0x7c, 0x66, 0x60, 0x60, 0x60, 0x00,
- 0x00, 0x00, 0x3e, 0x60, 0x3c, 0x06, 0x7c, 0x00,
- 0x00, 0x18, 0x7e, 0x18, 0x18, 0x18, 0x0e, 0x00,
- 0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x3e, 0x00,
- 0x00, 0x00, 0x66, 0x66, 0x66, 0x3c, 0x18, 0x00,
- 0x00, 0x00, 0x63, 0x6b, 0x7f, 0x3e, 0x36, 0x00,
- 0x00, 0x00, 0x66, 0x3c, 0x18, 0x3c, 0x66, 0x00,
- 0x00, 0x00, 0x66, 0x66, 0x66, 0x3e, 0x0c, 0x78,
- 0x00, 0x00, 0x7e, 0x0c, 0x18, 0x30, 0x7e, 0x00,
- 0x0e, 0x18, 0x18, 0x70, 0x18, 0x18, 0x0e, 0x00,
- 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00,
- 0x70, 0x18, 0x18, 0x0e, 0x18, 0x18, 0x70, 0x00,
- 0x31, 0x6b, 0x46, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
-};
-
uint16 readCastleC64Uint16LE(const Common::Array<byte> &data, uint32 offset) {
if (offset + 1 >= data.size())
error("Castle C64 database pointer read out of range at 0x%x", offset);
@@ -232,25 +221,30 @@ Common::Array<byte> normalizeCastleC64Database(Common::SeekableReadStream *file)
return decoded;
}
-Common::Array<Graphics::ManagedSurface *> loadCastleC64Font() {
+static Common::Array<Graphics::ManagedSurface *> loadCastleC64Font(const Common::Array<byte> &data) {
Common::Array<Graphics::ManagedSurface *> chars;
- for (uint chr = 0; chr < ARRAYSIZE(kCastleC64FontData) / 8; chr++) {
+ // $85eb expands four packed bytes per character into eight rows of
+ // double-width pixels. Row 1 uses color RAM for the highlight.
+ for (uint chr = 32; chr < 128; chr++) {
Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
- surface->create(8, 8, Graphics::PixelFormat::createFormatCLUT8());
+ surface->create(10, 8, Graphics::PixelFormat::createFormatCLUT8());
surface->clear(0);
-
- for (int y = 0; y < 8; y++) {
- byte row = kCastleC64FontData[chr * 8 + y];
- for (int x = 0; x < 8; x++) {
- if (row & (0x80 >> x))
- surface->setPixel(x, y, 1);
+ uint glyph = (chr >= 'a' && chr <= 'z') ? chr - 'a' + 'A' : chr;
+ if (glyph <= 'Z') {
+ for (int y = 0; y < 8; y++) {
+ byte row = data[0x231a + (glyph - 32) * 4 + y / 2];
+ row = (y & 1) ? row & 15 : row >> 4;
+ for (int x = 0; x < 4; x++) {
+ if (row & (8 >> x)) {
+ surface->setPixel(x * 2, y, y == 1 ? 2 : 1);
+ surface->setPixel(x * 2 + 1, y, y == 1 ? 2 : 1);
+ }
+ }
}
}
-
chars.push_back(surface);
}
-
return chars;
}
@@ -359,8 +353,6 @@ void CastleEngine::initC64() {
_viewArea = Common::Rect(40, 32, 280, 152);
}
-extern byte kC64Palette[16][3];
-
void CastleEngine::loadMessagesC64(Common::SeekableReadStream *file, int offset, int number) {
file->seek(offset);
debugC(1, kFreescapeDebugParser, "String table:");
@@ -389,32 +381,37 @@ void CastleEngine::loadRiddlesC64(Common::SeekableReadStream *file, int offset,
for (int i = 0; i < number; i++) {
Riddle riddle;
- riddle._origin = Common::Point(40, 33);
+ riddle._origin = Common::Point(40, 32);
+ int previousInset = 0;
int numberLines = file->readByte();
debugC(1, kFreescapeDebugParser, "c64 riddle %d number of lines: %d", i, numberLines);
for (int j = 0; j < numberLines; j++) {
- int8 x = (int8)file->readByte();
+ int8 x = 2 * (int8)file->readByte(); // C64 horizontal offsets count pixel pairs.
int8 y = (int8)file->readByte();
int size = file->readByte();
- if (size == 0xff)
+ // $6dd6 applies both deltas to every record, including the rows
+ // of asterisks. Discarding those rows also loses the text origin.
+ if (size == 0xff) {
+ riddle._lines.push_back(RiddleText(x + 6 - previousInset, y, "********************"));
+ previousInset = 6;
continue;
+ }
- file->readByte(); // color/control byte
+ int inset = file->readByte() ? 6 : 0;
+ if (inset)
+ size--;
+ if (size < 0 || file->pos() + size > file->size())
+ error("Truncated Castle C64 riddle %d", i);
Common::String message;
- int chars = 0;
- while (chars < size) {
- byte c = file->readByte();
- if (c <= 1 || c < 0x20 || c > 0xf0)
- continue;
- message += c;
- chars++;
- }
+ for (int chars = 0; chars < size; chars++)
+ message += file->readByte();
debugC(1, kFreescapeDebugParser, "'%s' with offset: %d, %d", message.c_str(), x, y);
- riddle._lines.push_back(RiddleText(x, y, message));
+ riddle._lines.push_back(RiddleText(x + inset - previousInset, y, message));
+ previousInset = inset;
}
_riddleList.push_back(riddle);
@@ -430,16 +427,87 @@ void CastleEngine::loadAssetsC64FullGame() {
if (!file.isOpen())
error("Failed to open castlemaster.c64.data");
- // The original tape loader preloads display support into high RAM before
- // this main program image starts; castlemaster.c64.data has no standalone
- // font block like the other C64 Freescape games.
- Common::Array<Graphics::ManagedSurface *> chars = loadCastleC64Font();
+ Common::Array<byte> uiData = unpackCastleC64UI(&file);
+ Common::MemoryReadStream uiStream(uiData.data(), uiData.size());
+ Common::Array<Graphics::ManagedSurface *> chars = loadCastleC64Font(uiData);
_font = Font(chars);
- _font.setCharWidth(8);
+ _font.setCharWidth(10);
+ _font.setSecondaryColor(castleC64UIColor(_gfx->_texturePixelFormat, 15));
_fontLoaded = true;
- loadMessagesC64(&file, 0x13a9, 75);
- loadRiddlesC64(&file, 0x1823, 9);
+ loadMessagesC64(&uiStream, 0x1401, 75);
+ loadRiddlesC64(&uiStream, 0x18ae, 9);
+
+ // Preserve multicolor pixel indices until drawing. VIC colors depend on
+ // the destination 8x8 cell, even within a single moving weight or key.
+ file.seek(0x301); // Packed color RAM at $0b00, high nibble first.
+ _c64UIColors.resize(1000 * 4);
+ for (int cell = 0; cell < 1000; cell += 2) {
+ byte colorRAM = file.readByte();
+ for (int i = 0; i < 2; i++) {
+ byte screen = uiData[0xc400 + cell + i];
+ byte colors[4] = {0, byte(screen >> 4), byte(screen & 15), byte(i ? colorRAM & 15 : colorRAM >> 4)};
+ for (int color = 0; color < 4; color++)
+ _c64UIColors[(cell + i) * 4 + color] = castleC64UIColor(_gfx->_texturePixelFormat, colors[color]);
+ }
+ }
+
+ loadCastleC64Frame(uiData, 0x1dd9, &_c64KeysBackground);
+ _keysBorderFrames.push_back(new Graphics::ManagedSurface());
+ loadCastleC64Frame(uiData, 0x1e32, _keysBorderFrames[0]);
+ _spiritsMeterIndicatorBackgroundFrame = new Graphics::ManagedSurface();
+ loadCastleC64Frame(uiData, 0x1e45, _spiritsMeterIndicatorBackgroundFrame);
+ _spiritsMeterIndicatorFrame = new Graphics::ManagedSurface();
+ loadCastleC64Frame(uiData, 0x1e8a, _spiritsMeterIndicatorFrame);
+ _strenghtBackgroundFrame = new Graphics::ManagedSurface();
+ loadCastleC64Frame(uiData, 0x1e9f, _strenghtBackgroundFrame);
+ _strenghtBarFrame = new Graphics::ManagedSurface();
+ loadCastleC64Frame(uiData, 0x1f49, _strenghtBarFrame);
+ for (int frame = 0; frame < 4; frame++) {
+ _strenghtWeightsFrames.push_back(new Graphics::ManagedSurface());
+ loadCastleC64Frame(uiData, 0x1f6f, _strenghtWeightsFrames[frame], frame);
+ }
+
+ // $6ee7 stretches seven three-byte rows across the riddle board. The
+ // bottom reverses the top six rows; row six fills the middle of the board.
+ Graphics::ManagedSurface *riddleFrames[3];
+ const byte riddleColors[4] = {0, 9, 7, 9};
+ for (int frame = 0; frame < 3; frame++) {
+ riddleFrames[frame] = new Graphics::ManagedSurface();
+ int height = frame == 1 ? 1 : 6;
+ riddleFrames[frame]->create(240, height, _gfx->_texturePixelFormat);
+ for (int y = 0; y < height; y++) {
+ int row = frame == 0 ? y : (frame == 1 ? 6 : 5 - y);
+ for (int x = 0; x < 240; x += 2) {
+ int column = x < 8 ? 0 : (x >= 232 ? 2 : 1);
+ byte pixels = uiData[0x2023 + row * 3 + column];
+ byte color = riddleColors[(pixels >> (6 - x % 8)) & 3];
+ uint32 pixel = castleC64UIColor(_gfx->_texturePixelFormat, color);
+ riddleFrames[frame]->setPixel(x, y, pixel);
+ riddleFrames[frame]->setPixel(x + 1, y, pixel);
+ }
+ }
+ }
+ _riddleTopFrame = riddleFrames[0];
+ _riddleBackgroundFrame = riddleFrames[1];
+ _riddleBottomFrame = riddleFrames[2];
+
+ // The IRQ at $74be advances the flag every eight PAL ticks. Its frames
+ // are six bitmap cells in C64 cell order, copied to $e128 and $e268.
+ for (int frame = 0; frame < 4; frame++) {
+ Graphics::ManagedSurface *flag = new Graphics::ManagedSurface();
+ flag->create(24, 16, Graphics::PixelFormat::createFormatCLUT8());
+ for (int y = 0; y < 16; y++) {
+ for (int x = 0; x < 24; x += 2) {
+ byte pixels = uiData[0x2038 + frame * 48 + (y / 8) * 24 + (x / 8) * 8 + y % 8];
+ byte color = (pixels >> (6 - x % 8)) & 3;
+ flag->setPixel(x, y, color);
+ flag->setPixel(x + 1, y, color);
+ }
+ }
+ _flagFrames.push_back(flag);
+ }
+
Common::Array<byte> database = normalizeCastleC64Database(&file);
CastleC64DatabaseReadStream databaseStream(database);
load8bitBinary(&databaseStream, 0, 16);
@@ -485,12 +553,82 @@ void CastleEngine::loadAssetsC64FullGame() {
// TODO: title screen is in BASIC loader (file 009) - not yet extracted
}
-void CastleEngine::drawC64UI(Graphics::Surface *surface) {
- uint32 front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x62, 0xD5, 0x32);
+void CastleEngine::drawC64HudSurface(Graphics::Surface *surface, const Graphics::Surface &frame, const Common::Point &origin) {
+ for (int y = 0; y < frame.h; y++) {
+ const byte *src = (const byte *)frame.getBasePtr(0, y);
+ for (int x = 0; x < frame.w; x++) {
+ int cell = ((origin.y + y) / 8) * 40 + (origin.x + x) / 8;
+ surface->setPixel(origin.x + x, origin.y + y, _c64UIColors[cell * 4 + src[x]]);
+ }
+ }
+}
+
+void CastleEngine::drawC64InfoMenu(Graphics::Surface *surface) {
+ uint32 front = castleC64UIColor(surface->format, 9);
+ uint32 highlight = castleC64UIColor(surface->format, 15);
+ uint32 back = castleC64UIColor(surface->format, 0);
+ surface->fillRect(_viewArea, back);
+ Common::String keys = _messagesList[72];
+ Common::String spirits = _messagesList[73];
+ Common::String score = _messagesList[74];
+ Common::replace(keys, "XX", Common::String::format("%2d", MIN<uint>(_keysCollected.size(), 10)));
+ Common::replace(spirits, "XX", Common::String::format("%2d", _gameStateVars[k8bitVariableSpiritsDestroyed]));
+ Common::replace(score, "XXXXXXX", Common::String::format("%07d", _gameStateVars[k8bitVariableScore]));
+
+ // The original menu at $7808 uses these rows within the 3D viewport.
+ drawStringInSurface("********************", 60, 46, front, highlight, back, surface);
+ drawStringInSurface(_messagesList[68], 50, 61, front, highlight, back, surface);
+ drawStringInSurface(_messagesList[69], 50, 82, front, highlight, back, surface);
+ drawStringInSurface(keys, 130, 82, front, highlight, back, surface);
+ drawStringInSurface(_messagesList[70], 50, 93, front, highlight, back, surface);
+ drawStringInSurface(spirits, 130, 93, front, highlight, back, surface);
+ drawStringInSurface(_messagesList[71], 50, 104, front, highlight, back, surface);
+ int strength = CLIP<int>(_gameStateVars[k8bitVariableShield], 1, 24);
+ drawStringInSurface(_messagesList[62 + (strength - 1) / 4], 150, 104, front, highlight, back, surface);
+ drawStringInSurface(score, 80, 115, front, highlight, back, surface);
+ drawStringInSurface("********************", 60, 133, front, highlight, back, surface);
+}
- uint8 r, g, b;
- _gfx->readFromPalette(0, r, g, b);
- uint32 back = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
+void CastleEngine::drawC64UI(Graphics::Surface *surface) {
+ uint32 front = castleC64UIColor(surface->format, 5);
+ uint32 back = castleC64UIColor(surface->format, 0);
+ _font.setSecondaryColor(castleC64UIColor(surface->format, 15));
+
+ // $702c rebuilds the key rack on a full redraw, including after loading.
+ // Composing it afresh also removes keys when restarting or loading a save.
+ Graphics::ManagedSurface buffer(96, 19, Graphics::PixelFormat::createFormatCLUT8());
+ buffer.copyRectToSurface(_c64KeysBackground, 0, 0, Common::Rect(48, 14));
+ for (uint key = 0; key < MIN<uint>(_keysCollected.size(), 10); key++)
+ buffer.copyRectToSurfaceWithKey(*_keysBorderFrames[0], 42 - 4 * key, 0, Common::Rect(8, 14), 0);
+ drawC64HudSurface(surface, buffer.getSubArea(Common::Rect(48, 14)), Common::Point(48, 179));
+
+ // $7171 draws paired discs from the outside inward, four pixels apart.
+ // A partial disc precedes the full discs; strength below four lowers the
+ // bar and both discs. Only the original 88x15 window is copied to the HUD.
+ buffer.copyRectToSurface(*_strenghtBackgroundFrame, 0, 0, Common::Rect(88, 15));
+ int strength = CLIP<int>(_gameStateVars[k8bitVariableShield], 0, 24);
+ int drop = MAX(0, 4 - strength);
+ buffer.copyRectToSurface(*_strenghtBarFrame, 6, 6 + drop, Common::Rect(88, 3));
+ int pairs = (strength + 3) / 4;
+ for (int pair = 0; pair < pairs; pair++) {
+ int frame = (pair == 0 && strength % 4) ? 4 - strength % 4 : 0;
+ buffer.copyRectToSurfaceWithKey(*_strenghtWeightsFrames[frame], 8 + pair * 4, drop, Common::Rect(8, 15), 0);
+ buffer.copyRectToSurfaceWithKey(*_strenghtWeightsFrames[frame], 80 - pair * 4, drop, Common::Rect(8, 15), 0);
+ }
+ drawC64HudSurface(surface, buffer.getSubArea(Common::Rect(88, 15)), Common::Point(40, 158));
+
+ // $726a rounds the spirit position up to a pixel pair, then clips the
+ // moving face to the middle eight bitmap cells. Derive it from saved
+ // state here so a loaded game does not display the previous position.
+ int spiritsDestroyed = CLIP<int>(_gameStateVars[k8bitVariableSpiritsDestroyed], 0, _spiritsToKill);
+ int position = CLIP<int>(_spiritsMeter * (_spiritsToKill - spiritsDestroyed) / _spiritsToKill, 0, 64);
+ buffer.copyRectToSurface(*_spiritsMeterIndicatorBackgroundFrame, 8, 0, Common::Rect(64, 8));
+ buffer.copyRectToSurfaceWithKey(*_spiritsMeterIndicatorFrame, (position + 1) & ~1, 0, Common::Rect(16, 8), 0);
+ drawC64HudSurface(surface, buffer.getSubArea(Common::Rect(8, 0, 72, 8)), Common::Point(160, 161));
+
+ int flagFrame = (g_system->getMillis() / 160) % 4;
+ drawC64HudSurface(surface, *_flagFrames[flagFrame], Common::Point(296, 0));
+ // TODO: animate the eye indicator using the frames at $1fb0.
// The original loader leaves "CASTLE MASTER" in the bottom message strip.
// Clear the whole writable part of that strip before drawing runtime text.
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 3b66a2de526..d8df1748037 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -1164,6 +1164,8 @@ void CastleEngine::drawInfoMenu() {
surface->copyRectToSurfaceWithKey((const Graphics::Surface)*sndIndicator, 96, 103,
Common::Rect(0, 0, sndIndicator->w, sndIndicator->h), black);
}
+ } else if (isC64()) {
+ drawC64InfoMenu(surface);
} else if (isSpectrum() || isCPC()) {
Common::Array<Common::String> lines;
lines.push_back(centerAndPadString("********************", 21));
@@ -1252,6 +1254,10 @@ void CastleEngine::drawInfoMenu() {
loadGameDialog();
_eventManager->purgeMouseEvents();
+ if (isC64()) {
+ drawC64InfoMenu(surface);
+ menuTexture->update(surface);
+ }
if (isDOS() || isAmiga() || isAtariST()) {
g_system->lockMouse(false);
CursorMan.showMouse(true);
@@ -1295,7 +1301,7 @@ void CastleEngine::drawInfoMenu() {
case Common::EVENT_RBUTTONDOWN:
// fallthrough
case Common::EVENT_LBUTTONDOWN:
- if (isSpectrum() || isCPC())
+ if (isSpectrum() || isCPC() || isC64())
break;
mousePos = getNormalizedPosition(event.mouse);
@@ -1470,7 +1476,10 @@ void CastleEngine::drawFullscreenGameOverAndWait() {
Common::String keysCollectedString;
if (isDOS())
keysCollectedString = _messagesList[130];
- else if (isSpectrum()) {
+ else if (isC64()) {
+ keysCollectedString = _messagesList[72];
+ Common::replace(keysCollectedString, "XX", "X");
+ } else if (isSpectrum()) {
if (_language == Common::EN_ANY)
keysCollectedString = "X COLLECTED";
else if (_language == Common::ES_ESP)
@@ -1488,6 +1497,8 @@ void CastleEngine::drawFullscreenGameOverAndWait() {
Common::String scoreString;
if (isDOS())
scoreString = _messagesList[131];
+ else if (isC64())
+ scoreString = _messagesList[74];
else if (isSpectrum() || isCPC()) {
if (_language == Common::EN_ANY)
scoreString = "SCORE XXXXXXX";
@@ -1503,7 +1514,10 @@ void CastleEngine::drawFullscreenGameOverAndWait() {
Common::String spiritsDestroyedString;
if (isDOS())
spiritsDestroyedString = _messagesList[133];
- else if (isSpectrum() || isCPC()) {
+ else if (isC64()) {
+ spiritsDestroyedString = _messagesList[73];
+ Common::replace(spiritsDestroyedString, "XX", "X");
+ } else if (isSpectrum() || isCPC()) {
if (_language == Common::EN_ANY)
spiritsDestroyedString = "X DESTROYED";
else if (_language == Common::ES_ESP)
@@ -1823,6 +1837,8 @@ void CastleEngine::drawFullscreenRiddleAndWait(uint16 riddle) {
uint32 front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
if (isAmiga())
front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0xEE, 0xAA, 0x00);
+ else if (isC64())
+ front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 119, 83, 0);
uint32 transparent = _gfx->_texturePixelFormat.ARGBToColor(0x00, 0x00, 0x00, 0x00);
Graphics::Surface *surface = new Graphics::Surface();
@@ -1887,6 +1903,10 @@ void CastleEngine::drawRiddle(uint16 riddle, uint32 front, uint32 back, Graphics
x = 40;
y = 46;
maxWidth = 139;
+ } else if (isC64()) {
+ x = 40;
+ y = 45;
+ maxWidth = 137;
} else if (isSpectrum()) {
x = 64;
y = 37;
@@ -1929,8 +1949,9 @@ void CastleEngine::drawRiddle(uint16 riddle, uint32 front, uint32 back, Graphics
}
}
if (_riddleBottomFrame) {
- Common::Rect srcRect(0, 0, _riddleBottomFrame->w, _riddleBottomFrame->h - 1);
- Common::Rect destRect(x, maxWidth, x + _riddleBottomFrame->w, maxWidth + _riddleBottomFrame->h - 1);
+ int height = _riddleBottomFrame->h - (isC64() ? 0 : 1);
+ Common::Rect srcRect(0, 0, _riddleBottomFrame->w, height);
+ Common::Rect destRect(x, maxWidth, x + _riddleBottomFrame->w, maxWidth + height);
destRect.clip(_viewArea);
srcRect = Common::Rect(destRect.left - x, destRect.top - maxWidth, destRect.right - x, destRect.bottom - maxWidth);
if (srcRect.isValidRect() && !srcRect.isEmpty())
@@ -1974,6 +1995,8 @@ void CastleEngine::drawRiddleStringInSurface(const Common::String &str, int x, i
_fontRiddle.drawString(surface, ustr, x, y, _screenW, fontColor);
} else {
_font.setBackground(backColor);
+ if (isC64())
+ _font.setSecondaryColor(fontColor);
_font.drawString(surface, ustr, x, y, _screenW, fontColor);
}
}
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index dedc4dc5bb6..ed17e58bf09 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -105,6 +105,10 @@ public:
void loadAssetsC64FullGame() override;
void drawC64UI(Graphics::Surface *surface) override;
+ void drawC64InfoMenu(Graphics::Surface *surface);
+ void drawC64HudSurface(Graphics::Surface *surface, const Graphics::Surface &frame, const Common::Point &origin);
+ Graphics::ManagedSurface _c64KeysBackground;
+ Common::Array<uint32> _c64UIColors;
void drawDOSUI(Graphics::Surface *surface) override;
void drawZXUI(Graphics::Surface *surface) override;
Commit: 7ca4207a12c18ebbea33f74e3a4e67dd0ec31e52
https://github.com/scummvm/scummvm/commit/7ca4207a12c18ebbea33f74e3a4e67dd0ec31e52
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-07T07:23:52+02:00
Commit Message:
FREESCAPE: added gate parsing/rendering for castle c64
Changed paths:
engines/freescape/games/castle/c64.cpp
engines/freescape/games/castle/castle.cpp
engines/freescape/games/castle/castle.h
diff --git a/engines/freescape/games/castle/c64.cpp b/engines/freescape/games/castle/c64.cpp
index 6ae46e4dca8..06484de979a 100644
--- a/engines/freescape/games/castle/c64.cpp
+++ b/engines/freescape/games/castle/c64.cpp
@@ -44,7 +44,10 @@ enum {
kCastleC64MessageLeft = 118,
kCastleC64MessageRight = 280,
kCastleC64MessageX = 120,
- kCastleC64MessageY = 182
+ kCastleC64MessageY = 182,
+ kCastleC64GateFrameTicks = 2,
+ kCastleC64GateLiftStep = 2,
+ kCastleC64GateTransparent = 4
};
// Match the colors of the bundled C64 border. The unused palette entries are
@@ -351,6 +354,7 @@ private:
void CastleEngine::initC64() {
_viewArea = Common::Rect(40, 32, 280, 152);
+ _c64LiftingGateStartTicks = -1;
}
void CastleEngine::loadMessagesC64(Common::SeekableReadStream *file, int offset, int number) {
@@ -468,6 +472,36 @@ void CastleEngine::loadAssetsC64FullGame() {
loadCastleC64Frame(uiData, 0x1f6f, _strenghtWeightsFrames[frame], frame);
}
+ // $8367 draws ten 24-pixel columns. Four-row crossbars repeat every
+ // 24 rows, with fifteen rows of vertical bars below the lowest crossbar.
+ // Crossbars overwrite all pixels, including pen 0; between them only
+ // the leftmost and rightmost pixel pairs cover the scene ($84a4).
+ _c64Gate.create(240, 120, Graphics::PixelFormat::createFormatCLUT8());
+ _c64Gate.fillRect(Common::Rect(240, 120), kCastleC64GateTransparent);
+ for (int y = 0; y < _c64Gate.h; y++) {
+ int fromBottom = _c64Gate.h - 1 - y;
+ if (fromBottom < 15 || (fromBottom - 15) % 24 >= 4) {
+ for (int x = 0; x < _c64Gate.w; x += 24) {
+ _c64Gate.fillRect(Common::Rect(x, y, x + 2, y + 1), 1);
+ _c64Gate.fillRect(Common::Rect(x + 22, y, x + 24, y + 1), 3);
+ }
+ continue;
+ }
+ int row = 3 - (fromBottom - 15) % 24;
+ for (int x = 0; x < _c64Gate.w; x += 2) {
+ byte pixels = uiData[0x82d7 + row * 3 + (x / 8) % 3];
+ byte color = (pixels >> (6 - x % 8)) & 3;
+ _c64Gate.setPixel(x, y, color);
+ _c64Gate.setPixel(x + 1, y, color);
+ }
+ }
+
+ // $82e3 uses twenty heights, followed by $ff, to accelerate the fall
+ // and bounce twice after landing. Each step waits for two timer ticks.
+ _c64GateDropHeights.clear();
+ for (int frame = 0; frame < 20; frame++)
+ _c64GateDropHeights.push_back(uiData[0x82c2 + frame]);
+
// $6ee7 stretches seven three-byte rows across the riddle board. The
// bottom reverses the top six rows; row six fills the middle of the board.
Graphics::ManagedSurface *riddleFrames[3];
@@ -589,7 +623,56 @@ void CastleEngine::drawC64InfoMenu(Graphics::Surface *surface) {
drawStringInSurface("********************", 60, 133, front, highlight, back, surface);
}
+void CastleEngine::liftC64Gate() {
+ // $8347 raises the gate by two rows per step. Start the clock after the
+ // initial area is ready so its setup does not consume animation time.
+ _c64LiftingGateStartTicks = _ticks;
+ waitInLoop(_c64Gate.h / kCastleC64GateLiftStep * kCastleC64GateFrameTicks);
+ _c64LiftingGateStartTicks = -1;
+}
+
+void CastleEngine::dropC64Gate() {
+ // $49bb completes the fall before polling for a restart. The wait loop
+ // consumes pending gameplay input while still allowing the user to quit.
+ _droppingGateStartTicks = _ticks;
+ waitInLoop(_c64GateDropHeights.size() * kCastleC64GateFrameTicks);
+}
+
+void CastleEngine::drawC64Gate(Graphics::Surface *surface) {
+ int height;
+ if ((_gameStateControl == kFreescapeGameStateStart || _gameStateControl == kFreescapeGameStateRestart) && _c64LiftingGateStartTicks >= 0) {
+ int ticks = MAX(0, _ticks - _c64LiftingGateStartTicks);
+ height = MAX(0, _c64Gate.h - (ticks / kCastleC64GateFrameTicks) * kCastleC64GateLiftStep);
+ } else if (_gameStateControl == kFreescapeGameStateEnd && _droppingGateStartTicks >= 0 && !hasEscaped()) {
+ int ticks = MAX(0, _ticks - _droppingGateStartTicks);
+ int frame = MIN<int>(ticks / kCastleC64GateFrameTicks, _c64GateDropHeights.size() - 1);
+ height = _c64GateDropHeights[frame];
+ } else {
+ return;
+ }
+ if (!height)
+ return;
+
+ // The gate is drawn into the viewport bitmap and uses its current VIC
+ // colors, unlike the HUD frames whose colors come from the static border.
+ uint32 colors[4];
+ for (int color = 0; color < 4; color++) {
+ uint8 r, g, b;
+ _gfx->selectColorFromFourColorPalette(color, r, g, b);
+ colors[color] = surface->format.ARGBToColor(255, r, g, b);
+ }
+ for (int y = 0; y < height; y++) {
+ const byte *src = (const byte *)_c64Gate.getBasePtr(0, _c64Gate.h - height + y);
+ for (int x = 0; x < _c64Gate.w; x++) {
+ if (src[x] != kCastleC64GateTransparent)
+ surface->setPixel(_viewArea.left + x, _viewArea.top + y, colors[src[x]]);
+ }
+ }
+}
+
void CastleEngine::drawC64UI(Graphics::Surface *surface) {
+ drawC64Gate(surface);
+
uint32 front = castleC64UIColor(surface->format, 5);
uint32 back = castleC64UIColor(surface->format, 0);
_font.setSecondaryColor(castleC64UIColor(surface->format, 15));
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index d8df1748037..9747f75b330 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -610,6 +610,8 @@ void CastleEngine::beforeStarting() {
waitInLoop(250);
else if (isSpectrum() || isCPC())
waitInLoop(100);
+ else if (isC64())
+ liftC64Gate();
else if (isAmiga() || isAtariST())
waitInLoop(250);
}
@@ -799,7 +801,7 @@ void CastleEngine::initGameState() {
_lastMinute = minutes;
_lastTenSeconds = seconds / 10;
- _droppingGateStartTicks = 0;
+ _droppingGateStartTicks = isC64() ? -1 : 0;
_thunderFrameDuration = 0;
if (_playerMusic)
@@ -1534,6 +1536,8 @@ void CastleEngine::drawFullscreenGameOverAndWait() {
// TODO: playSound(X, false);
} else if (isSpectrum() || isCPC()) {
playSound(9, false);
+ } else if (isC64() && !hasEscaped()) {
+ dropC64Gate();
}
if (!isDOS() && hasEscaped()) {
@@ -2592,6 +2596,11 @@ Common::Error CastleEngine::saveGameStreamExtended(Common::WriteStream *stream,
}
Common::Error CastleEngine::loadGameStreamExtended(Common::SeekableReadStream *stream) {
+ if (isC64()) {
+ _c64LiftingGateStartTicks = -1;
+ _droppingGateStartTicks = -1;
+ }
+
_keysCollected.clear();
int numberKeys = stream->readUint32LE();
for (int i = 0; i < numberKeys; i++) {
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index ed17e58bf09..99525ffcac3 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -107,8 +107,14 @@ public:
void drawC64UI(Graphics::Surface *surface) override;
void drawC64InfoMenu(Graphics::Surface *surface);
void drawC64HudSurface(Graphics::Surface *surface, const Graphics::Surface &frame, const Common::Point &origin);
+ void liftC64Gate();
+ void dropC64Gate();
+ void drawC64Gate(Graphics::Surface *surface);
Graphics::ManagedSurface _c64KeysBackground;
+ Graphics::ManagedSurface _c64Gate;
Common::Array<uint32> _c64UIColors;
+ Common::Array<byte> _c64GateDropHeights;
+ int _c64LiftingGateStartTicks;
void drawDOSUI(Graphics::Surface *surface) override;
void drawZXUI(Graphics::Surface *surface) override;
Commit: 217eb9e715a42834f68f247a60508cf35c0e7821
https://github.com/scummvm/scummvm/commit/217eb9e715a42834f68f247a60508cf35c0e7821
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-07T07:23:52+02:00
Commit Message:
FREESCAPE: refactored music and added sound effects for castle c64
Changed paths:
engines/freescape/games/castle/c64.cpp
engines/freescape/games/castle/c64.music.cpp
engines/freescape/games/castle/c64.music.h
engines/freescape/games/castle/castle.cpp
engines/freescape/games/castle/castle.h
diff --git a/engines/freescape/games/castle/c64.cpp b/engines/freescape/games/castle/c64.cpp
index 06484de979a..383ccb26a00 100644
--- a/engines/freescape/games/castle/c64.cpp
+++ b/engines/freescape/games/castle/c64.cpp
@@ -355,6 +355,18 @@ private:
void CastleEngine::initC64() {
_viewArea = Common::Rect(40, 32, 280, 152);
_c64LiftingGateStartTicks = -1;
+ _c64MusicEnabled = true;
+
+ // C64 call sites: throw $63b3, climb/drop $53cf/$549f, area change
+ // $6cde, and a damaging landing $8142. The gate supplies the start sound.
+ _soundIndexShoot = 5;
+ _soundIndexCollide = 3;
+ _soundIndexStepUp = 12;
+ _soundIndexStepDown = 12;
+ _soundIndexMenu = 3;
+ _soundIndexFallen = 8;
+ _soundIndexStart = -1;
+ _soundIndexAreaChange = 7;
}
void CastleEngine::loadMessagesC64(Common::SeekableReadStream *file, int offset, int number) {
@@ -582,7 +594,8 @@ void CastleEngine::loadAssetsC64FullGame() {
surf->free();
delete surf;
- _playerMusic = new CastleC64MusicPlayer();
+ _sound = createCastleC64Sound(_mixer, uiData);
+ _playerMusic = new CastleC64MusicPlayer(_mixer);
// TODO: title screen is in BASIC loader (file 009) - not yet extracted
}
@@ -612,6 +625,7 @@ void CastleEngine::drawC64InfoMenu(Graphics::Surface *surface) {
// The original menu at $7808 uses these rows within the 3D viewport.
drawStringInSurface("********************", 60, 46, front, highlight, back, surface);
drawStringInSurface(_messagesList[68], 50, 61, front, highlight, back, surface);
+ drawStringInSurface(_c64MusicEnabled ? "F-AUDIO: MUSIC" : "F-AUDIO: EFFECTS", 50, 72, front, highlight, back, surface);
drawStringInSurface(_messagesList[69], 50, 82, front, highlight, back, surface);
drawStringInSurface(keys, 130, 82, front, highlight, back, surface);
drawStringInSurface(_messagesList[70], 50, 93, front, highlight, back, surface);
@@ -623,11 +637,40 @@ void CastleEngine::drawC64InfoMenu(Graphics::Surface *surface) {
drawStringInSurface("********************", 60, 133, front, highlight, back, surface);
}
+void CastleEngine::toggleC64AudioMode() {
+ _c64MusicEnabled = !_c64MusicEnabled;
+ _syncSound = false;
+ // $79bf switches between music and effects. Stop and release the old
+ // SID first, since both modes use all three voices of the same chip.
+ if (_c64MusicEnabled) {
+ enableCastleC64Sound(_sound, false);
+ if (_playerMusic)
+ _playerMusic->startMusic();
+ } else {
+ if (_playerMusic)
+ _playerMusic->stopMusic();
+ enableCastleC64Sound(_sound, true);
+ if (_sound)
+ _sound->playSound(3, Sound::kTypeNormal);
+ }
+}
+
void CastleEngine::liftC64Gate() {
// $8347 raises the gate by two rows per step. Start the clock after the
// initial area is ready so its setup does not consume animation time.
_c64LiftingGateStartTicks = _ticks;
- waitInLoop(_c64Gate.h / kCastleC64GateLiftStep * kCastleC64GateFrameTicks);
+ for (int step = 0; step < _c64Gate.h / kCastleC64GateLiftStep && !shouldQuit(); step++) {
+ int remaining = _c64LiftingGateStartTicks + (step + 1) * kCastleC64GateFrameTicks - _ticks;
+ if (remaining <= 0)
+ continue;
+ // $834b retriggers the rattle on every step, then $8363 plays the
+ // impact. Gate sounds interrupt effects without waiting for them.
+ if (_sound)
+ _sound->playSound(3, Sound::kTypeNormal);
+ waitInLoop(remaining - 1);
+ }
+ if (_sound && !shouldQuit())
+ _sound->playSound(2, Sound::kTypeNormal);
_c64LiftingGateStartTicks = -1;
}
@@ -635,7 +678,15 @@ void CastleEngine::dropC64Gate() {
// $49bb completes the fall before polling for a restart. The wait loop
// consumes pending gameplay input while still allowing the user to quit.
_droppingGateStartTicks = _ticks;
- waitInLoop(_c64GateDropHeights.size() * kCastleC64GateFrameTicks);
+ for (uint frame = 0; frame < _c64GateDropHeights.size() && !shouldQuit(); frame++) {
+ int remaining = _droppingGateStartTicks + (frame + 1) * kCastleC64GateFrameTicks - _ticks;
+ if (remaining <= 0)
+ continue;
+ // $8300 plays an impact each time the gate reaches the ground.
+ if (_sound && _c64GateDropHeights[frame] == _c64Gate.h)
+ _sound->playSound(2, Sound::kTypeNormal);
+ waitInLoop(remaining - 1);
+ }
}
void CastleEngine::drawC64Gate(Graphics::Surface *surface) {
diff --git a/engines/freescape/games/castle/c64.music.cpp b/engines/freescape/games/castle/c64.music.cpp
index 8d4b8e930fc..40e97ebc26f 100644
--- a/engines/freescape/games/castle/c64.music.cpp
+++ b/engines/freescape/games/castle/c64.music.cpp
@@ -21,6 +21,8 @@
#include "engines/freescape/games/castle/c64.music.h"
+#include "audio/softsynth/sid.h"
+#include "common/mutex.h"
#include "common/textconsole.h"
#include "common/util.h"
#include "freescape/freescape.h"
@@ -31,6 +33,360 @@ using namespace Freescape::CastleMusicData;
namespace Freescape {
+// The original $cfd8 selects subtune index + 1 of the driver at $c800,
+// but ignores effects while music is selected. Interpret effects at 50 Hz
+// during playback, releasing the SID when switching back to music.
+class CastleC64Sound : public Sound, private Audio::AudioStream {
+public:
+ CastleC64Sound(Audio::Mixer *mixer, const Common::Array<byte> &data)
+ : _mixer(mixer), _sid(nullptr), _type(kTypeNormal), _enabled(false),
+ _ghostPlaying(false), _volume(0), _active(false), _finished(true),
+ _index(0), _ticks(0), _samplesUntilTick(0), _sampleRemainder(0),
+ _releaseTicks(0), _quietTicks(0), _peak(0) {
+ // Keep only the original score and instrument data, using C64 addresses.
+ _data.assign(data.begin(), data.begin() + 0x1047);
+ }
+
+ ~CastleC64Sound() override { setEnabled(false); }
+
+ void setEnabled(bool enabled) {
+ _enabled = enabled;
+ if (!enabled) {
+ stopSound(_type);
+ delete _sid;
+ _sid = nullptr;
+ }
+ }
+
+ void playSound(int index, Type type) override {
+ if (!isSoundAvailable(index) || !_mixer->isReady())
+ return;
+
+ // Remove the mixer callback before changing its SID or sequencer state.
+ _mixer->stopHandle(_handle);
+ if (!_sid) {
+ _sid = new Resid::SID(SID::Config::kSidPAL);
+ _sid->init();
+ _sid->setCallbackFrequency(50);
+ }
+ _sid->reset();
+ memset(_channels, 0, sizeof(_channels));
+ _type = type;
+ _ghostPlaying = false;
+ _volume = 15;
+ _active = true;
+ _finished = false;
+ _index = index;
+ _ticks = 0;
+ _samplesUntilTick = _sampleRemainder = 0;
+ _releaseTicks = _quietTicks = _peak = 0;
+ for (int ch = 0; ch < 3; ch++) {
+ Channel &c = _channels[ch];
+ uint address = 0x0d71 + (index + 1) * 6 + ch * 2;
+ c.order = read(address) | (read(address + 1) << 8);
+ for (int i = 0; i < 8; i++)
+ c.instruments[i] = read(0x102f + ch * 8 + i);
+ }
+
+ _mixer->playStream(Audio::Mixer::kSFXSoundType, &_handle, this, -1,
+ Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO);
+ }
+
+ void stopSound(Type type) override {
+ if (_type == type) {
+ _mixer->stopHandle(_handle);
+ _active = false;
+ _finished = true;
+ _ghostPlaying = false;
+ }
+ }
+
+ bool isPlayingSound(Type type) const override {
+ if (_type != type || !_mixer->isSoundHandleActive(_handle))
+ return false;
+ Common::StackLock lock(_mixer->mutex());
+ // The sequence's end releases SOUND waits while the SID decays.
+ return _active;
+ }
+
+ bool isSoundAvailable(int index) const override { return _enabled && index >= 0 && index < 16; }
+
+ void updateGhost(bool active) {
+ if (!_enabled)
+ return;
+ if (!active) {
+ if (_ghostPlaying)
+ stopSound(_type);
+ } else if (!isPlayingSound(_type)) {
+ playSound(15, kTypeNormal);
+ _ghostPlaying = true;
+ }
+ }
+
+private:
+ struct Channel {
+ uint16 order, pattern;
+ byte orderPosition, patternPosition, repeats;
+ byte transpose, detune, instruments[8];
+ byte registers[7];
+ byte duration, rest, gateOffTime;
+ byte vibrato, vibratoDelay, vibratoStep;
+ byte waveform, waveformDelay, waveformStep;
+ };
+
+ Audio::Mixer *_mixer;
+ Audio::SoundHandle _handle;
+ Common::Array<byte> _data;
+ Resid::SID *_sid;
+ Type _type;
+ bool _enabled;
+ bool _ghostPlaying;
+ Channel _channels[3];
+ byte _volume;
+ bool _active;
+ bool _finished;
+ int _index;
+ int _ticks;
+ int _samplesUntilTick;
+ int _sampleRemainder;
+ int _releaseTicks;
+ int _quietTicks;
+ int _peak;
+
+ int getRate() const override { return _mixer->getOutputRate(); }
+ bool isStereo() const override { return false; }
+ bool endOfData() const override { return _finished; }
+
+ int readBuffer(int16 *buffer, int numSamples) override {
+ int generated = 0;
+ while (generated < numSamples && !_finished) {
+ if (!_samplesUntilTick) {
+ if (_active) {
+ // Subtune 1 is a longer musical cue; bound malformed data
+ // without cutting that sequence short.
+ if (++_ticks > 180 * 50)
+ error("Unterminated Castle C64 sound %d", _index);
+ advance();
+ }
+ _sampleRemainder += getRate();
+ _samplesUntilTick = _sampleRemainder / 50;
+ _sampleRemainder %= 50;
+ _peak = 0;
+ }
+ int count = MIN(numSamples - generated, _samplesUntilTick);
+ _sid->readBuffer(buffer + generated, count);
+ if (!_active) {
+ for (int i = 0; i < count; i++)
+ _peak = MAX(_peak, ABS(int(buffer[generated + i])));
+ }
+ generated += count;
+ _samplesUntilTick -= count;
+ if (!_active && !_samplesUntilTick) {
+ // Let the release envelopes finish in the live stream.
+ _quietTicks = _peak < 8 ? _quietTicks + 1 : 0;
+ _finished = ++_releaseTicks >= 30 * 50 || _quietTicks >= 5;
+ }
+ }
+ return generated;
+ }
+
+ byte read(uint address) const {
+ if (address >= _data.size())
+ error("Invalid Castle C64 sound address %x", address);
+ return _data[address];
+ }
+
+ byte readOrder(Channel &c) { return read(c.order + c.orderPosition++); }
+ byte readPattern(Channel &c) { return read(c.pattern + c.patternPosition++); }
+
+ bool nextPattern(Channel &c) {
+ if (c.repeats) {
+ c.repeats--;
+ c.patternPosition = 0;
+ return true;
+ }
+ for (int commands = 0; commands < 256; commands++) {
+ byte command = readOrder(c);
+ if (command == 0xff)
+ return false;
+ if (command < 0x80) {
+ if (command >= 51)
+ error("Invalid Castle C64 sound pattern %d", command);
+ c.pattern = read(0x0d07 + command) | (read(0x0d3c + command) << 8);
+ c.patternPosition = 0;
+ return true;
+ }
+ // $ca72: transpose, repeat, detune, or per-channel instrument map.
+ switch (command & 0x60) {
+ case 0x00:
+ c.transpose = readOrder(c);
+ break;
+ case 0x20:
+ c.repeats = (command & 0x1f) - 1;
+ break;
+ case 0x40:
+ c.detune = readOrder(c);
+ break;
+ case 0x60:
+ if (command == 0xe8)
+ _volume = readOrder(c);
+ else
+ c.instruments[command & 7] = readOrder(c);
+ break;
+ }
+ }
+ error("Invalid Castle C64 sound order list");
+ }
+
+ void setInstrument(Channel &c, byte instrument) {
+ if (instrument >= 8)
+ error("Invalid Castle C64 sound instrument %d", instrument);
+ uint address = 0x0f15 + c.instruments[instrument];
+ // All instruments used by the effect subtunes have fixed pulse width.
+ if (read(address) != 0)
+ error("Unsupported Castle C64 sound pulse modulation");
+ for (int reg = 3; reg < 7; reg++)
+ c.registers[reg] = read(address + reg - 2);
+ c.gateOffTime = read(address + 5);
+ c.vibrato = read(address + 7);
+ c.waveform = read(address + 8);
+ }
+
+ bool parse(Channel &c, int ch) {
+ for (int commands = 0; commands < 256; commands++) {
+ if (!c.pattern && !nextPattern(c))
+ return false;
+ byte command = readPattern(c);
+ if (command == 0xff) {
+ if (!nextPattern(c))
+ return false;
+ } else if (command >= 0x80 && command < 0x90) {
+ setInstrument(c, command & 15);
+ } else if (command == 0xa0) {
+ c.duration = c.rest = readPattern(c);
+ _sid->writeReg(ch * 7 + kSIDV1Ctrl, 0);
+ return true;
+ } else if (command < 0x80) {
+ byte note = command + c.transpose + 20;
+ if (note >= 96)
+ error("Invalid Castle C64 sound note %d", note);
+ uint16 frequency = (read(0x065a + note) | (read(0x06ba + note) << 8)) + c.detune;
+ c.registers[0] = frequency;
+ c.registers[1] = frequency >> 8;
+ c.registers[4] |= 1;
+ c.duration = readPattern(c);
+ if (c.vibrato) {
+ c.vibratoDelay = read(0x0fe2 + c.vibrato);
+ c.vibratoStep = 1;
+ }
+ if (c.waveform) {
+ byte entry = read(0x100e + c.waveform);
+ c.waveformDelay = (entry >> 4) + 1;
+ c.waveformStep = 1;
+ c.registers[4] = read(0x100a + (entry & 7)) | 1;
+ }
+ return true;
+ } else {
+ // The sixteen effect subtunes use notes, instruments and rests.
+ error("Unsupported Castle C64 sound command %x", command);
+ }
+ }
+ error("Invalid Castle C64 sound pattern");
+ }
+
+ void updateEffects(Channel &c) {
+ // $cd97: signed frequency deltas, with the high bit of the step
+ // reversing the table after each seven-entry vibrato half-cycle.
+ if (c.vibrato && c.vibratoDelay != 0x80) {
+ if (c.vibratoDelay && !(c.vibratoDelay & 0x80)) {
+ c.vibratoDelay--;
+ } else {
+ int delta = int8(read(0x0fe2 + c.vibrato + (c.vibratoStep & 0x7f)));
+ if (c.vibratoStep & 0x80)
+ delta = -delta;
+ uint16 frequency = (c.registers[0] | (c.registers[1] << 8)) + delta;
+ c.registers[0] = frequency;
+ c.registers[1] = frequency >> 8;
+ if ((++c.vibratoStep & 0x7f) == 8) {
+ c.vibratoStep ^= 0x89;
+ if (c.vibratoStep == 1 && c.vibratoDelay)
+ c.vibratoDelay--;
+ }
+ }
+ }
+ // $ce26: timed waveform changes, including the noise attack used by
+ // the collision/menu instrument. Preserve the current gate bit.
+ if (c.waveform && !--c.waveformDelay && !(c.waveformStep & 0x80)) {
+ byte entry = read(0x100e + c.waveform + c.waveformStep);
+ c.waveformDelay = entry >> 4;
+ c.registers[4] = (c.registers[4] & 1) | read(0x100a + (entry & 7));
+ byte control = read(0x100d + c.waveform);
+ if (++c.waveformStep == (control & 7))
+ c.waveformStep = control & 0x80 ? 0 : 0xff;
+ }
+ }
+
+ void advance() {
+ // $c885 parses the next note at duration 1, suppressing that frame's
+ // register copy so that the previous gate-off reaches the SID first.
+ bool skipWrites = false;
+ for (int ch = 0; ch < 3; ch++) {
+ Channel &c = _channels[ch];
+ if (c.duration == 1) {
+ c.duration = c.rest = 0;
+ skipWrites = true;
+ }
+ }
+ for (int ch = 0; ch < 3; ch++) {
+ Channel &c = _channels[ch];
+ if (!c.duration) {
+ if (!parse(c, ch)) {
+ _active = false;
+ break;
+ }
+ } else {
+ if (c.duration == c.gateOffTime)
+ c.registers[4] &= 0xfe;
+ c.duration--;
+ if (c.rest)
+ c.rest--;
+ else if (!skipWrites)
+ updateEffects(c);
+ }
+ }
+ if (!_active) {
+ for (int ch = 0; ch < 3; ch++)
+ _sid->writeReg(ch * 7 + kSIDV1Ctrl, 0);
+ } else if (!skipWrites) {
+ for (int ch = 0; ch < 3; ch++) {
+ if (!_channels[ch].rest) {
+ for (int reg = 0; reg < 7; reg++)
+ _sid->writeReg(ch * 7 + reg, _channels[ch].registers[reg]);
+ }
+ }
+ _sid->writeReg(kSIDVolume, _volume);
+ }
+ }
+};
+
+Sound *createCastleC64Sound(Audio::Mixer *mixer, const Common::Array<byte> &data) {
+ if (data.size() < 0x1047)
+ error("Missing Castle C64 sound data");
+ if (mixer->isReady())
+ return new CastleC64Sound(mixer, data);
+ return nullptr;
+}
+
+void enableCastleC64Sound(Sound *sound, bool enabled) {
+ if (sound)
+ static_cast<CastleC64Sound *>(sound)->setEnabled(enabled);
+}
+
+void updateCastleC64GhostSound(Sound *sound, bool active) {
+ if (sound)
+ static_cast<CastleC64Sound *>(sound)->updateGhost(active);
+}
+
const int kCastleSIDVoiceOffset[] = { 0, 7, 14 };
void CastleC64MusicPlayer::ChannelState::reset(const byte *channelOrderList) {
@@ -51,9 +407,12 @@ void CastleC64MusicPlayer::ChannelState::reset(const byte *channelOrderList) {
active = false;
}
-CastleC64MusicPlayer::CastleC64MusicPlayer()
+CastleC64MusicPlayer::CastleC64MusicPlayer(Audio::Mixer *mixer)
: _sid(nullptr),
+ _mixer(mixer),
_musicActive(false),
+ _samplesUntilTick(0),
+ _sampleRemainder(0),
_tick(0) {
}
@@ -71,7 +430,9 @@ void CastleC64MusicPlayer::initSID() {
return;
}
- _sid->start(new Common::Functor0Mem<void, CastleC64MusicPlayer>(this, &CastleC64MusicPlayer::onTimer), 50);
+ // Pull samples through our own music stream. Chip::start() would attach
+ // the SID as plain audio, bypassing ScummVM's music volume control.
+ _sid->setCallbackFrequency(50);
}
void CastleC64MusicPlayer::destroySID() {
@@ -89,21 +450,50 @@ void CastleC64MusicPlayer::sidWrite(int reg, byte data) {
void CastleC64MusicPlayer::startMusic() {
stopMusic();
+ if (!_mixer->isReady())
+ return;
initSID();
if (!_sid)
return;
setupSong();
+ _samplesUntilTick = 0;
+ _sampleRemainder = 0;
+ _mixer->playStream(Audio::Mixer::kMusicSoundType, &_handle, this, -1,
+ Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO);
}
void CastleC64MusicPlayer::stopMusic() {
+ // Stop the mixer callback before touching its SID or channel state.
+ _mixer->stopHandle(_handle);
_musicActive = false;
silenceAll();
destroySID();
}
bool CastleC64MusicPlayer::isPlaying() const {
- return _musicActive;
+ return _mixer->isSoundHandleActive(_handle);
+}
+
+int CastleC64MusicPlayer::getRate() const {
+ return _mixer->getOutputRate();
+}
+
+int CastleC64MusicPlayer::readBuffer(int16 *buffer, int numSamples) {
+ int generated = 0;
+ while (generated < numSamples) {
+ if (!_samplesUntilTick) {
+ onTimer();
+ _sampleRemainder += getRate();
+ _samplesUntilTick = _sampleRemainder / 50;
+ _sampleRemainder %= 50;
+ }
+ int count = MIN(numSamples - generated, _samplesUntilTick);
+ static_cast<Resid::SID *>(_sid)->readBuffer(buffer + generated, count);
+ generated += count;
+ _samplesUntilTick -= count;
+ }
+ return numSamples;
}
void CastleC64MusicPlayer::silenceAll() {
diff --git a/engines/freescape/games/castle/c64.music.h b/engines/freescape/games/castle/c64.music.h
index 368eba5f71c..9657a443a3c 100644
--- a/engines/freescape/games/castle/c64.music.h
+++ b/engines/freescape/games/castle/c64.music.h
@@ -23,18 +23,30 @@
#define FREESCAPE_CASTLE_C64_MUSIC_H
#include "audio/sid.h"
+#include "audio/audiostream.h"
+#include "audio/mixer.h"
+#include "common/array.h"
#include "freescape/music.h"
namespace Freescape {
-class CastleC64MusicPlayer : public MusicPlayer {
+class Sound;
+Sound *createCastleC64Sound(Audio::Mixer *mixer, const Common::Array<byte> &data);
+void enableCastleC64Sound(Sound *sound, bool enabled);
+void updateCastleC64GhostSound(Sound *sound, bool active);
+
+class CastleC64MusicPlayer : public MusicPlayer, private Audio::AudioStream {
public:
- CastleC64MusicPlayer();
+ CastleC64MusicPlayer(Audio::Mixer *mixer);
~CastleC64MusicPlayer() override;
void startMusic() override;
void stopMusic() override;
bool isPlaying() const override;
+ int readBuffer(int16 *buffer, int numSamples) override;
+ int getRate() const override;
+ bool isStereo() const override { return false; }
+ bool endOfData() const override { return false; }
private:
enum {
@@ -63,7 +75,11 @@ private:
};
SID::SID *_sid;
+ Audio::Mixer *_mixer;
+ Audio::SoundHandle _handle;
bool _musicActive;
+ int _samplesUntilTick;
+ int _sampleRemainder;
uint32 _tick;
ChannelState _channels[kChannelCount];
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 9747f75b330..837da24fd7c 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -35,6 +35,7 @@
#include "freescape/freescape.h"
#include "freescape/gfx.h"
#include "freescape/games/castle/castle.h"
+#include "freescape/games/castle/c64.music.h"
#include "freescape/language/8bitDetokeniser.h"
#include "freescape/music.h"
@@ -553,9 +554,9 @@ void CastleEngine::initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *inf
act->addDefaultInputMapping("q");
infoScreenKeyMap->addAction(act);
- act = new Common::Action("TOGGLESOUND", _("Toggle sound"));
+ act = new Common::Action("TOGGLESOUND", isC64() ? _("Toggle music/sound effects") : _("Toggle sound"));
act->setCustomEngineActionEvent(kActionToggleSound);
- act->addDefaultInputMapping("t");
+ act->addDefaultInputMapping(isC64() ? "f" : "t");
infoScreenKeyMap->addAction(act);
act = new Common::Action("ROTL", _("Rotate left"));
@@ -742,6 +743,11 @@ void CastleEngine::gotoArea(uint16 areaID, int entranceID) {
}
void CastleEngine::initGameState() {
+ if (isC64()) {
+ stopAllSounds();
+ stopAllSounds(Sound::kTypeMovement);
+ _syncSound = false;
+ }
FreescapeEngine::initGameState();
_playerHeightNumber = 1;
@@ -804,7 +810,9 @@ void CastleEngine::initGameState() {
_droppingGateStartTicks = isC64() ? -1 : 0;
_thunderFrameDuration = 0;
- if (_playerMusic)
+ if (isC64())
+ enableCastleC64Sound(_sound, !_c64MusicEnabled);
+ if (_playerMusic && (!isC64() || _c64MusicEnabled))
_playerMusic->startMusic();
}
@@ -1279,6 +1287,10 @@ void CastleEngine::drawInfoMenu() {
}
_gfx->setViewport(_viewArea);
+ } else if (isC64() && event.customType == kActionToggleSound) {
+ toggleC64AudioMode();
+ drawC64InfoMenu(surface);
+ menuTexture->update(surface);
} else if (isDOS() && event.customType == kActionToggleSound) {
// TODO
} else if (event.customType == kActionQuit) {
@@ -2133,6 +2145,12 @@ void CastleEngine::checkSensors() {
_lastTick = _ticks;
+ if (isC64()) {
+ // The IRQ at $73a1 supplies the ghost tone while a live spirit is
+ // present. Ordinary effects take priority over this repeating cue.
+ updateCastleC64GhostSound(_sound, _gameStateControl == kFreescapeGameStatePlaying && !_disableSensors && ghostInArea());
+ }
+
if (_sensors.empty()) {
_gfx->_shakeOffset = Common::Point();
return;
@@ -2599,6 +2617,9 @@ Common::Error CastleEngine::loadGameStreamExtended(Common::SeekableReadStream *s
if (isC64()) {
_c64LiftingGateStartTicks = -1;
_droppingGateStartTicks = -1;
+ stopAllSounds();
+ stopAllSounds(Sound::kTypeMovement);
+ _syncSound = false;
}
_keysCollected.clear();
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index 99525ffcac3..74c85b0a2d1 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -106,6 +106,7 @@ public:
void loadAssetsC64FullGame() override;
void drawC64UI(Graphics::Surface *surface) override;
void drawC64InfoMenu(Graphics::Surface *surface);
+ void toggleC64AudioMode();
void drawC64HudSurface(Graphics::Surface *surface, const Graphics::Surface &frame, const Common::Point &origin);
void liftC64Gate();
void dropC64Gate();
@@ -115,6 +116,7 @@ public:
Common::Array<uint32> _c64UIColors;
Common::Array<byte> _c64GateDropHeights;
int _c64LiftingGateStartTicks;
+ bool _c64MusicEnabled;
void drawDOSUI(Graphics::Surface *surface) override;
void drawZXUI(Graphics::Surface *surface) override;
Commit: 81583b292ce3065c72186b0827c40c302b10ba86
https://github.com/scummvm/scummvm/commit/81583b292ce3065c72186b0827c40c302b10ba86
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-07T07:23:52+02:00
Commit Message:
FREESCAPE: background and lighting in castle c64
Changed paths:
engines/freescape/games/castle/c64.cpp
engines/freescape/games/castle/castle.cpp
engines/freescape/games/castle/castle.h
diff --git a/engines/freescape/games/castle/c64.cpp b/engines/freescape/games/castle/c64.cpp
index 383ccb26a00..3df3296ca9b 100644
--- a/engines/freescape/games/castle/c64.cpp
+++ b/engines/freescape/games/castle/c64.cpp
@@ -21,6 +21,7 @@
#include "common/file.h"
#include "common/memstream.h"
+#include "common/random.h"
#include "graphics/managed_surface.h"
#include "freescape/freescape.h"
@@ -47,7 +48,11 @@ enum {
kCastleC64MessageY = 182,
kCastleC64GateFrameTicks = 2,
kCastleC64GateLiftStep = 2,
- kCastleC64GateTransparent = 4
+ kCastleC64GateTransparent = 4,
+ kCastleC64ThunderClockTicks = 60,
+ kCastleC64LightningIdle = 0,
+ kCastleC64LightningBolt = 1,
+ kCastleC64LightningFlash = 2
};
// Match the colors of the bundled C64 border. The unused palette entries are
@@ -116,6 +121,19 @@ static Common::Array<byte> unpackCastleC64UI(Common::SeekableReadStream *file) {
return data;
}
+static void loadCastleC64Bitmap(const Common::Array<byte> &data, uint address, uint width, uint height, Graphics::ManagedSurface *surface) {
+ if (!width || !height || address + width * height > data.size())
+ error("Invalid Castle C64 bitmap at %x", address);
+ surface->create(width * 8, height, Graphics::PixelFormat::createFormatCLUT8());
+ for (uint y = 0; y < height; y++) {
+ for (uint x = 0; x < width * 8; x += 2) {
+ byte color = (data[address + y * width + x / 8] >> (6 - x % 8)) & 3;
+ surface->setPixel(x, y, color);
+ surface->setPixel(x + 1, y, color);
+ }
+ }
+}
+
static void loadCastleC64Frame(const Common::Array<byte> &data, uint address, Graphics::ManagedSurface *surface, int frame = 0) {
// $7cf6 reads a five-byte header: byte width, height, final-byte mask,
// and frame size. These HUD frames all use the whole final byte.
@@ -127,14 +145,7 @@ static void loadCastleC64Frame(const Common::Array<byte> &data, uint address, Gr
uint pixels = address + 5 + frame * size;
if (!width || !height || data[address + 2] != 0xff || size != width * height || pixels + size > data.size())
error("Invalid Castle C64 UI frame at %x", address);
- surface->create(width * 8, height, Graphics::PixelFormat::createFormatCLUT8());
- for (uint y = 0; y < height; y++) {
- for (uint x = 0; x < width * 8; x += 2) {
- byte color = (data[pixels + y * width + x / 8] >> (6 - x % 8)) & 3;
- surface->setPixel(x, y, color);
- surface->setPixel(x + 1, y, color);
- }
- }
+ loadCastleC64Bitmap(data, pixels, width, height, surface);
}
struct CastleC64Repeat {
@@ -356,6 +367,7 @@ void CastleEngine::initC64() {
_viewArea = Common::Rect(40, 32, 280, 152);
_c64LiftingGateStartTicks = -1;
_c64MusicEnabled = true;
+ resetC64Lightning();
// C64 call sites: throw $63b3, climb/drop $53cf/$549f, area change
// $6cde, and a damaging landing $8142. The gate supplies the start sound.
@@ -454,6 +466,11 @@ void CastleEngine::loadAssetsC64FullGame() {
loadMessagesC64(&uiStream, 0x1401, 75);
loadRiddlesC64(&uiStream, 0x18ae, 9);
+ // $4d06 tiles sixteen bytes per row; $4ddd overlays a single 85-row
+ // bolt. Both bitmaps use VIC multicolor pixel pairs, without headers.
+ loadCastleC64Bitmap(uiData, 0x21fa, 16, 18, &_c64MountainBackground);
+ loadCastleC64Bitmap(uiData, 0x20f8, 3, 85, &_c64Lightning);
+
// Preserve multicolor pixel indices until drawing. VIC colors depend on
// the destination 8x8 cell, even within a single moving weight or key.
file.seek(0x301); // Packed color RAM at $0b00, high nibble first.
@@ -655,6 +672,142 @@ void CastleEngine::toggleC64AudioMode() {
}
}
+void CastleEngine::updateC64BackgroundPalette() {
+ uint32 colors[4];
+ for (int color = 0; color < 4; color++) {
+ uint8 r, g, b;
+ _gfx->selectColorFromFourColorPalette(color, r, g, b);
+ // Pen 0 is transparent when compositing the lightning bitmap.
+ colors[color] = _gfx->_texturePixelFormat.ARGBToColor(color ? 255 : 0, r, g, b);
+ }
+ if (!_background)
+ _background = new Graphics::ManagedSurface();
+ if (_thunderFrames.empty())
+ _thunderFrames.push_back(new Graphics::ManagedSurface());
+
+ const Graphics::Surface *sources[] = {&_c64MountainBackground.rawSurface(), &_c64Lightning.rawSurface()};
+ Graphics::ManagedSurface *destinations[] = {_background, _thunderFrames[0]};
+ for (uint frame = 0; frame < ARRAYSIZE(sources); frame++) {
+ const Graphics::Surface &src = *sources[frame];
+ Graphics::ManagedSurface *dst = destinations[frame];
+ dst->create(src.w, src.h, _gfx->_texturePixelFormat);
+ for (int y = 0; y < src.h; y++) {
+ const byte *pixels = (const byte *)src.getBasePtr(0, y);
+ for (int x = 0; x < src.w; x++)
+ dst->setPixel(x, y, colors[pixels[x]]);
+ }
+ }
+
+ delete _skyTexture;
+ _skyTexture = nullptr;
+ for (auto *texture : _thunderTextures)
+ delete texture;
+ _thunderTextures.clear();
+}
+
+void CastleEngine::resetC64Lightning() {
+ _c64NextLightningTicks = -1;
+ _c64LightningPhase = kCastleC64LightningIdle;
+ _c64LightningPhaseTicks = 0;
+ _c64LightningX = 0;
+}
+
+void CastleEngine::updateC64Lightning() {
+ int ticks = _ticks;
+ if (_gameStateControl != kFreescapeGameStatePlaying) {
+ resetC64Lightning();
+ return;
+ }
+
+ // $7531 decrements the initial counter of 5 every 60 PAL ticks,
+ // stopping at 1 until the main loop draws the bolt ($4cb8).
+ if (_c64NextLightningTicks < 0)
+ _c64NextLightningTicks = ticks + 4 * kCastleC64ThunderClockTicks;
+ if (_c64LightningPhase == kCastleC64LightningIdle && ticks >= _c64NextLightningTicks) {
+ // $483e reloads from the low six timer bits plus ten; the next
+ // bolt appears when that counter reaches 1.
+ _c64NextLightningTicks = ticks + (9 + _rnd->getRandomNumber(63)) * kCastleC64ThunderClockTicks;
+ if (_currentArea->isOutside() && !_avoidRenderingFrames) {
+ _c64LightningPhase = kCastleC64LightningBolt;
+ _c64LightningPhaseTicks = ticks + 1;
+ // $4d90 chooses one of 27 byte-aligned positions in the viewport.
+ _c64LightningX = 8 * (1 + _rnd->getRandomNumber(26));
+ }
+ } else if (_c64LightningPhase != kCastleC64LightningIdle && ticks >= _c64LightningPhaseTicks) {
+ if (_c64LightningPhase == kCastleC64LightningBolt) {
+ _c64LightningPhase = kCastleC64LightningFlash;
+ _c64LightningPhaseTicks = ticks + 1;
+ // $484d plays sound 8 and flashes the background for one PAL tick.
+ // A rendering callback must not enter the scripted SOUND wait loop.
+ if (_sound && _currentArea->isOutside())
+ _sound->playSound(8, Sound::kTypeNormal);
+ } else {
+ _c64LightningPhase = kCastleC64LightningIdle;
+ }
+ }
+ if (!_currentArea->isOutside())
+ _c64LightningPhase = kCastleC64LightningIdle;
+}
+
+void CastleEngine::drawC64Background() {
+ updateC64Lightning();
+ clearBackground();
+ _gfx->drawBackground(_currentArea->_skyColor);
+ if (_avoidRenderingFrames || !_currentArea->isOutside())
+ return;
+
+ // Use the same perspective skybox as the other Castle releases. Center
+ // it on the camera so movement does not introduce foreground parallax.
+ Math::Vector3d camera = _inWaitLoop ? _position : getCameraRenderPosition();
+ if (_currentArea->getAreaID() == 1 && _background) {
+ if (!_skyTexture)
+ _skyTexture = _gfx->createTexture(_background->surfacePtr(), true);
+ _gfx->drawSkybox(_skyTexture, camera);
+ }
+
+ if (_c64LightningPhase == kCastleC64LightningFlash)
+ _gfx->clear(255, 255, 255);
+ if (_c64LightningPhase != kCastleC64LightningBolt || _thunderFrames.empty())
+ return;
+
+ // $4c19/$4da5 place the bottom of the lightning bitmap ten rows above
+ // the horizon. Project that horizon using the Castle viewport's FOV.
+ float horizontal = sqrt(_cameraFront.x() * _cameraFront.x() + _cameraFront.z() * _cameraFront.z());
+ if (horizontal < 0.001f)
+ return;
+ float focalLength = _viewArea.height() * 0.5f * 1.6f / tan(Math::deg2rad(75.0f) * 0.5f);
+ float horizon = _viewArea.top + _viewArea.height() * 0.5f + focalLength * _cameraFront.y() / horizontal;
+ if (horizon < _viewArea.top || horizon > _viewArea.bottom + 95)
+ return;
+ int horizonY = int(horizon) + 1;
+
+ // Compose a full-screen layer: the shader renderer's 2D path does not
+ // support partial source/destination rectangles. Clip before uploading.
+ const Graphics::Surface &source = _thunderFrames[0]->rawSurface();
+ int x = _viewArea.left + _c64LightningX;
+ int y = horizonY - 95;
+ Common::Rect dst(x, y, x + source.w, y + source.h);
+ dst.clip(_viewArea);
+ if (dst.isEmpty())
+ return;
+ Common::Rect src(dst.left - x, dst.top - y, dst.right - x, dst.bottom - y);
+ Graphics::ManagedSurface lightning(_screenW, _screenH, _gfx->_texturePixelFormat);
+ lightning.clear(0);
+ lightning.copyRectToSurfaceWithKey(source, dst.left, dst.top, src, 0);
+ if (_thunderTextures.empty())
+ _thunderTextures.push_back(_gfx->createTexture(lightning.surfacePtr()));
+ else
+ _thunderTextures[0]->update(lightning.surfacePtr());
+ _gfx->setViewport(_fullscreenViewArea);
+ _gfx->drawTexturedRect2D(_fullscreenViewArea, _fullscreenViewArea, _thunderTextures[0]);
+ _gfx->setViewport(_viewArea);
+
+ // The lightning blit changes the OpenGL matrices. Restore the camera
+ // before drawing the 3D scene, which must occlude the background.
+ _gfx->updateProjectionMatrix(75.0f, 1.6f, _nearClipPlane, _farClipPlane * 100);
+ _gfx->positionCamera(camera, camera + _cameraFront, _roll);
+}
+
void CastleEngine::liftC64Gate() {
// $8347 raises the gate by two rows per step. Start the clock after the
// initial area is ready so its setup does not consume animation time.
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 837da24fd7c..87f5020ba6d 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -681,6 +681,8 @@ void CastleEngine::gotoArea(uint16 areaID, int entranceID) {
_gfx->fillColorPairArray();
swapPalette(areaID);
+ if (isC64())
+ updateC64BackgroundPalette();
// Enable/disable COLOR15 cycling based on per-area flag (Amiga/Atari)
if ((isAmiga() || isAtariST()) && _currentArea)
@@ -747,6 +749,7 @@ void CastleEngine::initGameState() {
stopAllSounds();
stopAllSounds(Sound::kTypeMovement);
_syncSound = false;
+ resetC64Lightning();
}
FreescapeEngine::initGameState();
_playerHeightNumber = 1;
@@ -2617,6 +2620,7 @@ Common::Error CastleEngine::loadGameStreamExtended(Common::SeekableReadStream *s
if (isC64()) {
_c64LiftingGateStartTicks = -1;
_droppingGateStartTicks = -1;
+ resetC64Lightning();
stopAllSounds();
stopAllSounds(Sound::kTypeMovement);
_syncSound = false;
@@ -2646,6 +2650,10 @@ Common::Error CastleEngine::loadGameStreamExtended(Common::SeekableReadStream *s
void CastleEngine::drawBackground() {
+ if (isC64()) {
+ drawC64Background();
+ return;
+ }
clearBackground();
_gfx->drawBackground(_currentArea->_skyColor);
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index 74c85b0a2d1..f48672351da 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -107,16 +107,26 @@ public:
void drawC64UI(Graphics::Surface *surface) override;
void drawC64InfoMenu(Graphics::Surface *surface);
void toggleC64AudioMode();
+ void updateC64BackgroundPalette();
+ void resetC64Lightning();
+ void updateC64Lightning();
+ void drawC64Background();
void drawC64HudSurface(Graphics::Surface *surface, const Graphics::Surface &frame, const Common::Point &origin);
void liftC64Gate();
void dropC64Gate();
void drawC64Gate(Graphics::Surface *surface);
Graphics::ManagedSurface _c64KeysBackground;
Graphics::ManagedSurface _c64Gate;
+ Graphics::ManagedSurface _c64MountainBackground;
+ Graphics::ManagedSurface _c64Lightning;
Common::Array<uint32> _c64UIColors;
Common::Array<byte> _c64GateDropHeights;
int _c64LiftingGateStartTicks;
bool _c64MusicEnabled;
+ int _c64NextLightningTicks;
+ int _c64LightningPhase;
+ int _c64LightningPhaseTicks;
+ int _c64LightningX;
void drawDOSUI(Graphics::Surface *surface) override;
void drawZXUI(Graphics::Surface *surface) override;
Commit: 1e6f0d0504e4f8048ac4cfa941547808dc1a362c
https://github.com/scummvm/scummvm/commit/1e6f0d0504e4f8048ac4cfa941547808dc1a362c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-07T07:23:53+02:00
Commit Message:
FREESCAPE: ghost attack effect implemented in castle c64
Changed paths:
engines/freescape/games/castle/c64.cpp
engines/freescape/games/castle/castle.cpp
engines/freescape/games/castle/castle.h
diff --git a/engines/freescape/games/castle/c64.cpp b/engines/freescape/games/castle/c64.cpp
index 3df3296ca9b..c024a0e105f 100644
--- a/engines/freescape/games/castle/c64.cpp
+++ b/engines/freescape/games/castle/c64.cpp
@@ -367,6 +367,7 @@ void CastleEngine::initC64() {
_viewArea = Common::Rect(40, 32, 280, 152);
_c64LiftingGateStartTicks = -1;
_c64MusicEnabled = true;
+ _c64SpiritAttackStartTicks = -1;
resetC64Lightning();
// C64 call sites: throw $63b3, climb/drop $53cf/$549f, area change
@@ -466,6 +467,10 @@ void CastleEngine::loadAssetsC64FullGame() {
loadMessagesC64(&uiStream, 0x1401, 75);
loadRiddlesC64(&uiStream, 0x18ae, 9);
+ // $7403 selects the screen-RAM colour pairs at counter values 8 and 4.
+ _c64SpiritAttackColors[0] = uiData[0x735c];
+ _c64SpiritAttackColors[1] = uiData[0x735b];
+
// $4d06 tiles sixteen bytes per row; $4ddd overlays a single 85-row
// bolt. Both bitmaps use VIC multicolor pixel pairs, without headers.
loadCastleC64Bitmap(uiData, 0x21fa, 16, 18, &_c64MountainBackground);
@@ -672,6 +677,34 @@ void CastleEngine::toggleC64AudioMode() {
}
}
+void CastleEngine::updateC64SpiritPalette() {
+ int screenHigh = _currentArea->_underFireBackgroundColor;
+ int screenLow = _currentArea->_paperColor;
+ if (_gameStateControl == kFreescapeGameStatePlaying && !isPaused() && !_disableSensors && ghostInArea()) {
+ int ticks = _ticks;
+ if (_c64SpiritAttackStartTicks < 0)
+ _c64SpiritAttackStartTicks = ticks;
+
+ // $73cb-$742d counts down from ten at 50 Hz: two ticks of the
+ // area colours, four of $78, four of $82, then repeat. Only the
+ // viewport's screen RAM changes; background and colour RAM do not.
+ int phase = (ticks - _c64SpiritAttackStartTicks) % 10;
+ if (phase >= 2) {
+ byte colors = _c64SpiritAttackColors[phase < 6 ? 0 : 1];
+ screenHigh = colors >> 4;
+ screenLow = colors & 15;
+ }
+ } else {
+ _c64SpiritAttackStartTicks = -1;
+ }
+
+ if (_gfx->_underFireBackgroundColor == screenHigh && _gfx->_paperColor == screenLow)
+ return;
+ _gfx->_underFireBackgroundColor = screenHigh;
+ _gfx->_paperColor = screenLow;
+ updateC64BackgroundPalette();
+}
+
void CastleEngine::updateC64BackgroundPalette() {
uint32 colors[4];
for (int color = 0; color < 4; color++) {
@@ -750,6 +783,7 @@ void CastleEngine::updateC64Lightning() {
}
void CastleEngine::drawC64Background() {
+ updateC64SpiritPalette();
updateC64Lightning();
clearBackground();
_gfx->drawBackground(_currentArea->_skyColor);
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 87f5020ba6d..5a1c88b47fc 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -630,6 +630,8 @@ void CastleEngine::gotoArea(uint16 areaID, int entranceID) {
assert(_areaMap.contains(areaID));
_currentArea = _areaMap[areaID];
+ if (isC64())
+ _c64SpiritAttackStartTicks = -1;
_currentArea->show();
_maxFallingDistance = MAX(32, _currentArea->getScale() * 16 - 2);
@@ -749,6 +751,7 @@ void CastleEngine::initGameState() {
stopAllSounds();
stopAllSounds(Sound::kTypeMovement);
_syncSound = false;
+ _c64SpiritAttackStartTicks = -1;
resetC64Lightning();
}
FreescapeEngine::initGameState();
@@ -2175,8 +2178,9 @@ void CastleEngine::checkSensors() {
_mixer->playStream(Audio::Mixer::kSFXSoundType, &_soundFxGhostHandle, speaker, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::YES);
}*/
- // This is the frequency to shake the screen
- if (_ticks % 5 == 0) {
+ // C64 cycles its viewport colours before rendering in drawC64Background.
+ // The other platforms use the generic attack flash/shake path.
+ if (!isC64() && _ticks % 5 == 0) {
if (_underFireFrames <= 0)
_underFireFrames = 1;
}
@@ -2620,6 +2624,7 @@ Common::Error CastleEngine::loadGameStreamExtended(Common::SeekableReadStream *s
if (isC64()) {
_c64LiftingGateStartTicks = -1;
_droppingGateStartTicks = -1;
+ _c64SpiritAttackStartTicks = -1;
resetC64Lightning();
stopAllSounds();
stopAllSounds(Sound::kTypeMovement);
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index f48672351da..056fdd026f1 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -107,6 +107,7 @@ public:
void drawC64UI(Graphics::Surface *surface) override;
void drawC64InfoMenu(Graphics::Surface *surface);
void toggleC64AudioMode();
+ void updateC64SpiritPalette();
void updateC64BackgroundPalette();
void resetC64Lightning();
void updateC64Lightning();
@@ -123,6 +124,8 @@ public:
Common::Array<byte> _c64GateDropHeights;
int _c64LiftingGateStartTicks;
bool _c64MusicEnabled;
+ byte _c64SpiritAttackColors[2];
+ int _c64SpiritAttackStartTicks;
int _c64NextLightningTicks;
int _c64LightningPhase;
int _c64LightningPhaseTicks;
Commit: 3d55dacb49eb0302a56f09ecc28608045e319846
https://github.com/scummvm/scummvm/commit/3d55dacb49eb0302a56f09ecc28608045e319846
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-07T07:23:53+02:00
Commit Message:
FREESCAPE: reworked painters algorithm to fix some artifacts
Changed paths:
engines/freescape/area.cpp
engines/freescape/area.h
engines/freescape/freescape.cpp
engines/freescape/loaders/8bitBinaryLoader.cpp
engines/freescape/objects/geometricobject.cpp
engines/freescape/ui.cpp
diff --git a/engines/freescape/area.cpp b/engines/freescape/area.cpp
index 61894ad188c..5c53b8148b8 100644
--- a/engines/freescape/area.cpp
+++ b/engines/freescape/area.cpp
@@ -88,6 +88,8 @@ Area::Area(uint16 areaID_, uint16 areaFlags_, ObjectMap *objectsByID_, ObjectMap
_lastTick = 0;
_lastDepthLayerTick = 0;
+ _lastCameraRoll = 0.0f;
+ _lastDepthLayerCameraRoll = 0.0f;
_lastFov = 0.0f;
_lastAspectRatio = 0.0f;
_lastNearClipPlane = 0.0f;
@@ -258,8 +260,7 @@ static float aabbMinProjection(const Math::AABB &aabb, const Math::Vector3d &axi
return support.dotProduct(axis);
}
-static bool aabbIntersectsViewVolume(const Math::AABB &aabb, const Math::Vector3d &camera, const Math::Vector3d &direction, float fov, float aspectRatio, float nearClipPlane, float farClipPlane) {
- (void)aspectRatio;
+static bool aabbIntersectsViewVolume(const Math::AABB &aabb, const Math::Vector3d &camera, const Math::Vector3d &direction, float roll, float fov, float aspectRatio, float nearClipPlane, float farClipPlane) {
if (!aabb.isValid())
return false;
@@ -267,40 +268,110 @@ static bool aabbIntersectsViewVolume(const Math::AABB &aabb, const Math::Vector3
if (front.getSquareMagnitude() == 0.0f)
return true;
+ // Exclude offscreen objects before sorting; they can change the visible draw order.
+ Math::Vector3d right = Math::Vector3d::crossProduct(front, Math::Vector3d(0.0f, 1.0f, 0.0f));
+ if (right.getSquareMagnitude() < 0.0001f)
+ right = Math::Vector3d(1.0f, 0.0f, 0.0f);
+ else
+ right.normalize();
+ Math::Vector3d up = Math::Vector3d::crossProduct(right, front).getNormalized();
+ if (roll != 0.0f) {
+ // Match positionCamera's roll by rotating the view axes inversely.
+ const float c = cos(Math::deg2rad(roll));
+ const float s = sin(Math::deg2rad(roll));
+ auto rotateAxis = [c, s](const Math::Vector3d &axis) {
+ return Math::Vector3d(c * axis.x() + s * axis.y(), -s * axis.x() + c * axis.y(), axis.z());
+ };
+ front = rotateAxis(front);
+ right = rotateAxis(right);
+ up = rotateAxis(up);
+ }
+
const float padding = 32.0f;
const float minDepth = aabbMinProjection(aabb, front) - camera.dotProduct(front);
const float maxDepth = aabbMaxProjection(aabb, front) - camera.dotProduct(front);
-
if (maxDepth < nearClipPlane - padding)
return false;
if (minDepth > farClipPlane + padding)
return false;
- // Coarse view-octant cull matching the original (compute_view_clip_bounds):
- // keep every object in the world octant(s) the frustum spans rather than a tight
- // cone, and on axes the frustum straddles (its component within the fov
- // half-angle) keep both sides. The kept set must match the original because the
- // painter's bubble sort below is non-transitive: a tighter cone drops
- // straddle-axis side objects and reorders the visible ones.
- const float threshold = (float)sin(Math::deg2rad(fov) / 2.0f);
- const Math::Vector3d mn = aabb.getMin();
- const Math::Vector3d mx = aabb.getMax();
- for (int i = 0; i < 3; i++) {
- const float comp = front.getValue(i);
- if (comp > threshold && mx.getValue(i) < camera.getValue(i))
- return false;
- if (comp < -threshold && mn.getValue(i) > camera.getValue(i))
+ // Match updateProjectionMatrix's horizontal FOV.
+ const float horizontalScale = tan(Math::deg2rad(fov) / 2.0f);
+ const float verticalScale = horizontalScale / aspectRatio;
+ const Math::Vector3d planes[] = {
+ front * horizontalScale + right, front * horizontalScale - right,
+ front * verticalScale + up, front * verticalScale - up
+ };
+ for (uint i = 0; i < ARRAYSIZE(planes); i++) {
+ if (aabbMaxProjection(aabb, planes[i]) - camera.dotProduct(planes[i]) < -padding)
return false;
}
return true;
}
-static bool objectIsSortCandidate(Object *obj, const Math::Vector3d &camera, const Math::Vector3d &direction, float fov, float aspectRatio, float nearClipPlane, float farClipPlane) {
+static bool objectIsSortCandidate(Object *obj, const Math::Vector3d &camera, const Math::Vector3d &direction, float roll, float fov, float aspectRatio, float nearClipPlane, float farClipPlane) {
if (!obj || obj->isDestroyed() || obj->isInvisible() || !obj->isGeometric())
return false;
- return aabbIntersectsViewVolume(obj->_occlusionBox, camera, direction, fov, aspectRatio, nearClipPlane, farClipPlane);
+ // Sorting bounds may exclude geometry; cull using the actual bounds.
+ return aabbIntersectsViewVolume(obj->_boundingBox, camera, direction, roll, fov, aspectRatio, nearClipPlane, farClipPlane);
+}
+
+// Returns 0 if incomparable, 1 if A is closer, or 2 if B is closer.
+static int compareBoundingBoxAxis(float minA, float maxA, float minB, float maxB) {
+ // Touching bounds are comparable; overlapping intervals are not.
+ if (minA < maxB && minB < maxA)
+ return 0;
+
+ const bool negativeA = minA < 0.0f;
+ const bool negativeB = minB < 0.0f;
+ if (negativeA != (maxA < 0.0f))
+ return 1;
+ if (negativeB != (maxB < 0.0f))
+ return 2;
+ if (negativeA != negativeB)
+ return 0;
+
+ float difference = minB - minA;
+ if (difference == 0.0f)
+ difference = maxB - maxA;
+ return (difference < 0.0f) == negativeB ? 1 : 2;
+}
+
+static void sortObjectsForRendering(ObjectArray &objects, const Math::Vector3d &camera) {
+ const int n = objects.size();
+ if (n < 2)
+ return;
+
+ // Start in file order, with globals first.
+ Common::sort(objects.begin(), objects.end(), [](Object *a, Object *b) {
+ return a->_loadIndex < b->_loadIndex;
+ });
+
+ // Incomparable pairs also swap, so keep n - 1 passes over all adjacent pairs.
+ for (int pass = 1; pass < n; pass++) {
+ bool changed = false;
+ for (int j = 0; j < n - 1; j++) {
+ // Sort using unrotated header bounds relative to the camera.
+ const Math::Vector3d minA = objects[j]->_occlusionBox.getMin() - camera;
+ const Math::Vector3d maxA = objects[j]->_occlusionBox.getMax() - camera;
+ const Math::Vector3d minB = objects[j + 1]->_occlusionBox.getMin() - camera;
+ const Math::Vector3d maxB = objects[j + 1]->_occlusionBox.getMax() - camera;
+ int result = 0;
+ for (int axis = 0; axis < 3; axis++)
+ result = (result << 2) | compareBoundingBoxAxis(minA.getValue(axis), maxA.getValue(axis), minB.getValue(axis), maxB.getValue(axis));
+
+ // Keep order only if B is closer on some axis and A is closer on none.
+ if (result != 0 && (result & 0x15) == 0)
+ continue;
+
+ SWAP(objects[j], objects[j + 1]);
+ changed = true;
+ }
+ if (!changed)
+ break;
+ }
}
static float aabbNearestDepth(const Math::AABB &aabb, const Math::Vector3d &camera, const Math::Vector3d &direction) {
@@ -356,10 +427,10 @@ static bool objectInDepthLayer(Object *obj, const Math::Vector3d &camera, const
return depthLayer == Area::kRenderDepthForeground ? foreground : !foreground;
}
-void Area::draw(Freescape::Renderer *gfx, uint32 animationTicks, Math::Vector3d camera, Math::Vector3d direction, bool insideWait, float fov, float aspectRatio, float nearClipPlane, float farClipPlane) {
+void Area::draw(Freescape::Renderer *gfx, uint32 animationTicks, Math::Vector3d camera, Math::Vector3d direction, float roll, bool insideWait, float fov, float aspectRatio, float nearClipPlane, float farClipPlane) {
bool runAnimation = animationTicks != _lastTick;
bool cameraChanged = camera != _lastCameraPosition;
- bool directionChanged = direction != _lastCameraDirection;
+ bool directionChanged = direction != _lastCameraDirection || roll != _lastCameraRoll;
bool projectionChanged = fov != _lastFov || aspectRatio != _lastAspectRatio || nearClipPlane != _lastNearClipPlane || farClipPlane != _lastFarClipPlane;
bool sort = runAnimation || cameraChanged || directionChanged || projectionChanged || _sortedObjects.empty();
@@ -394,7 +465,7 @@ void Area::draw(Freescape::Renderer *gfx, uint32 animationTicks, Math::Vector3d
continue;
}
- if (sort && objectIsSortCandidate(obj, camera, direction, fov, aspectRatio, nearClipPlane, farClipPlane))
+ if (sort && objectIsSortCandidate(obj, camera, direction, roll, fov, aspectRatio, nearClipPlane, farClipPlane))
_sortedObjects.push_back(obj);
}
}
@@ -403,117 +474,8 @@ void Area::draw(Freescape::Renderer *gfx, uint32 animationTicks, Math::Vector3d
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 != signMaxA) // A covers 0 (L9ce6_first_object_is_closer)
- return 1; // A is closer
- if (signMinB != signMaxB) // B covers 0 (L9cec_second_object_is_closer)
- return 2; // B is closer
-
- 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 = _sortedObjects.size();
- if (n > 1 && sort) {
- // Seed the non-transitive bubble sort below in object load (file) order --
- // the same order the original game's renderer iterates its object list.
- // That data order is what makes the Newell pairwise sort resolve
- // correctly; any depth heuristic (center distance, nearest depth) is only
- // an approximation and mis-orders some scenes, the file order does not.
- Common::sort(_sortedObjects.begin(), _sortedObjects.end(),
- [](Object *a, Object *b) {
- return a->_loadIndex < b->_loadIndex;
- });
- 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 = _sortedObjects[j];
- Object *b = _sortedObjects[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)
- _sortedObjects[j] = b;
- _sortedObjects[j + 1] = a;
- changed = true;
- }
- }
- if (!changed)
- break;
- }
- }
+ if (sort)
+ sortObjectsForRendering(_sortedObjects, camera);
for (auto &obj : _sortedObjects) {
obj->draw(gfx);
@@ -528,6 +490,7 @@ void Area::draw(Freescape::Renderer *gfx, uint32 animationTicks, Math::Vector3d
if (sort) {
_lastCameraPosition = camera;
_lastCameraDirection = direction;
+ _lastCameraRoll = roll;
_lastFov = fov;
_lastAspectRatio = aspectRatio;
_lastNearClipPlane = nearClipPlane;
@@ -535,10 +498,10 @@ void Area::draw(Freescape::Renderer *gfx, uint32 animationTicks, Math::Vector3d
}
}
-void Area::drawDepthLayer(Freescape::Renderer *gfx, uint32 animationTicks, Math::Vector3d camera, Math::Vector3d direction, bool insideWait, RenderDepthLayer depthLayer, float foregroundDistance, float fov, float aspectRatio, float nearClipPlane, float farClipPlane) {
+void Area::drawDepthLayer(Freescape::Renderer *gfx, uint32 animationTicks, Math::Vector3d camera, Math::Vector3d direction, float roll, bool insideWait, RenderDepthLayer depthLayer, float foregroundDistance, float fov, float aspectRatio, float nearClipPlane, float farClipPlane) {
bool runAnimation = depthLayer != kRenderDepthBackground && animationTicks != _lastDepthLayerTick;
bool cameraChanged = camera != _lastDepthLayerCameraPosition;
- bool directionChanged = direction != _lastDepthLayerCameraDirection;
+ bool directionChanged = direction != _lastDepthLayerCameraDirection || roll != _lastDepthLayerCameraRoll;
bool projectionChanged = fov != _lastDepthLayerFov || aspectRatio != _lastDepthLayerAspectRatio || nearClipPlane != _lastDepthLayerNearClipPlane || farClipPlane != _lastDepthLayerFarClipPlane;
bool layerChanged = depthLayer != _lastRenderDepthLayer || (depthLayer != kRenderDepthAll && ABS(foregroundDistance - _lastForegroundDistance) > 0.001f);
bool sort = runAnimation || cameraChanged || directionChanged || projectionChanged || layerChanged || _depthLayerSortedObjects.empty();
@@ -579,7 +542,7 @@ void Area::drawDepthLayer(Freescape::Renderer *gfx, uint32 animationTicks, Math:
if (sort &&
objectInDepthLayer(obj, camera, normalizedDirection, depthLayer, foregroundDistance) &&
- objectIsSortCandidate(obj, camera, direction, fov, aspectRatio, nearClipPlane, farClipPlane))
+ objectIsSortCandidate(obj, camera, direction, roll, fov, aspectRatio, nearClipPlane, farClipPlane))
_depthLayerSortedObjects.push_back(obj);
}
}
@@ -588,113 +551,8 @@ void Area::drawDepthLayer(Freescape::Renderer *gfx, uint32 animationTicks, Math:
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 != signMaxA) // A covers 0 (L9ce6_first_object_is_closer)
- return 1; // A is closer
- if (signMinB != signMaxB) // B covers 0 (L9cec_second_object_is_closer)
- return 2; // B is closer
-
- 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) {
- // Seed in object load (file) order, matching the original (see Area::draw).
- Common::sort(_depthLayerSortedObjects.begin(), _depthLayerSortedObjects.end(),
- [](Object *a, Object *b) {
- return a->_loadIndex < b->_loadIndex;
- });
- 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;
- }
- }
+ if (sort)
+ sortObjectsForRendering(_depthLayerSortedObjects, camera);
for (auto &obj : _depthLayerSortedObjects) {
obj->draw(gfx);
@@ -710,6 +568,7 @@ void Area::drawDepthLayer(Freescape::Renderer *gfx, uint32 animationTicks, Math:
if (sort) {
_lastDepthLayerCameraPosition = camera;
_lastDepthLayerCameraDirection = direction;
+ _lastDepthLayerCameraRoll = roll;
_lastDepthLayerFov = fov;
_lastDepthLayerAspectRatio = aspectRatio;
_lastDepthLayerNearClipPlane = nearClipPlane;
diff --git a/engines/freescape/area.h b/engines/freescape/area.h
index 0e1d908cc7c..988c476f799 100644
--- a/engines/freescape/area.h
+++ b/engines/freescape/area.h
@@ -64,8 +64,8 @@ public:
uint8 getScale();
void remapColor(int index, int color);
void unremapColor(int index);
- void draw(Renderer *gfx, uint32 animationTicks, Math::Vector3d camera, Math::Vector3d direction, bool insideWait, float fov, float aspectRatio, float nearClipPlane, float farClipPlane);
- void drawDepthLayer(Renderer *gfx, uint32 animationTicks, Math::Vector3d camera, Math::Vector3d direction, bool insideWait, RenderDepthLayer depthLayer, float foregroundDistance, float fov, float aspectRatio, float nearClipPlane, float farClipPlane);
+ void draw(Renderer *gfx, uint32 animationTicks, Math::Vector3d camera, Math::Vector3d direction, float roll, bool insideWait, float fov, float aspectRatio, float nearClipPlane, float farClipPlane);
+ void drawDepthLayer(Renderer *gfx, uint32 animationTicks, Math::Vector3d camera, Math::Vector3d direction, float roll, bool insideWait, RenderDepthLayer depthLayer, float foregroundDistance, float fov, float aspectRatio, float nearClipPlane, float farClipPlane);
void drawGroup(Renderer *gfx, Group *group, bool runAnimation);
void show();
@@ -117,6 +117,7 @@ public:
private:
Math::Vector3d _lastCameraPosition;
Math::Vector3d _lastCameraDirection;
+ float _lastCameraRoll;
float _lastFov;
float _lastAspectRatio;
float _lastNearClipPlane;
@@ -125,6 +126,7 @@ private:
ObjectArray _depthLayerSortedObjects;
Math::Vector3d _lastDepthLayerCameraPosition;
Math::Vector3d _lastDepthLayerCameraDirection;
+ float _lastDepthLayerCameraRoll;
float _lastDepthLayerFov;
float _lastDepthLayerAspectRatio;
float _lastDepthLayerNearClipPlane;
diff --git a/engines/freescape/freescape.cpp b/engines/freescape/freescape.cpp
index e435cbc80b6..0f8c645fe0c 100644
--- a/engines/freescape/freescape.cpp
+++ b/engines/freescape/freescape.cpp
@@ -747,7 +747,7 @@ void FreescapeEngine::drawFrame() {
drawBackground();
if (_avoidRenderingFrames == 0) { // Avoid rendering inside objects
- _currentArea->draw(_gfx, _ticks / 10, renderPosition, _cameraFront, false, fov, aspectRatio, _nearClipPlane, farClipPlane);
+ _currentArea->draw(_gfx, _ticks / 10, renderPosition, _cameraFront, _roll, false, fov, aspectRatio, _nearClipPlane, farClipPlane);
if (_gameStateControl == kFreescapeGameStatePlaying &&
_currentArea->hasActiveGroups() && _ticks % 50 == 0) {
executeMovementConditions();
@@ -821,7 +821,7 @@ void FreescapeEngine::drawFrameStereo(int farClipPlane) {
drawBackground();
if (_avoidRenderingFrames == 0)
- _currentArea->drawDepthLayer(_gfx, _ticks / 10, renderPosition, _cameraFront, false, Area::kRenderDepthBackground, stereoForegroundDistance, fov, aspectRatio, _nearClipPlane, farClipPlane);
+ _currentArea->drawDepthLayer(_gfx, _ticks / 10, renderPosition, _cameraFront, _roll, false, Area::kRenderDepthBackground, stereoForegroundDistance, fov, aspectRatio, _nearClipPlane, farClipPlane);
for (int pass = 0; pass < 2; pass++) {
_gfx->setStereoEye(pass == 0 ? Renderer::kStereoEyeLeft : Renderer::kStereoEyeRight);
@@ -831,7 +831,7 @@ void FreescapeEngine::drawFrameStereo(int farClipPlane) {
_gfx->clearDepthBuffer();
if (_avoidRenderingFrames == 0) // Avoid rendering inside objects
- _currentArea->drawDepthLayer(_gfx, _ticks / 10, renderPosition, _cameraFront, false, Area::kRenderDepthForeground, stereoForegroundDistance, fov, aspectRatio, _nearClipPlane, farClipPlane);
+ _currentArea->drawDepthLayer(_gfx, _ticks / 10, renderPosition, _cameraFront, _roll, false, Area::kRenderDepthForeground, stereoForegroundDistance, fov, aspectRatio, _nearClipPlane, farClipPlane);
if (_underFireFrames > 0) {
for (auto &it : _sensors) {
diff --git a/engines/freescape/loaders/8bitBinaryLoader.cpp b/engines/freescape/loaders/8bitBinaryLoader.cpp
index 8b732d7903e..f1f99c47df6 100644
--- a/engines/freescape/loaders/8bitBinaryLoader.cpp
+++ b/engines/freescape/loaders/8bitBinaryLoader.cpp
@@ -793,7 +793,8 @@ Area *FreescapeEngine::load8bitArea(Common::SeekableReadStream *file, uint16 nco
if (newObject) {
newObject->scale(scale);
- newObject->_loadIndex = 0x4000 + object; // area objects render after globals (original pass 2)
+ // Seed the sort with globals (area 255) before local objects.
+ newObject->_loadIndex = (areaNumber == 255 ? 0 : 0x4000) + object;
if (newObject->getType() == kEntranceType) {
if (entrancesByID->contains(newObject->getObjectID() & 0x7fff))
error("WARNING: replacing object id %d (%d)", newObject->getObjectID(), newObject->getObjectID() & 0x7fff);
diff --git a/engines/freescape/objects/geometricobject.cpp b/engines/freescape/objects/geometricobject.cpp
index 9741105a7b3..16042386de4 100644
--- a/engines/freescape/objects/geometricobject.cpp
+++ b/engines/freescape/objects/geometricobject.cpp
@@ -190,10 +190,15 @@ GeometricObject::GeometricObject(
assert(flatAxes <= 1);
}
+ // Preserve header bounds for sorting, separately from geometry bounds.
+ _occlusionBox.expand(_origin);
+ _occlusionBox.expand(_origin + _size);
computeBoundingBox();
}
void GeometricObject::setOrigin(Math::Vector3d origin_) {
+ const Math::Vector3d offset = origin_ - _origin;
+ _occlusionBox = Math::AABB(_occlusionBox.getMin() + offset, _occlusionBox.getMax() + offset);
_origin = origin_;
computeBoundingBox();
}
@@ -223,6 +228,8 @@ void GeometricObject::offsetOrigin(Math::Vector3d origin_) {
}
void GeometricObject::scale(int factor) {
+ // Scale endpoints directly to avoid rounding overlaps between touching objects.
+ _occlusionBox = Math::AABB(_occlusionBox.getMin() / factor, _occlusionBox.getMax() / factor);
_origin = _origin / factor;
_size = _size / factor;
if (_ordinates) {
@@ -278,16 +285,12 @@ Object *GeometricObject::duplicate() {
copy->_cyclingColors = _cyclingColors;
copy->_loadIndex = _loadIndex;
+ copy->_occlusionBox = _occlusionBox;
return copy;
}
void GeometricObject::computeBoundingBox() {
_boundingBox = Math::AABB();
- _occlusionBox = Math::AABB();
-
- // These are used for the rendered, they should NOT be refined or it will break the sorting algorithm
- _occlusionBox.expand(_origin);
- _occlusionBox.expand(_origin + _size);
Math::Vector3d v;
switch (_type) {
diff --git a/engines/freescape/ui.cpp b/engines/freescape/ui.cpp
index 0ef9f6b0ce2..ea85db50f24 100644
--- a/engines/freescape/ui.cpp
+++ b/engines/freescape/ui.cpp
@@ -92,7 +92,7 @@ void FreescapeEngine::waitInLoop(int maxWait) {
_gfx->positionCamera(_position, _position + _cameraFront, _roll);
drawBackground();
- _currentArea->draw(_gfx, _ticks / 10, _position, _cameraFront, true, fov, aspectRatio, _nearClipPlane, farClipPlane);
+ _currentArea->draw(_gfx, _ticks / 10, _position, _cameraFront, _roll, true, fov, aspectRatio, _nearClipPlane, farClipPlane);
drawBorder();
drawUI();
Commit: 253f3b0ea97eb9840a31f81019d90f6077ff82f5
https://github.com/scummvm/scummvm/commit/253f3b0ea97eb9840a31f81019d90f6077ff82f5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-07T07:23:53+02:00
Commit Message:
FREESCAPE: implemented loading of simple geometry for 3DCK games
Changed paths:
A engines/freescape/games/3dck/3dck.cpp
A engines/freescape/games/3dck/3dck.h
engines/freescape/detection.cpp
engines/freescape/freescape.cpp
engines/freescape/freescape.h
engines/freescape/games/palettes.cpp
engines/freescape/gfx.cpp
engines/freescape/metaengine.cpp
engines/freescape/module.mk
engines/freescape/movement.cpp
engines/freescape/ui.cpp
diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index 3f4e6ef5d32..32d1e535374 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -1156,6 +1156,15 @@ const ADGameDescription gameDescriptions[] = {
GUIO3(GUIO_NOMIDI, GUIO_RENDEREGA, GAMEOPTION_WASD_CONTROLS)
},
// 3D Construction Kit games
+ {
+ "3dkit",
+ "Cube",
+ AD_ENTRY1s("CUBE.RUN", "3b7930be0f646b98885cfb70c26c89a2", 66138),
+ Common::EN_ANY,
+ Common::kPlatformDOS,
+ ADGF_UNSTABLE,
+ GUIO2(GUIO_NOMIDI, GUIO_RENDERVGA)
+ },
{
"3dkit",
"The 3-D Kit Game",
diff --git a/engines/freescape/freescape.cpp b/engines/freescape/freescape.cpp
index 0f8c645fe0c..8928e213545 100644
--- a/engines/freescape/freescape.cpp
+++ b/engines/freescape/freescape.cpp
@@ -258,6 +258,8 @@ FreescapeEngine::FreescapeEngine(OSystem *syst, const ADGameDescription *gd)
// close-up detail. Other games need a smaller value to avoid clipping of nearby objects
_nearClipPlane = (isDriller() || isDark()) ? 2 : 0.5;
_farClipPlane = 8192 + 1802; // Added some extra distance to avoid flickering
+ _fieldOfView = 75.0f;
+ _viewAspectRatio = isCastle() ? 1.6f : 2.18f;
// These depends on the specific game
_playerHeight = 0;
@@ -721,8 +723,8 @@ void FreescapeEngine::drawFrame() {
return;
}
- const float fov = 75.0f;
- float aspectRatio = isCastle() ? 1.6 : 2.18;
+ const float fov = _fieldOfView;
+ float aspectRatio = _viewAspectRatio;
Math::Vector3d renderPosition = getCameraRenderPosition();
@@ -787,8 +789,8 @@ void FreescapeEngine::drawFrame() {
}
void FreescapeEngine::drawFrameStereo(int farClipPlane) {
- const float fov = 75.0f;
- float aspectRatio = isCastle() ? 1.6 : 2.18;
+ const float fov = _fieldOfView;
+ float aspectRatio = _viewAspectRatio;
Math::Vector3d renderPosition = getCameraRenderPosition();
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index f8dcd533c2a..f3798dfb4ce 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -575,6 +575,8 @@ public:
void flashScreen(int backgroundColor);
uint8 _colorNumber;
Math::Vector3d _scaleVector;
+ float _fieldOfView;
+ float _viewAspectRatio;
float _nearClipPlane;
float _farClipPlane;
diff --git a/engines/freescape/games/3dck/3dck.cpp b/engines/freescape/games/3dck/3dck.cpp
new file mode 100644
index 00000000000..8957d71e7ea
--- /dev/null
+++ b/engines/freescape/games/3dck/3dck.cpp
@@ -0,0 +1,289 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/substream.h"
+#include "math/utils.h"
+
+#include "freescape/games/3dck/3dck.h"
+
+namespace Freescape {
+
+static void requireBytes(Common::SeekableReadStream &file, uint32 size) {
+ if (file.err() || file.pos() > file.size() || size > file.size() - file.pos())
+ error("Truncated 3D Construction Kit data");
+}
+
+static uint16 readBlockSize(Common::SeekableReadStream &file) {
+ requireBytes(file, 2);
+ uint16 size = file.readUint16LE();
+ requireBytes(file, size);
+ return size;
+}
+
+static Math::Vector3d readVector(Common::SeekableReadStream &file) {
+ uint16 x = file.readUint16BE();
+ uint16 y = file.readUint16BE();
+ uint16 z = file.readUint16BE();
+ return Math::Vector3d(x, y, z);
+}
+
+KitEngine::KitEngine(OSystem *syst, const ADGameDescription *gd) : FreescapeEngine(syst, gd), _initialPlayerHeight(0) {
+ _renderMode = Common::kRenderVGA;
+ _screenW = 320;
+ _screenH = 200;
+ _fullscreenViewArea = Common::Rect(_screenW, _screenH);
+ _playerHeightNumber = 0;
+ _playerHeightMaxNumber = 0;
+ _playerWidth = 12;
+ _playerDepth = 32;
+ _soundIndexShoot = -1;
+}
+
+void KitEngine::loadAssets() {
+ Common::File file;
+ if (!file.open(_gameDescription->filesDescriptions[0].fileName))
+ error("Unable to open 3D Construction Kit RUN file");
+
+ char description[80];
+ if (file.read(description, sizeof(description)) != sizeof(description) ||
+ memcmp(description + 75, "VGA\x1a", 5) != 0)
+ error("Unsupported 3D Construction Kit RUN format");
+
+ // RUN blocks use little-endian lengths; the world data is big-endian.
+ uint16 worldSize = readBlockSize(file);
+ uint32 worldEnd = file.pos() + worldSize;
+ Common::SeekableSubReadStream world(&file, file.pos(), worldEnd);
+ loadWorld(world);
+ file.seek(worldEnd);
+
+ uint16 menuSize = readBlockSize(file);
+ file.skip(menuSize);
+ uint16 borderSize = readBlockSize(file);
+ if (borderSize != _screenW * _screenH)
+ error("Invalid 3D Construction Kit border size");
+ _border = new Graphics::ManagedSurface();
+ _border->create(_screenW, _screenH, Graphics::PixelFormat::createFormatCLUT8());
+ for (int y = 0; y < _screenH; y++) {
+ if (file.read(_border->getBasePtr(0, y), _screenW) != uint(_screenW))
+ error("Truncated 3D Construction Kit border");
+ }
+
+ if (file.read(_palette, sizeof(_palette)) != sizeof(_palette))
+ error("Truncated 3D Construction Kit palette");
+ for (uint i = 0; i < sizeof(_palette); i++) {
+ byte component = _palette[i] & 0x3f;
+ _palette[i] = (component << 2) | (component >> 4);
+ }
+ _border->setPalette(_palette, 0, 256);
+ _gfx->_palette = _palette;
+ _gfx->_keyColor = 0;
+}
+
+void KitEngine::loadWorld(Common::SeekableReadStream &file) {
+ requireBytes(file, 500);
+ uint32 signature = file.readUint32BE();
+ if (signature != MKTAG('C', 'P', 0, 0) && signature != MKTAG('C', 'P', '0', '1'))
+ error("Unsupported 3D Construction Kit world format");
+ uint16 areaCount = file.readUint16BE();
+ uint32 globalConditions = 2 * file.readUint16BE();
+ uint16 centerX = file.readUint16BE();
+ uint16 centerY = file.readUint16BE();
+ uint16 halfWidth = file.readUint16BE();
+ uint16 halfHeight = file.readUint16BE();
+ if (!halfWidth || !halfHeight || centerX < halfWidth || centerY < halfHeight ||
+ centerX + halfWidth > _screenW || centerY + halfHeight >= _screenH)
+ error("Invalid 3D Construction Kit viewport");
+ int top = _screenH - 1 - centerY - halfHeight;
+ _viewArea = Common::Rect(centerX - halfWidth, top, centerX + halfWidth, top + 2 * halfHeight);
+
+ // RUNVGA derives its projection scales from the viewport dimensions.
+ int xScale = 74 * (_viewArea.height() - 1) / 256;
+ int yScale = 55 * (_viewArea.width() - 1) / 256;
+ if (!xScale || !yScale)
+ error("3D Construction Kit viewport is too small");
+ _fieldOfView = 2.0f * Math::rad2deg(atan(24.0f / xScale));
+ _viewAspectRatio = float(yScale) / xScale;
+
+ file.skip(10);
+ _maxFallingDistance = file.readUint16BE();
+ _stepUpDistance = file.readUint16BE();
+ _startArea = file.readUint16BE();
+ _startEntrance = file.readUint16BE();
+ _initialPlayerHeight = file.readUint16BE();
+ uint16 step = file.readUint16BE();
+ uint16 angle = file.readUint16BE();
+ uint16 vehicle = file.readUint16BE();
+ if (!_initialPlayerHeight || !step || !angle || angle > 90 || vehicle)
+ error("Unsupported 3D Construction Kit player settings");
+ _playerSteps.clear();
+ _playerSteps.push_back(step);
+ _playerStepIndex = 0;
+ _angleRotations.clear();
+ _angleRotations.push_back(angle);
+ _angleRotationIndex = 0;
+
+ file.seek(500);
+ requireBytes(file, uint32(areaCount) * 4);
+ Common::Array<uint32> areaOffsets;
+ for (uint i = 0; i < areaCount; i++) {
+ uint32 offset = file.readUint32BE();
+ if (offset > uint32(file.size()) / 2)
+ error("Invalid 3D Construction Kit area offset");
+ areaOffsets.push_back(2 * offset);
+ }
+ if (globalConditions < uint32(file.pos()) || globalConditions > uint32(file.size()))
+ error("Invalid 3D Construction Kit global condition offset");
+ file.seek(globalConditions);
+ requireBytes(file, 2);
+ if (file.readUint16BE())
+ error("3D Construction Kit conditions are not implemented yet");
+
+ for (uint i = 0; i < areaOffsets.size(); i++) {
+ if (areaOffsets[i] < globalConditions + 2)
+ error("Invalid 3D Construction Kit area offset");
+ file.seek(areaOffsets[i]);
+ Area *area = loadArea(file);
+ uint16 id = area->getAreaID();
+ if (_areaMap.contains(id))
+ error("Duplicate 3D Construction Kit area %u", id);
+ _areaMap[id] = area;
+ }
+ if (!_areaMap.contains(_startArea) || !_areaMap[_startArea]->entranceWithID(_startEntrance))
+ error("Invalid 3D Construction Kit starting area or entrance");
+}
+
+Area *KitEngine::loadArea(Common::SeekableReadStream &file) {
+ uint32 start = file.pos();
+ requireBytes(file, 30);
+ uint16 flags = file.readUint16BE();
+ uint16 objectCount = file.readUint16BE();
+ uint16 id = file.readUint16BE();
+ file.skip(2);
+ uint32 conditions = start + 2 * file.readUint16BE();
+ uint16 scale = file.readUint16BE();
+ uint16 sky = file.readUint16BE();
+ uint16 ground = file.readUint16BE();
+ file.skip(14);
+ if (!scale || scale > 255 || conditions < uint32(file.pos()) || conditions > uint32(file.size()))
+ error("Invalid 3D Construction Kit area header");
+ if (id == 255 && objectCount)
+ error("3D Construction Kit global objects are not implemented yet");
+
+ ObjectMap *objects = new ObjectMap();
+ ObjectMap *entrances = new ObjectMap();
+ Common::SeekableSubReadStream objectData(&file, file.pos(), conditions);
+ for (uint i = 0; i < objectCount; i++) {
+ Object *obj = loadObject(objectData);
+ obj->scale(scale);
+ obj->_loadIndex = i;
+ ObjectMap *map = obj->getType() == kEntranceType ? entrances : objects;
+ if (map->contains(obj->getObjectID()))
+ error("Duplicate 3D Construction Kit object %u", obj->getObjectID());
+ (*map)[obj->getObjectID()] = obj;
+ }
+ if (objectData.pos() != objectData.size())
+ error("Invalid 3D Construction Kit object count");
+ file.seek(conditions);
+ requireBytes(file, 2);
+ if (file.readUint16BE())
+ error("3D Construction Kit conditions are not implemented yet");
+
+ Area *area = new Area(id, flags, objects, entrances, false);
+ area->_scale = scale;
+ area->_name = Common::String::format("AREA %u", id);
+ area->_skyColor = ((sky & 0xf) << 4) | ((sky >> 8) & 0xf);
+ area->_groundColor = ((ground & 0xf) << 4) | ((ground >> 8) & 0xf);
+ area->_usualBackgroundColor = 0;
+ area->_underFireBackgroundColor = 0;
+ return area;
+}
+
+Object *KitEngine::loadObject(Common::SeekableReadStream &file) {
+ requireBytes(file, 20);
+ byte flags = file.readByte();
+ byte type = file.readByte();
+ file.skip(2);
+ Math::Vector3d origin = readVector(file);
+ Math::Vector3d size = readVector(file);
+ uint16 id = file.readUint16BE();
+ uint16 words = file.readUint16BE();
+ if (words < 10)
+ error("Invalid 3D Construction Kit object size");
+ requireBytes(file, 2 * (words - 10));
+
+ if (type == kEntranceType && words == 10)
+ return new Entrance(id & 0x7fff, origin, size, FCLInstructionVector(), "");
+ if (type != kCubeType || words != 13 || (flags & 0x80))
+ error("Unsupported 3D Construction Kit object %u (type %u)", id, type);
+
+ Common::Array<uint8> *colors = new Common::Array<uint8>();
+ for (uint i = 0; i < 3; i++) {
+ // Each word interleaves the nibbles of two VGA palette indices.
+ uint16 pair = file.readUint16BE();
+ colors->push_back(((pair & 0xf) << 4) | ((pair >> 8) & 0xf));
+ colors->push_back((pair & 0xf0) | ((pair >> 12) & 0xf));
+ }
+ return new GeometricObject(kCubeType, id, (flags & 4) ? 0x80 : 0,
+ origin, size, colors, nullptr, nullptr, FCLInstructionVector());
+}
+
+void KitEngine::initGameState() {
+ FreescapeEngine::initGameState();
+ _playerHeight = _initialPlayerHeight;
+}
+
+void KitEngine::gotoArea(uint16 areaID, int entranceID) {
+ if (!_areaMap.contains(areaID))
+ error("Unknown 3D Construction Kit area %u", areaID);
+ _currentArea = _areaMap[areaID];
+ Entrance *entrance = static_cast<Entrance *>(_currentArea->entranceWithID(entranceID));
+ if (!entrance)
+ error("Unknown 3D Construction Kit entrance %d", entranceID);
+ _position = entrance->getOrigin();
+ _position.y() += _playerHeight;
+ Math::Vector3d rotation = entrance->getRotation();
+ _pitch = rotation.x();
+ _yaw = 90.0f - rotation.y();
+ _roll = rotation.z();
+ _lastPosition = _position;
+ _gfx->_scale = _currentArea->getScale();
+ _gotoExecuted = true;
+ resetInput();
+}
+
+void KitEngine::checkIfStillInArea() {
+ float limit = 8192.0f / _currentArea->getScale();
+ _position.x() = CLIP(_position.x(), 0.0f, limit);
+ _position.z() = CLIP(_position.z(), 0.0f, limit);
+}
+
+bool KitEngine::checkIfGameEnded() {
+ if (_hasFallen || _playerWasCrushed)
+ _gameStateControl = kFreescapeGameStateRestart;
+ return false;
+}
+
+void KitEngine::drawUI() {
+ _gfx->setViewport(_fullscreenViewArea);
+ _gfx->renderCrossair(_crossairPosition);
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/games/3dck/3dck.h b/engines/freescape/games/3dck/3dck.h
new file mode 100644
index 00000000000..2fb235baa88
--- /dev/null
+++ b/engines/freescape/games/3dck/3dck.h
@@ -0,0 +1,54 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef FREESCAPE_GAMES_3DCK_H
+#define FREESCAPE_GAMES_3DCK_H
+
+#include "freescape/freescape.h"
+
+namespace Freescape {
+
+class KitEngine : public FreescapeEngine {
+public:
+ KitEngine(OSystem *syst, const ADGameDescription *gd);
+
+ void loadAssets() override;
+ void initGameState() override;
+ void gotoArea(uint16 areaID, int entranceID) override;
+ void checkIfStillInArea() override;
+ bool checkIfGameEnded() override;
+ void borderScreen() override {}
+ void drawUI() override;
+ bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override { return false; }
+ bool canSaveGameStateCurrently(Common::U32String *msg = nullptr) override { return false; }
+
+private:
+ void loadWorld(Common::SeekableReadStream &file);
+ Area *loadArea(Common::SeekableReadStream &file);
+ Object *loadObject(Common::SeekableReadStream &file);
+
+ byte _palette[256 * 3];
+ uint16 _initialPlayerHeight;
+};
+
+} // namespace Freescape
+
+#endif
diff --git a/engines/freescape/games/palettes.cpp b/engines/freescape/games/palettes.cpp
index 062f1c0498c..639a7144138 100644
--- a/engines/freescape/games/palettes.cpp
+++ b/engines/freescape/games/palettes.cpp
@@ -144,6 +144,9 @@ byte kDrillerCPCPalette[32][3] = {
void FreescapeEngine::loadColorPalette() {
if (_renderMode == Common::kRenderEGA) {
_gfx->_palette = (byte *)&kEGADefaultPalette;
+ } else if (_renderMode == Common::kRenderVGA) {
+ if (!_gfx->_palette)
+ error("Missing VGA palette");
} else if (_renderMode == Common::kRenderC64) {
_gfx->_palette = (byte *)&kC64Palette;
} else if (_renderMode == Common::kRenderZX) {
diff --git a/engines/freescape/gfx.cpp b/engines/freescape/gfx.cpp
index 57a9b6669c9..bcfa6e3c884 100644
--- a/engines/freescape/gfx.cpp
+++ b/engines/freescape/gfx.cpp
@@ -640,7 +640,13 @@ bool Renderer::getRGBAt(uint8 index, uint8 ecolor, uint8 &r1, uint8 &g1, uint8 &
return true;
}
- if (_renderMode == Common::kRenderAmiga || _renderMode == Common::kRenderAtariST) {
+ if (_renderMode == Common::kRenderVGA) {
+ readFromPalette(index, r1, g1, b1);
+ r2 = r1;
+ g2 = g1;
+ b2 = b1;
+ return true;
+ } else if (_renderMode == Common::kRenderAmiga || _renderMode == Common::kRenderAtariST) {
// Hardware palette cycling: if the main color index matches the cycling
// palette entry and cycling is active, use the cycling color directly.
// This must happen BEFORE color pair resolution since on real hardware
diff --git a/engines/freescape/metaengine.cpp b/engines/freescape/metaengine.cpp
index 5d08dc4d5ee..7ca7b9ed389 100644
--- a/engines/freescape/metaengine.cpp
+++ b/engines/freescape/metaengine.cpp
@@ -31,6 +31,7 @@
#include "freescape/games/dark/dark.h"
#include "freescape/games/driller/driller.h"
#include "freescape/games/eclipse/eclipse.h"
+#include "freescape/games/3dck/3dck.h"
#include "freescape/detection.h"
@@ -208,6 +209,8 @@ Common::Error FreescapeMetaEngine::createInstance(OSystem *syst, Engine **engine
*engine = (Engine *)new Freescape::EclipseEngine(syst, gd);
} else if (Common::String(gd->gameId) == "castlemaster" || Common::String(gd->gameId) == "castlemaster2") {
*engine = (Engine *)new Freescape::CastleEngine(syst, gd);
+ } else if (Common::String(gd->gameId) == "3dkit") {
+ *engine = new Freescape::KitEngine(syst, gd);
} else
*engine = new Freescape::FreescapeEngine(syst, gd);
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index cf4c1f7cbee..933e7c7e52e 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -52,6 +52,7 @@ MODULE_OBJS := \
games/eclipse/opl.music.o \
games/eclipse/cpc.o \
games/eclipse/zx.o \
+ games/3dck/3dck.o \
games/palettes.o \
gfx.o \
loaders/8bitImage.o \
diff --git a/engines/freescape/movement.cpp b/engines/freescape/movement.cpp
index c6753ab517b..e699693df56 100644
--- a/engines/freescape/movement.cpp
+++ b/engines/freescape/movement.cpp
@@ -317,8 +317,8 @@ void FreescapeEngine::shoot() {
float ndcY = 1.0f - (2.0f * (_crossairPosition.y - _viewArea.top) / _viewArea.height());
// Calculate angular offsets using perspective projection
- float fovHorizontalRad = (float)(75.0f * M_PI / 180.0f);
- float aspectRatio = isCastle() ? 1.6 : 2.18;
+ float fovHorizontalRad = Math::deg2rad(_fieldOfView);
+ float aspectRatio = _viewAspectRatio;
float fovVerticalRad = 2.0f * atan(tan(fovHorizontalRad / 2.0f) / aspectRatio);
// Convert NDC to angle offset
diff --git a/engines/freescape/ui.cpp b/engines/freescape/ui.cpp
index ea85db50f24..88fb63133fd 100644
--- a/engines/freescape/ui.cpp
+++ b/engines/freescape/ui.cpp
@@ -86,8 +86,8 @@ void FreescapeEngine::waitInLoop(int maxWait) {
if (_currentArea->isOutside())
farClipPlane *= 100;
- const float fov = 75.0f;
- float aspectRatio = isCastle() ? 1.6 : 2.18;
+ const float fov = _fieldOfView;
+ float aspectRatio = _viewAspectRatio;
_gfx->updateProjectionMatrix(fov, aspectRatio, _nearClipPlane, farClipPlane);
_gfx->positionCamera(_position, _position + _cameraFront, _roll);
Commit: be883d1a563a20b3e992ebd401d96b2e3a96b543
https://github.com/scummvm/scummvm/commit/be883d1a563a20b3e992ebd401d96b2e3a96b543
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-07T07:23:53+02:00
Commit Message:
FREESCAPE: allow to load geometry of the 3DCK sample game
Changed paths:
engines/freescape/detection.cpp
engines/freescape/games/3dck/3dck.cpp
engines/freescape/games/3dck/3dck.h
diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index 32d1e535374..17be9887e18 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -1171,8 +1171,8 @@ const ADGameDescription gameDescriptions[] = {
AD_ENTRY1s("3DKIT.RUN", "f35147729a2f5b6852a504223aeb6a57", 112158),
Common::EN_ANY,
Common::kPlatformDOS,
- ADGF_UNSUPPORTED,
- GUIO1(GUIO_NOMIDI)
+ ADGF_UNSTABLE,
+ GUIO2(GUIO_NOMIDI, GUIO_RENDERVGA)
},
{
"3dkit",
diff --git a/engines/freescape/games/3dck/3dck.cpp b/engines/freescape/games/3dck/3dck.cpp
index 8957d71e7ea..517ce2b8137 100644
--- a/engines/freescape/games/3dck/3dck.cpp
+++ b/engines/freescape/games/3dck/3dck.cpp
@@ -19,6 +19,7 @@
*
*/
+#include "common/algorithm.h"
#include "common/substream.h"
#include "math/utils.h"
@@ -26,6 +27,12 @@
namespace Freescape {
+enum {
+ kKitAnimatorType = 16,
+ kKitInitiallyInvisible = 0x04,
+ kKitMovable = 0x80
+};
+
static void requireBytes(Common::SeekableReadStream &file, uint32 size) {
if (file.err() || file.pos() > file.size() || size > file.size() - file.pos())
error("Truncated 3D Construction Kit data");
@@ -39,12 +46,37 @@ static uint16 readBlockSize(Common::SeekableReadStream &file) {
}
static Math::Vector3d readVector(Common::SeekableReadStream &file) {
- uint16 x = file.readUint16BE();
- uint16 y = file.readUint16BE();
- uint16 z = file.readUint16BE();
+ int16 x = file.readSint16BE();
+ int16 y = file.readSint16BE();
+ int16 z = file.readSint16BE();
return Math::Vector3d(x, y, z);
}
+static Common::Array<uint16> readWords(Common::SeekableReadStream &file, uint32 count) {
+ requireBytes(file, 2 * count);
+ Common::Array<uint16> words;
+ for (uint32 i = 0; i < count; i++)
+ words.push_back(file.readUint16BE());
+ return words;
+}
+
+static Common::Array<byte> readCode(Common::SeekableReadStream &file, uint32 size) {
+ requireBytes(file, size);
+ Common::Array<byte> code;
+ code.resize(size);
+ if (size && file.read(code.data(), size) != size)
+ error("Truncated 3D Construction Kit condition");
+ return code;
+}
+
+static void readColors(Common::SeekableReadStream &file, byte &first, byte &second) {
+ requireBytes(file, 2);
+ // Each word interleaves the nibbles of two VGA palette indices.
+ uint16 pair = file.readUint16BE();
+ first = ((pair & 0xf) << 4) | ((pair >> 8) & 0xf);
+ second = (pair & 0xf0) | ((pair >> 12) & 0xf);
+}
+
KitEngine::KitEngine(OSystem *syst, const ADGameDescription *gd) : FreescapeEngine(syst, gd), _initialPlayerHeight(0) {
_renderMode = Common::kRenderVGA;
_screenW = 320;
@@ -140,42 +172,74 @@ void KitEngine::loadWorld(Common::SeekableReadStream &file) {
_angleRotations.push_back(angle);
_angleRotationIndex = 0;
+ file.skip(2);
+ uint32 indicatorOffset = 2 * file.readUint16BE();
+ uint16 indicatorCount = file.readUint16BE();
+ if (indicatorOffset > uint32(file.size()) || (!indicatorOffset && indicatorCount))
+ error("Invalid 3D Construction Kit indicator offset");
+ uint32 areasEnd = indicatorOffset ? indicatorOffset : file.size();
+ if (indicatorCount) {
+ file.seek(indicatorOffset);
+ // The indicator table follows the last area's conditions.
+ _indicatorData = readWords(file, 17 * indicatorCount);
+ }
+
file.seek(500);
requireBytes(file, uint32(areaCount) * 4);
Common::Array<uint32> areaOffsets;
for (uint i = 0; i < areaCount; i++) {
uint32 offset = file.readUint32BE();
- if (offset > uint32(file.size()) / 2)
+ if (offset >= areasEnd / 2)
error("Invalid 3D Construction Kit area offset");
areaOffsets.push_back(2 * offset);
}
- if (globalConditions < uint32(file.pos()) || globalConditions > uint32(file.size()))
+ Common::sort(areaOffsets.begin(), areaOffsets.end());
+ if (areaOffsets.empty() || globalConditions < uint32(file.pos()) ||
+ globalConditions + 2 > areaOffsets.front())
error("Invalid 3D Construction Kit global condition offset");
- file.seek(globalConditions);
- requireBytes(file, 2);
- if (file.readUint16BE())
- error("3D Construction Kit conditions are not implemented yet");
+ Common::SeekableSubReadStream conditionData(&file, globalConditions, areaOffsets.front());
+ _globalConditions = loadConditions(conditionData);
+ if (conditionData.pos() != conditionData.size())
+ error("Invalid 3D Construction Kit global condition size");
for (uint i = 0; i < areaOffsets.size(); i++) {
- if (areaOffsets[i] < globalConditions + 2)
+ uint32 end = i + 1 < areaOffsets.size() ? areaOffsets[i + 1] : areasEnd;
+ if (areaOffsets[i] >= end)
error("Invalid 3D Construction Kit area offset");
- file.seek(areaOffsets[i]);
- Area *area = loadArea(file);
+ Common::SeekableSubReadStream areaData(&file, areaOffsets[i], end);
+ Area *area = loadArea(areaData);
uint16 id = area->getAreaID();
- if (_areaMap.contains(id))
- error("Duplicate 3D Construction Kit area %u", id);
_areaMap[id] = area;
}
if (!_areaMap.contains(_startArea) || !_areaMap[_startArea]->entranceWithID(_startEntrance))
error("Invalid 3D Construction Kit starting area or entrance");
}
+Common::Array<KitEngine::ConditionData> KitEngine::loadConditions(Common::SeekableReadStream &file) {
+ requireBytes(file, 2);
+ uint16 count = file.readUint16BE();
+ Common::Array<ConditionData> conditions;
+ for (uint i = 0; i < count; i++) {
+ requireBytes(file, 14);
+ char name[13] = {};
+ file.read(name, 12);
+ uint16 words = file.readUint16BE() & 0x7fff;
+ ConditionData condition;
+ condition.name = name;
+ condition.code = readCode(file, 2 * words);
+ conditions.push_back(condition);
+ }
+ return conditions;
+}
+
Area *KitEngine::loadArea(Common::SeekableReadStream &file) {
uint32 start = file.pos();
requireBytes(file, 30);
uint16 flags = file.readUint16BE();
uint16 objectCount = file.readUint16BE();
uint16 id = file.readUint16BE();
+ if (_areaMap.contains(id))
+ error("Duplicate 3D Construction Kit area %u", id);
file.skip(2);
uint32 conditions = start + 2 * file.readUint16BE();
uint16 scale = file.readUint16BE();
@@ -184,16 +248,22 @@ Area *KitEngine::loadArea(Common::SeekableReadStream &file) {
file.skip(14);
if (!scale || scale > 255 || conditions < uint32(file.pos()) || conditions > uint32(file.size()))
error("Invalid 3D Construction Kit area header");
- if (id == 255 && objectCount)
- error("3D Construction Kit global objects are not implemented yet");
+ AreaData &data = _areaData[id];
ObjectMap *objects = new ObjectMap();
ObjectMap *entrances = new ObjectMap();
Common::SeekableSubReadStream objectData(&file, file.pos(), conditions);
for (uint i = 0; i < objectCount; i++) {
- Object *obj = loadObject(objectData);
- obj->scale(scale);
- obj->_loadIndex = i;
+ ObjectData record;
+ Object *obj = loadObject(objectData, record);
+ if (data.objects.contains(record.id))
+ error("Duplicate 3D Construction Kit object %u in area %u", record.id, id);
+ data.objects[record.id] = record;
+ if (!obj)
+ continue;
+ if (id != 255)
+ obj->scale(scale);
+ obj->_loadIndex = (id == 255 ? 0 : 0x4000) + i;
ObjectMap *map = obj->getType() == kEntranceType ? entrances : objects;
if (map->contains(obj->getObjectID()))
error("Duplicate 3D Construction Kit object %u", obj->getObjectID());
@@ -202,9 +272,10 @@ Area *KitEngine::loadArea(Common::SeekableReadStream &file) {
if (objectData.pos() != objectData.size())
error("Invalid 3D Construction Kit object count");
file.seek(conditions);
- requireBytes(file, 2);
- if (file.readUint16BE())
- error("3D Construction Kit conditions are not implemented yet");
+ data.conditions = loadConditions(file);
+ if (file.pos() != file.size())
+ error("Invalid 3D Construction Kit area condition size");
+ debugC(1, kFreescapeDebugParser, "3DCK area %u: %u objects, %u conditions", id, objectCount, data.conditions.size());
Area *area = new Area(id, flags, objects, entrances, false);
area->_scale = scale;
@@ -213,36 +284,92 @@ Area *KitEngine::loadArea(Common::SeekableReadStream &file) {
area->_groundColor = ((ground & 0xf) << 4) | ((ground >> 8) & 0xf);
area->_usualBackgroundColor = 0;
area->_underFireBackgroundColor = 0;
+ // The runner supplies a default floor at Y=0.
+ if (id != 255)
+ area->addFloor();
return area;
}
-Object *KitEngine::loadObject(Common::SeekableReadStream &file) {
+Object *KitEngine::loadObject(Common::SeekableReadStream &file, ObjectData &data) {
requireBytes(file, 20);
- byte flags = file.readByte();
- byte type = file.readByte();
- file.skip(2);
- Math::Vector3d origin = readVector(file);
- Math::Vector3d size = readVector(file);
- uint16 id = file.readUint16BE();
+ data.flags = file.readByte();
+ data.type = file.readByte() & 0x7f;
+ data.state = file.readUint16BE();
+ data.origin = readVector(file);
+ data.size = readVector(file);
+ data.initialOrigin = data.origin;
+ data.id = file.readUint16BE();
uint16 words = file.readUint16BE();
if (words < 10)
error("Invalid 3D Construction Kit object size");
requireBytes(file, 2 * (words - 10));
+ uint32 end = file.pos() + 2 * (words - 10);
+ Common::SeekableSubReadStream payload(&file, file.pos(), end);
+ if (data.type > kKitAnimatorType)
+ error("Unsupported 3D Construction Kit object %u (type %u)", data.id, data.type);
+
+ bool geometric = data.type >= kCubeType && data.type <= kHexagonType && data.type != kSensorType;
+ Common::Array<uint8> *colors = nullptr;
+ Common::Array<float> *ordinates = nullptr;
+ if (geometric) {
+ ObjectType type = ObjectType(data.type);
+ int colorCount = GeometricObject::numberOfColoursForObjectOfType(type);
+ colors = new Common::Array<uint8>();
+ for (int i = 0; i < colorCount; i += 2) {
+ byte first, second;
+ readColors(payload, first, second);
+ colors->push_back(first);
+ colors->push_back(second);
+ }
+ int ordinateCount = GeometricObject::numberOfOrdinatesForType(type);
+ if (ordinateCount) {
+ requireBytes(payload, 2 * ordinateCount);
+ ordinates = new Common::Array<float>();
+ for (int i = 0; i < ordinateCount; i++)
+ ordinates->push_back(payload.readSint16BE());
+ }
+ } else if (data.type == kSensorType) {
+ requireBytes(payload, 10);
+ readColors(payload, data.sensor.colors[0], data.sensor.colors[1]);
+ data.sensor.interval = payload.readUint16BE();
+ data.sensor.range = payload.readUint16BE();
+ data.sensor.unknown = payload.readUint16BE();
+ data.sensor.directions = payload.readUint16BE();
+ } else if (data.type == kKitAnimatorType) {
+ data.extra = readWords(payload, 3);
+ }
+
+ if (data.flags & kKitMovable) {
+ requireBytes(payload, 6);
+ data.initialOrigin = readVector(payload);
+ }
+ if (data.type == kGroupType || (data.type == kEntranceType && data.id == 255)) {
+ data.members = readWords(payload, data.state);
+ if (payload.pos() != payload.size())
+ error("Invalid 3D Construction Kit object list %u", data.id);
+ } else if (data.type == kEntranceType) {
+ // Entrances can retain editor data after their header.
+ data.extra = readWords(payload, (payload.size() - payload.pos()) / 2);
+ } else {
+ data.code = readCode(payload, payload.size() - payload.pos());
+ }
+ file.seek(end);
+ debugC(1, kFreescapeDebugParser, "3DCK object %u: type %u, flags %02x, %u script bytes",
+ data.id, data.type, data.flags, data.code.size());
+
+ if (data.type == kEntranceType && data.id != 255)
+ return new Entrance(data.id & 0x7fff, data.initialOrigin, data.size, FCLInstructionVector(), "");
+ if (!geometric)
+ return nullptr;
- if (type == kEntranceType && words == 10)
- return new Entrance(id & 0x7fff, origin, size, FCLInstructionVector(), "");
- if (type != kCubeType || words != 13 || (flags & 0x80))
- error("Unsupported 3D Construction Kit object %u (type %u)", id, type);
-
- Common::Array<uint8> *colors = new Common::Array<uint8>();
- for (uint i = 0; i < 3; i++) {
- // Each word interleaves the nibbles of two VGA palette indices.
- uint16 pair = file.readUint16BE();
- colors->push_back(((pair & 0xf) << 4) | ((pair >> 8) & 0xf));
- colors->push_back((pair & 0xf0) | ((pair >> 12) & 0xf));
+ ObjectType type = ObjectType(data.type);
+ if (GeometricObject::isPolygon(type)) {
+ // Polygon vertices are relative; pyramid ordinates are already offsets.
+ for (uint i = 0; i < ordinates->size(); i++)
+ (*ordinates)[i] += data.initialOrigin.getValue(i % 3);
}
- return new GeometricObject(kCubeType, id, (flags & 4) ? 0x80 : 0,
- origin, size, colors, nullptr, nullptr, FCLInstructionVector());
+ return new GeometricObject(type, data.id, (data.flags & kKitInitiallyInvisible) ? 0x80 : 0,
+ data.initialOrigin, data.size, colors, nullptr, ordinates, FCLInstructionVector());
}
void KitEngine::initGameState() {
diff --git a/engines/freescape/games/3dck/3dck.h b/engines/freescape/games/3dck/3dck.h
index 2fb235baa88..fb58fbdda91 100644
--- a/engines/freescape/games/3dck/3dck.h
+++ b/engines/freescape/games/3dck/3dck.h
@@ -41,9 +41,44 @@ public:
bool canSaveGameStateCurrently(Common::U32String *msg = nullptr) override { return false; }
private:
+ struct ConditionData {
+ Common::String name;
+ Common::Array<byte> code;
+ };
+
+ struct SensorData {
+ byte colors[2] = {};
+ uint16 interval = 0;
+ uint16 range = 0;
+ uint16 unknown = 0;
+ uint16 directions = 0;
+ };
+
+ struct ObjectData {
+ uint16 id = 0;
+ byte type = 0;
+ byte flags = 0;
+ uint16 state = 0;
+ Math::Vector3d origin, size, initialOrigin;
+ Common::Array<uint16> members;
+ Common::Array<uint16> extra;
+ Common::Array<byte> code;
+ SensorData sensor;
+ };
+
+ struct AreaData {
+ Common::HashMap<uint16, ObjectData> objects;
+ Common::Array<ConditionData> conditions;
+ };
+
void loadWorld(Common::SeekableReadStream &file);
Area *loadArea(Common::SeekableReadStream &file);
- Object *loadObject(Common::SeekableReadStream &file);
+ Object *loadObject(Common::SeekableReadStream &file, ObjectData &data);
+ Common::Array<ConditionData> loadConditions(Common::SeekableReadStream &file);
+
+ Common::HashMap<uint16, AreaData> _areaData;
+ Common::Array<ConditionData> _globalConditions;
+ Common::Array<uint16> _indicatorData;
byte _palette[256 * 3];
uint16 _initialPlayerHeight;
Commit: f65035d8f055907b35be2e0d913fe700ba5ea972
https://github.com/scummvm/scummvm/commit/f65035d8f055907b35be2e0d913fe700ba5ea972
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-07T07:23:53+02:00
Commit Message:
FREESCAPE: initial implementation of 3DCK opcodes and scripting
Changed paths:
A engines/freescape/games/3dck/ui.cpp
A engines/freescape/language/16bitDetokeniser.cpp
A engines/freescape/language/16bitDetokeniser.h
A engines/freescape/language/instruction16bit.cpp
A engines/freescape/language/instruction16bit.h
engines/freescape/freescape.cpp
engines/freescape/freescape.h
engines/freescape/games/3dck/3dck.cpp
engines/freescape/games/3dck/3dck.h
engines/freescape/language/8bitDetokeniser.h
engines/freescape/language/instruction.cpp
engines/freescape/language/instruction.h
engines/freescape/language/token.h
engines/freescape/module.mk
diff --git a/engines/freescape/freescape.cpp b/engines/freescape/freescape.cpp
index 8928e213545..09d618480ef 100644
--- a/engines/freescape/freescape.cpp
+++ b/engines/freescape/freescape.cpp
@@ -940,6 +940,9 @@ void FreescapeEngine::processInput() {
continue;
}
+ if (handleInput(event))
+ continue;
+
switch (event.type) {
case Common::EVENT_CUSTOM_ENGINE_ACTION_START:
if (_hasFallen || _playerWasCrushed)
@@ -1233,6 +1236,7 @@ Common::Error FreescapeEngine::run() {
checkSensors();
checkIfPlayerWasCrushed();
+ updateScripts();
drawFrame();
if (_shootingFrames == 0) {
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index f3798dfb4ce..c73ef2591ab 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -394,6 +394,7 @@ public:
virtual void initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *infoScreenKeyMap, const char *target);
EventManagerWrapper *_eventManager;
void processInput();
+ virtual bool handleInput(const Common::Event &event) { return false; }
void resetInput();
void stopMovement();
void generateDemoInput();
@@ -401,7 +402,7 @@ public:
virtual void releasedKey(const int keycode);
Common::Point getNormalizedPosition(Common::Point position);
virtual bool onScreenControls(Common::Point mouse);
- void updatePlayerMovement(float deltaTime);
+ virtual void updatePlayerMovement(float deltaTime);
void updatePlayerMovementSmooth(float deltaTime);
void updatePlayerMovementClassic(float deltaTime);
void resolveCollisions(Math::Vector3d newPosition);
@@ -477,13 +478,14 @@ public:
Math::Vector3d _objExecutingCodeSize;
bool _executingGlobalCode;
virtual void executeMovementConditions();
- bool executeObjectConditions(GeometricObject *obj, bool shot, bool collided, bool activated);
+ virtual bool executeObjectConditions(GeometricObject *obj, bool shot, bool collided, bool activated);
void executeEntranceConditions(Entrance *entrance);
- void executeLocalGlobalConditions(bool shot, bool collided, bool timer);
+ virtual void executeLocalGlobalConditions(bool shot, bool collided, bool timer);
+ virtual void updateScripts() {}
bool executeCode(FCLInstructionVector &code, bool shot, bool collided, bool timer, bool activated);
// Instructions
- bool checkConditional(FCLInstruction &instruction, bool shot, bool collided, bool timer, bool activated);
+ bool checkConditional(const FCLInstruction &instruction, bool shot, bool collided, bool timer, bool activated);
bool checkIfGreaterOrEqual(FCLInstruction &instruction);
bool checkIfLessOrEqual(FCLInstruction &instruction);
void executeExecute(FCLInstruction &instruction);
diff --git a/engines/freescape/games/3dck/3dck.cpp b/engines/freescape/games/3dck/3dck.cpp
index 517ce2b8137..b11e91cda74 100644
--- a/engines/freescape/games/3dck/3dck.cpp
+++ b/engines/freescape/games/3dck/3dck.cpp
@@ -24,6 +24,7 @@
#include "math/utils.h"
#include "freescape/games/3dck/3dck.h"
+#include "freescape/language/16bitDetokeniser.h"
namespace Freescape {
@@ -127,6 +128,8 @@ void KitEngine::loadAssets() {
_border->setPalette(_palette, 0, 256);
_gfx->_palette = _palette;
_gfx->_keyColor = 0;
+ _scriptSurface.create(_screenW, _screenH, _gfx->_texturePixelFormat);
+ _scriptSurface.fillRect(_fullscreenViewArea, 0);
}
void KitEngine::loadWorld(Common::SeekableReadStream &file) {
@@ -154,7 +157,9 @@ void KitEngine::loadWorld(Common::SeekableReadStream &file) {
_fieldOfView = 2.0f * Math::rad2deg(atan(24.0f / xScale));
_viewAspectRatio = float(yScale) / xScale;
- file.skip(10);
+ file.skip(6);
+ _timerInterval = file.readUint16BE();
+ _activationRange = file.readUint16BE();
_maxFallingDistance = file.readUint16BE();
_stepUpDistance = file.readUint16BE();
_startArea = file.readUint16BE();
@@ -175,6 +180,7 @@ void KitEngine::loadWorld(Common::SeekableReadStream &file) {
file.skip(2);
uint32 indicatorOffset = 2 * file.readUint16BE();
uint16 indicatorCount = file.readUint16BE();
+ _initialCondition = file.readUint16BE();
if (indicatorOffset > uint32(file.size()) || (!indicatorOffset && indicatorCount))
error("Invalid 3D Construction Kit indicator offset");
uint32 areasEnd = indicatorOffset ? indicatorOffset : file.size();
@@ -184,7 +190,8 @@ void KitEngine::loadWorld(Common::SeekableReadStream &file) {
_indicatorData = readWords(file, 17 * indicatorCount);
}
- file.seek(500);
+ file.seek(150);
+ _controlData = readWords(file, 35 * 5);
requireBytes(file, uint32(areaCount) * 4);
Common::Array<uint32> areaOffsets;
for (uint i = 0; i < areaCount; i++) {
@@ -199,6 +206,8 @@ void KitEngine::loadWorld(Common::SeekableReadStream &file) {
error("Invalid 3D Construction Kit global condition offset");
Common::SeekableSubReadStream conditionData(&file, globalConditions, areaOffsets.front());
_globalConditions = loadConditions(conditionData);
+ if (_initialCondition > _globalConditions.size())
+ error("Invalid 3D Construction Kit initial condition");
if (conditionData.pos() != conditionData.size())
error("Invalid 3D Construction Kit global condition size");
@@ -226,7 +235,8 @@ Common::Array<KitEngine::ConditionData> KitEngine::loadConditions(Common::Seekab
uint16 words = file.readUint16BE() & 0x7fff;
ConditionData condition;
condition.name = name;
- condition.code = readCode(file, 2 * words);
+ Common::String source = detokenise16bitCondition(readCode(file, 2 * words), condition.condition);
+ debugC(1, kFreescapeDebugParser, "3DCK condition %s:\n%s", name, source.c_str());
conditions.push_back(condition);
}
return conditions;
@@ -259,6 +269,7 @@ Area *KitEngine::loadArea(Common::SeekableReadStream &file) {
if (data.objects.contains(record.id))
error("Duplicate 3D Construction Kit object %u in area %u", record.id, id);
data.objects[record.id] = record;
+ data.objectOrder.push_back(record.id);
if (!obj)
continue;
if (id != 255)
@@ -351,11 +362,12 @@ Object *KitEngine::loadObject(Common::SeekableReadStream &file, ObjectData &data
// Entrances can retain editor data after their header.
data.extra = readWords(payload, (payload.size() - payload.pos()) / 2);
} else {
- data.code = readCode(payload, payload.size() - payload.pos());
+ Common::String source = detokenise16bitCondition(readCode(payload, payload.size() - payload.pos()), data.condition);
+ debugC(1, kFreescapeDebugParser, "3DCK object %u condition:\n%s", data.id, source.c_str());
}
file.seek(end);
- debugC(1, kFreescapeDebugParser, "3DCK object %u: type %u, flags %02x, %u script bytes",
- data.id, data.type, data.flags, data.code.size());
+ debugC(1, kFreescapeDebugParser, "3DCK object %u: type %u, flags %02x, %u instructions",
+ data.id, data.type, data.flags, data.condition.size());
if (data.type == kEntranceType && data.id != 255)
return new Entrance(data.id & 0x7fff, data.initialOrigin, data.size, FCLInstructionVector(), "");
@@ -375,11 +387,15 @@ Object *KitEngine::loadObject(Common::SeekableReadStream &file, ObjectData &data
void KitEngine::initGameState() {
FreescapeEngine::initGameState();
_playerHeight = _initialPlayerHeight;
+ resetScripts();
+ _currentArea = nullptr;
}
void KitEngine::gotoArea(uint16 areaID, int entranceID) {
if (!_areaMap.contains(areaID))
error("Unknown 3D Construction Kit area %u", areaID);
+ if (_currentArea)
+ _kitVariables[9] = _currentArea->getAreaID();
_currentArea = _areaMap[areaID];
Entrance *entrance = static_cast<Entrance *>(_currentArea->entranceWithID(entranceID));
if (!entrance)
@@ -393,7 +409,13 @@ void KitEngine::gotoArea(uint16 areaID, int entranceID) {
_lastPosition = _position;
_gfx->_scale = _currentArea->getScale();
_gotoExecuted = true;
+ _delayedShootObject = nullptr;
+ _timerTicks = 0;
+ _scriptSurface.fillRect(_viewArea, 0);
resetInput();
+ _shootMode = true;
+ g_system->lockMouse(false);
+ readSystemVariables();
}
void KitEngine::checkIfStillInArea() {
@@ -403,14 +425,7 @@ void KitEngine::checkIfStillInArea() {
}
bool KitEngine::checkIfGameEnded() {
- if (_hasFallen || _playerWasCrushed)
- _gameStateControl = kFreescapeGameStateRestart;
return false;
}
-void KitEngine::drawUI() {
- _gfx->setViewport(_fullscreenViewArea);
- _gfx->renderCrossair(_crossairPosition);
-}
-
} // namespace Freescape
diff --git a/engines/freescape/games/3dck/3dck.h b/engines/freescape/games/3dck/3dck.h
index fb58fbdda91..1dd0a9837d6 100644
--- a/engines/freescape/games/3dck/3dck.h
+++ b/engines/freescape/games/3dck/3dck.h
@@ -23,6 +23,7 @@
#define FREESCAPE_GAMES_3DCK_H
#include "freescape/freescape.h"
+#include "freescape/language/instruction16bit.h"
namespace Freescape {
@@ -37,13 +38,27 @@ public:
bool checkIfGameEnded() override;
void borderScreen() override {}
void drawUI() override;
+ bool handleInput(const Common::Event &event) override;
+ void updatePlayerMovement(float deltaTime) override;
+ void updateTimeVariables() override;
+ void updateScripts() override;
+ bool executeObjectConditions(GeometricObject *obj, bool shot, bool collided, bool activated) override;
+ void executeLocalGlobalConditions(bool shot, bool collided, bool timer) override {}
bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override { return false; }
bool canSaveGameStateCurrently(Common::U32String *msg = nullptr) override { return false; }
private:
+ struct ObjectData;
+ struct ScriptState : FCLExecutionState {
+ ObjectData *object = nullptr;
+ uint16 area = 0;
+ byte events = 0;
+ };
+
struct ConditionData {
Common::String name;
- Common::Array<byte> code;
+ FCLInstructionVector condition;
+ ScriptState script;
};
struct SensorData {
@@ -62,26 +77,66 @@ private:
Math::Vector3d origin, size, initialOrigin;
Common::Array<uint16> members;
Common::Array<uint16> extra;
- Common::Array<byte> code;
+ FCLInstructionVector condition;
SensorData sensor;
+ ScriptState script;
+ Common::Array<uint16> animatedObjects;
+ uint16 animator = 0;
};
struct AreaData {
Common::HashMap<uint16, ObjectData> objects;
+ Common::Array<uint16> objectOrder;
Common::Array<ConditionData> conditions;
};
+ struct ScriptEntry {
+ ScriptState *script;
+ bool resume;
+ ScriptEntry(ScriptState *s, bool r) : script(s), resume(r) {}
+ };
+
void loadWorld(Common::SeekableReadStream &file);
Area *loadArea(Common::SeekableReadStream &file);
Object *loadObject(Common::SeekableReadStream &file, ObjectData &data);
Common::Array<ConditionData> loadConditions(Common::SeekableReadStream &file);
+ void resetScripts();
+ void startScript(ScriptState &script);
+ void beginScriptFrame();
+ FCLExecutionResult executeCode(ScriptState &script, uint &budget);
+ void readSystemVariables();
+ void writeSystemVariables();
+ void setScriptVariable(byte index, uint32 value);
+ int32 getVariableOrConstant(int32 operand, Token::Type type) const;
+ void setScriptPredicate(ScriptState &script, bool value);
+ ObjectData *scriptObject(uint16 area, uint16 id);
+ void collectObjects(uint16 area, uint16 id, Common::Array<uint16> &objects);
+ void setObjectStatus(uint16 area, uint16 id, Token::Type operation);
+ bool moveAnimation(ScriptState &script, Math::Vector3d movement, bool absolute);
+ void interact(bool shot);
+ void printMessage(uint16 indicator, const Common::String &message);
+ void updateIndicators();
+ uint32 indicatorColor(byte color) const;
Common::HashMap<uint16, AreaData> _areaData;
Common::Array<ConditionData> _globalConditions;
Common::Array<uint16> _indicatorData;
+ Common::Array<uint16> _controlData;
byte _palette[256 * 3];
uint16 _initialPlayerHeight;
+ uint16 _initialCondition = 0, _timerInterval = 0, _activationRange = 0;
+ uint32 _kitVariables[256] = {};
+ uint32 _changedVariables = 0;
+ uint32 _scriptTicks = 0, _timerTicks = 0, _delayUntil = 0;
+ int _lastScriptTick = 0;
+ bool _timerTriggered = false, _initialScriptPending = false;
+ bool _scriptFrameActive = false, _scriptDelayed = false;
+ bool _soundWarning = false;
+ uint _scriptQueueIndex = 0;
+ Common::Array<ScriptEntry> _scriptQueue;
+ Common::Array<ScriptState *> _suspendedScripts;
+ Graphics::ManagedSurface _scriptSurface;
};
} // namespace Freescape
diff --git a/engines/freescape/games/3dck/ui.cpp b/engines/freescape/games/3dck/ui.cpp
new file mode 100644
index 00000000000..76757e87e9f
--- /dev/null
+++ b/engines/freescape/games/3dck/ui.cpp
@@ -0,0 +1,201 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "graphics/fonts/dosfont.h"
+#include "math/utils.h"
+
+#include "freescape/games/3dck/3dck.h"
+
+namespace Freescape {
+
+uint32 KitEngine::indicatorColor(byte color) const {
+ return _scriptSurface.format.ARGBToColor(255,
+ _palette[3 * color], _palette[3 * color + 1], _palette[3 * color + 2]);
+}
+
+void KitEngine::printMessage(uint16 indicator, const Common::String &message) {
+ if (!indicator || indicator > _indicatorData.size() / 17)
+ return;
+ const uint16 *data = &_indicatorData[17 * (indicator - 1)];
+ if (data[0] != 1 || data[1] >= _screenW || data[2] >= _screenH)
+ return;
+ Common::Rect rect(data[1], data[2], MIN<int>(data[1] + data[3], _screenW),
+ MIN<int>(data[2] + data[4], _screenH));
+ if (rect.isEmpty())
+ return;
+ Graphics::Surface surface = _scriptSurface.getSubArea(rect);
+ Graphics::DosFont font;
+ int x = 0, y = 0;
+ for (uint i = 0; i < message.size(); i++) {
+ byte chr = message[i];
+ if (chr == 0x1b && i + 1 < message.size()) {
+ chr = message[++i];
+ if (chr == 'N') {
+ x = 0;
+ y += 8;
+ continue;
+ }
+ if (chr >= 'A' && chr <= 'Z')
+ continue;
+ }
+ if (y >= surface.h)
+ break;
+ Common::Rect cell(x, y, MIN(x + 8, int(surface.w)), MIN(y + 8, int(surface.h)));
+ surface.fillRect(cell, indicatorColor(data[11]));
+ font.drawChar(&surface, chr, x, y, indicatorColor(data[10]));
+ x += 8;
+ if (x >= surface.w) {
+ x = 0;
+ y += 8;
+ }
+ }
+}
+
+void KitEngine::updateIndicators() {
+ Graphics::DosFont font;
+ for (uint i = 0; i < _indicatorData.size(); i += 17) {
+ const uint16 *data = &_indicatorData[i];
+ if (data[0] < 2 || data[0] > 4 || data[1] >= _screenW || data[2] >= _screenH)
+ continue;
+ Common::Rect rect(data[1], data[2], MIN<int>(data[1] + data[3], _screenW),
+ MIN<int>(data[2] + data[4], _screenH));
+ if (rect.isEmpty())
+ continue;
+ int32 first = int32((uint32(data[5]) << 16) | data[6]);
+ int32 last = int32((uint32(data[7]) << 16) | data[8]);
+ int32 value = CLIP<int32>(_kitVariables[data[9] & 0xff], MIN(first, last), MAX(first, last));
+ uint32 foreground = indicatorColor(data[10]), background = indicatorColor(data[11]);
+ _scriptSurface.fillRect(rect, background);
+ if (data[0] == 2) {
+ int digits = MIN<int>(rect.width() / 8, 8);
+ Common::String number = Common::String::format("%0*d", digits, value);
+ Graphics::Surface surface = _scriptSurface.getSubArea(rect);
+ font.drawString(&surface, number, 0, 0, rect.width(), foreground);
+ } else if (first != last) {
+ int length = data[0] == 3 ? rect.width() : rect.height();
+ int filled = (int64(value) - first) * length / (int64(last) - first);
+ if (data[0] == 3)
+ rect.right = rect.left + filled;
+ else
+ rect.top = rect.bottom - filled;
+ if (!rect.isEmpty())
+ _scriptSurface.fillRect(rect, foreground);
+ }
+ }
+}
+
+void KitEngine::drawUI() {
+ updateIndicators();
+ drawFullscreenSurface(_scriptSurface.surfacePtr());
+ _gfx->setViewport(_fullscreenViewArea);
+ _gfx->renderCrossair(_crossairPosition);
+}
+
+bool KitEngine::handleInput(const Common::Event &event) {
+ if (event.type == Common::EVENT_KEYDOWN) {
+ _kitVariables[15] = event.kbd.ascii;
+ if (!_kitVariables[15] && event.kbd.keycode < 128)
+ _kitVariables[15] = event.kbd.keycode;
+ } else if (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START) {
+ int control = -1;
+ switch (event.customType) {
+ case kActionEscape:
+ return false;
+ case kActionChangeMode: case kActionSkip:
+ _kitVariables[15] = ' ';
+ return true;
+ case kActionMoveUp: control = 0; break;
+ case kActionMoveDown: control = 1; break;
+ case kActionMoveLeft: control = 3; break;
+ case kActionMoveRight: control = 2; break;
+ case kActionRotateUp: control = 8; break;
+ case kActionRotateDown: control = 9; break;
+ case kActionRotateLeft: control = 6; break;
+ case kActionRotateRight: control = 7; break;
+ case kActionTurnBack: control = 12; break;
+ case kActionShoot: control = 30; break;
+ case kActionActivate: control = 31; break;
+ case kActionInfoMenu:
+ _kitVariables[15] = 'I';
+ return true;
+ default: break;
+ }
+ if (control >= 0) {
+ byte key = _controlData[5 * control + 4] >> 8;
+ if (key != 0xff)
+ _kitVariables[15] = key;
+ }
+ if (event.customType == kActionShoot || event.customType == kActionActivate) {
+ if (!_scriptFrameActive)
+ interact(event.customType == kActionShoot);
+ return true;
+ }
+ if (_scriptFrameActive)
+ return true;
+ } else if (_scriptFrameActive && event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_END &&
+ (event.customType == kActionRiseOrFlyUp || event.customType == kActionLowerOrFlyDown)) {
+ _moveUp = _moveDown = false;
+ return true;
+ } else if (event.type == Common::EVENT_LBUTTONDOWN || event.type == Common::EVENT_RBUTTONDOWN) {
+ _crossairPosition = getNormalizedPosition(event.mouse);
+ int buttons = g_system->getEventManager()->getButtonState();
+ _kitVariables[16] = ((buttons & Common::EventManager::LBUTTON) ? 1 : 0) |
+ ((buttons & Common::EventManager::RBUTTON) ? 2 : 0);
+ _kitVariables[16] |= event.type == Common::EVENT_LBUTTONDOWN ? 1 : 2;
+ _kitVariables[17] = _crossairPosition.x;
+ _kitVariables[18] = _crossairPosition.y;
+ if (!_scriptFrameActive)
+ interact(event.type == Common::EVENT_LBUTTONDOWN);
+ return true;
+ }
+ return false;
+}
+
+void KitEngine::interact(bool shot) {
+ if (!_viewArea.contains(_crossairPosition) || (shot && !(_kitVariables[20] & 1)))
+ return;
+ float x = 2.0f * (_crossairPosition.x - _viewArea.left) / _viewArea.width() - 1;
+ float y = 1 - 2.0f * (_crossairPosition.y - _viewArea.top) / _viewArea.height();
+ float projection = tan(Math::deg2rad(_fieldOfView) / 2);
+ Math::Vector3d direction = directionToVector(_pitch + Math::rad2deg(atan(y * projection / _viewAspectRatio)),
+ _yaw - Math::rad2deg(atan(x * projection)), false);
+ Object *object = _currentArea->checkCollisionRay(Math::Ray(_position, direction), 8192, true);
+ if (shot) {
+ _kitVariables[21]++;
+ if (_kitVariables[20] & 2)
+ _shootingFrames = 3;
+ }
+ if (!object || !object->isGeometric())
+ return;
+ if (!shot) {
+ float distance = 0;
+ for (int axis = 0; axis < 3; axis++) {
+ if (object->getSize().getValue(axis) * _currentArea->getScale() > 1000)
+ return;
+ distance += ABS(object->getOrigin().getValue(axis) + object->getSize().getValue(axis) / 2 - _position.getValue(axis));
+ }
+ if (distance > _activationRange)
+ return;
+ }
+ executeObjectConditions(static_cast<GeometricObject *>(object), shot, false, !shot);
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/language/16bitDetokeniser.cpp b/engines/freescape/language/16bitDetokeniser.cpp
new file mode 100644
index 00000000000..4e8585e2f12
--- /dev/null
+++ b/engines/freescape/language/16bitDetokeniser.cpp
@@ -0,0 +1,181 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/endian.h"
+#include "common/textconsole.h"
+
+#include "freescape/language/16bitDetokeniser.h"
+
+namespace Freescape {
+
+static const struct {
+ byte opcode;
+ Token::Type type;
+ byte minArgs, maxArgs;
+ const char *name;
+} opcodeTable[] = {
+ {0x00, Token::NOP, 0, 0, "NOP"},
+ {0x01, Token::CONDITIONAL, 0, 0, "ACTIVATED?"},
+ {0x02, Token::CONDITIONAL, 0, 0, "COLLIDED?"},
+ {0x03, Token::CONDITIONAL, 0, 0, "SHOT?"},
+ {0x04, Token::CONDITIONAL, 0, 0, "TIMER?"},
+ {0x10, Token::SETVAR, 2, 2, "SETVAR"},
+ {0x11, Token::ADDVAR, 2, 2, "ADDVAR"},
+ {0x12, Token::SUBVAR, 2, 2, "SUBVAR"},
+ {0x13, Token::ANDV, 2, 2, "ANDV"},
+ {0x14, Token::ORV, 2, 2, "ORV"},
+ {0x15, Token::NOTV, 1, 1, "NOTV"},
+ {0x16, Token::VAREQ, 2, 2, "VAR=?"},
+ {0x17, Token::VARGT, 2, 2, "VAR>?"},
+ {0x18, Token::VARLT, 2, 2, "VAR<?"},
+ {0x2f, Token::DESTROYEDQ, 1, 2, "DESTROYED?"},
+ {0x30, Token::INVIS, 1, 2, "INVIS"},
+ {0x31, Token::VIS, 1, 2, "VIS"},
+ {0x32, Token::TOGVIS, 1, 2, "TOGVIS"},
+ {0x33, Token::DESTROY, 1, 2, "DESTROY"},
+ {0x34, Token::INVISQ, 1, 2, "INVIS?"},
+ {0x35, Token::VISQ, 1, 2, "VIS?"},
+ {0x36, Token::MOVE, 3, 3, "MOVE"},
+ {0x37, Token::GETXPOS, 2, 3, "GETXPOS"},
+ {0x38, Token::GETYPOS, 2, 3, "GETYPOS"},
+ {0x39, Token::GETZPOS, 2, 3, "GETZPOS"},
+ {0x3a, Token::MOVETO, 3, 3, "MOVETO"},
+ {0x40, Token::IF, 0, 0, "IF"},
+ {0x41, Token::THEN, 0, 0, "THEN"},
+ {0x42, Token::ELSE, 0, 0, "ELSE"},
+ {0x43, Token::ENDIF, 0, 0, "ENDIF"},
+ {0x44, Token::AND, 0, 0, "AND"},
+ {0x45, Token::OR, 0, 0, "OR"},
+ {0x50, Token::STARTANIM, 1, 2, "STARTANIM"},
+ {0x51, Token::STOPANIM, 1, 2, "STOPANIM"},
+ {0x52, Token::START, 0, 0, "START"},
+ {0x53, Token::RESTART, 0, 0, "RESTART"},
+ {0x54, Token::INCLUDE, 1, 1, "INCLUDE"},
+ {0x55, Token::WAITTRIG, 0, 0, "WAITTRIG"},
+ {0x56, Token::TRIGANIM, 1, 2, "TRIGANIM"},
+ {0x57, Token::REMOVE, 1, 1, "REMOVE"},
+ {0x60, Token::LOOP, 1, 1, "LOOP"},
+ {0x61, Token::AGAIN, 2, 2, "AGAIN"},
+ {0x70, Token::SOUND, 1, 1, "SOUND"},
+ {0x71, Token::SYNCSND, 1, 1, "SYNCSND"},
+ {0x80, Token::WAIT, 0, 0, "WAIT"},
+ {0x81, Token::DELAY, 1, 1, "DELAY"},
+ {0x82, Token::UPDATEI, 1, 1, "UPDATEI"},
+ {0x83, Token::PRINT, 2, 255, "PRINT"},
+ {0x84, Token::REDRAW, 0, 0, "REDRAW"},
+ {0x85, Token::MODE, 1, 1, "MODE"},
+ {0x86, Token::ENDGAME, 0, 0, "ENDGAME"},
+ {0x87, Token::EXECUTE, 1, 1, "EXECUTE"},
+ {0x90, Token::GOTO, 1, 2, "GOTO"},
+ {0xff, Token::END, 0, 0, "END"}
+};
+
+Common::String detokenise16bitCondition(const Common::Array<byte> &tokenisedCondition, FCLInstructionVector &instructions) {
+ Common::String detokenisedStream;
+ int loops = 0;
+ for (uint32 bytePointer = 0; bytePointer < tokenisedCondition.size();) {
+ if (tokenisedCondition.size() - bytePointer < 2)
+ error("Truncated 16-bit FCL instruction at %u", bytePointer);
+ byte count = tokenisedCondition[bytePointer], opcode = tokenisedCondition[bytePointer + 1];
+ if (tokenisedCondition.size() - bytePointer < 2U + 2U * count)
+ error("Truncated 16-bit FCL instruction at %u", bytePointer);
+ uint index = 0;
+ while (index < ARRAYSIZE(opcodeTable) && opcodeTable[index].opcode != opcode)
+ index++;
+ if (index == ARRAYSIZE(opcodeTable))
+ error("Unknown 16-bit FCL opcode %02x at %u", opcode, bytePointer);
+ const auto &entry = opcodeTable[index];
+ if (count < entry.minArgs || count > entry.maxArgs)
+ error("Invalid argument count for 16-bit FCL opcode %02x at %u", opcode, bytePointer);
+
+ FCLInstruction instruction(entry.type);
+ int32 operands[3] = {};
+ Token::Type types[3] = {Token::UNKNOWN, Token::UNKNOWN, Token::UNKNOWN};
+ uint32 argumentPointer = bytePointer + 2;
+ uint argumentCount = count;
+ detokenisedStream += entry.name;
+ if (entry.type == Token::PRINT) {
+ uint16 length = READ_BE_UINT16(&tokenisedCondition[argumentPointer]);
+ if (count != 2 + (length + 1) / 2)
+ error("Invalid 16-bit FCL PRINT instruction at %u", bytePointer);
+ instruction._text = Common::String(reinterpret_cast<const char *>(&tokenisedCondition[argumentPointer + 2]), length);
+ detokenisedStream += Common::String::format(" (\"%s\", ", instruction._text.c_str());
+ argumentPointer += 2 + ((length + 1) & ~1);
+ argumentCount = 1;
+ } else if (entry.type == Token::AGAIN) {
+ argumentCount = 0; // The two words hold the runner's loop state.
+ } else if (count) {
+ detokenisedStream += " (";
+ }
+ for (uint i = 0; i < argumentCount; i++) {
+ uint16 operand = READ_BE_UINT16(&tokenisedCondition[argumentPointer + 2 * i]);
+ types[i] = operand & 0x8000 ? Token::VARIABLE : Token::CONSTANT;
+ operands[i] = types[i] == Token::VARIABLE ? operand & 0xff : (operand & 0x4000 ? int32(operand) - 0x8000 : operand);
+ if (i)
+ detokenisedStream += ", ";
+ detokenisedStream += Common::String::format(types[i] == Token::VARIABLE ? "v%d" : "%d", operands[i]);
+ }
+ if (argumentCount)
+ detokenisedStream += ")";
+ detokenisedStream += "\n";
+
+ // Match the operand order used by the 8-bit detokeniser.
+ switch (entry.type) {
+ case Token::SETVAR: case Token::ADDVAR: case Token::SUBVAR: case Token::ANDV: case Token::ORV:
+ case Token::GOTO:
+ SWAP(operands[0], operands[1]);
+ SWAP(types[0], types[1]);
+ break;
+ case Token::INVIS: case Token::VIS: case Token::TOGVIS: case Token::DESTROY:
+ case Token::INVISQ: case Token::VISQ: case Token::DESTROYEDQ:
+ case Token::STARTANIM: case Token::STOPANIM: case Token::TRIGANIM:
+ if (count == 2) {
+ SWAP(operands[0], operands[1]);
+ SWAP(types[0], types[1]);
+ }
+ break;
+ case Token::CONDITIONAL:
+ operands[0] = opcode == 1 ? kConditionalActivated : opcode == 2 ? kConditionalCollided :
+ opcode == 3 ? kConditionalShot : kConditionalTimeout;
+ types[0] = Token::CONSTANT;
+ break;
+ case Token::LOOP:
+ loops++;
+ break;
+ case Token::AGAIN:
+ if (--loops < 0)
+ error("16-bit FCL AGAIN without LOOP");
+ break;
+ default:
+ break;
+ }
+ instruction.setSource(operands[0], types[0]);
+ instruction.setDestination(operands[1], types[1]);
+ instruction.setAdditional(operands[2], types[2]);
+ instructions.push_back(instruction);
+ bytePointer += 2 + 2 * count;
+ }
+ if (loops)
+ error("Unterminated 16-bit FCL LOOP");
+ return detokenisedStream;
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/language/16bitDetokeniser.h b/engines/freescape/language/16bitDetokeniser.h
new file mode 100644
index 00000000000..08b63b214ca
--- /dev/null
+++ b/engines/freescape/language/16bitDetokeniser.h
@@ -0,0 +1,33 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef FREESCAPE_16BITDETOKENISER_H
+#define FREESCAPE_16BITDETOKENISER_H
+
+#include "freescape/language/instruction.h"
+
+namespace Freescape {
+
+Common::String detokenise16bitCondition(const Common::Array<byte> &tokenisedCondition, FCLInstructionVector &instructions);
+
+} // namespace Freescape
+
+#endif
diff --git a/engines/freescape/language/8bitDetokeniser.h b/engines/freescape/language/8bitDetokeniser.h
index 7f00dcd04de..c06d5b73c5b 100644
--- a/engines/freescape/language/8bitDetokeniser.h
+++ b/engines/freescape/language/8bitDetokeniser.h
@@ -42,13 +42,6 @@ enum {
k8bitMaxVariable = 64
};
-enum {
- kConditionalShot = 1 << 0,
- kConditionalTimeout = 1 << 1,
- kConditionalCollided = 1 << 2,
- kConditionalActivated = 1 << 3,
-};
-
extern uint8 k8bitVariableShield;
Common::String detokenise8bitCondition(Common::Array<uint16> &tokenisedCondition, FCLInstructionVector &instructions, bool enableActivated);
diff --git a/engines/freescape/language/instruction.cpp b/engines/freescape/language/instruction.cpp
index 8d6c924d57a..eae6d37fcad 100644
--- a/engines/freescape/language/instruction.cpp
+++ b/engines/freescape/language/instruction.cpp
@@ -41,9 +41,10 @@ FCLInstructionVector *duplicateCondition(FCLInstructionVector *condition) {
FCLInstruction FCLInstruction::duplicate() {
FCLInstruction copy(_type);
- copy.setSource(_source);
- copy.setDestination(_destination);
- copy.setAdditional(_additional);
+ copy.setSource(_source, _sourceType);
+ copy.setDestination(_destination, _destinationType);
+ copy.setAdditional(_additional, _additionalType);
+ copy._text = _text;
copy._thenInstructions = duplicateCondition(_thenInstructions);
copy._elseInstructions = duplicateCondition(_elseInstructions);
@@ -55,6 +56,7 @@ FCLInstruction::FCLInstruction(Token::Type type_) {
_source = 0;
_destination = 0;
_additional = 0;
+ _sourceType = _destinationType = _additionalType = Token::UNKNOWN;
_type = type_;
_thenInstructions = nullptr;
_elseInstructions = nullptr;
@@ -64,21 +66,25 @@ FCLInstruction::FCLInstruction() {
_source = 0;
_destination = 0;
_additional = 0;
+ _sourceType = _destinationType = _additionalType = Token::UNKNOWN;
_type = Token::UNKNOWN;
_thenInstructions = nullptr;
_elseInstructions = nullptr;
}
-void FCLInstruction::setSource(int32 source_) {
+void FCLInstruction::setSource(int32 source_, Token::Type type) {
_source = source_;
+ _sourceType = type;
}
-void FCLInstruction::setAdditional(int32 additional_) {
+void FCLInstruction::setAdditional(int32 additional_, Token::Type type) {
_additional = additional_;
+ _additionalType = type;
}
-void FCLInstruction::setDestination(int32 destination_) {
+void FCLInstruction::setDestination(int32 destination_, Token::Type type) {
_destination = destination_;
+ _destinationType = type;
}
void FCLInstruction::setBranches(FCLInstructionVector *thenBranch, FCLInstructionVector *elseBranch) {
@@ -587,7 +593,7 @@ bool FreescapeEngine::executeEndIfVisibilityIsEqual(FCLInstruction &instruction)
return (obj->isInvisible() == (value != 0));
}
-bool FreescapeEngine::checkConditional(FCLInstruction &instruction, bool shot, bool collided, bool timer, bool activated) {
+bool FreescapeEngine::checkConditional(const FCLInstruction &instruction, bool shot, bool collided, bool timer, bool activated) {
uint16 conditional = instruction._source;
bool result = false;
diff --git a/engines/freescape/language/instruction.h b/engines/freescape/language/instruction.h
index c7f6835cba4..c3feb0e0dec 100644
--- a/engines/freescape/language/instruction.h
+++ b/engines/freescape/language/instruction.h
@@ -26,10 +26,18 @@
#define FREESCAPE_INSTRUCTION_H
#include "common/array.h"
+#include "common/str.h"
#include "freescape/language/token.h"
namespace Freescape {
+enum {
+ kConditionalShot = 1 << 0,
+ kConditionalTimeout = 1 << 1,
+ kConditionalCollided = 1 << 2,
+ kConditionalActivated = 1 << 3,
+};
+
class FCLInstruction;
typedef Common::Array<FCLInstruction> FCLInstructionVector;
@@ -37,9 +45,9 @@ class FCLInstruction {
public:
FCLInstruction();
FCLInstruction(Token::Type type);
- void setSource(int32 source);
- void setAdditional(int32 additional);
- void setDestination(int32 destination);
+ void setSource(int32 source, Token::Type type = Token::CONSTANT);
+ void setAdditional(int32 additional, Token::Type type = Token::CONSTANT);
+ void setDestination(int32 destination, Token::Type type = Token::CONSTANT);
Token::Type getType() const;
@@ -57,6 +65,11 @@ public:
int32 _source;
int32 _additional;
int32 _destination;
+ // UNKNOWN denotes an omitted operand.
+ Token::Type _sourceType;
+ Token::Type _additionalType;
+ Token::Type _destinationType;
+ Common::String _text;
FCLInstructionVector *_thenInstructions;
FCLInstructionVector *_elseInstructions;
diff --git a/engines/freescape/language/instruction16bit.cpp b/engines/freescape/language/instruction16bit.cpp
new file mode 100644
index 00000000000..581c84323fd
--- /dev/null
+++ b/engines/freescape/language/instruction16bit.cpp
@@ -0,0 +1,640 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/algorithm.h"
+#include "math/utils.h"
+
+#include "freescape/games/3dck/3dck.h"
+
+namespace Freescape {
+
+void KitEngine::resetScripts() {
+ // V255 survives ENDGAME, as in the original runner.
+ memset(_kitVariables, 0, 255 * sizeof(_kitVariables[0]));
+ _changedVariables = 0;
+ _kitVariables[20] = 7;
+ _scriptTicks = _timerTicks = 0;
+ _lastScriptTick = _ticks;
+ _scriptFrameActive = _scriptDelayed = false;
+ _initialScriptPending = _initialCondition != 0;
+ _scriptQueue.clear();
+ _suspendedScripts.clear();
+ _scriptSurface.fillRect(_fullscreenViewArea, 0);
+ for (auto &condition : _globalConditions) {
+ condition.script = ScriptState();
+ condition.script.source = &condition.condition;
+ }
+ for (auto &entry : _areaData) {
+ AreaData &area = entry._value;
+ for (auto &condition : area.conditions) {
+ condition.script = ScriptState();
+ condition.script.area = entry._key;
+ condition.script.source = &condition.condition;
+ }
+ for (auto &item : area.objects) {
+ ObjectData &object = item._value;
+ object.script = ScriptState();
+ object.script.object = &object;
+ object.script.area = entry._key;
+ object.script.source = &object.condition;
+ object.animatedObjects.clear();
+ object.animator = 0;
+ object.flags &= 0x84;
+ if ((object.flags & 4) || object.type == 16)
+ object.flags |= 2;
+ object.origin = object.initialOrigin;
+ Object *geometry = _areaMap[entry._key]->objectWithID(object.id);
+ if (geometry && geometry->isGeometric()) {
+ float scale = entry._key == 255 ? 1 : _areaMap[entry._key]->getScale();
+ static_cast<GeometricObject *>(geometry)->offsetOrigin(object.origin / scale);
+ }
+ }
+ _areaMap[entry._key]->getSortedObjects().clear();
+ }
+}
+
+void KitEngine::startScript(ScriptState &script) {
+ script.code = script.source;
+ script.ip = script.restart = 0;
+ script.loops.clear();
+ script.predicate = script.previousPredicate = true;
+ script.booleanOp = Token::UNKNOWN;
+ script.events = script.object ? script.object->flags & 0x38 : 0;
+ if (script.object)
+ script.object->flags &= ~0x38;
+ script.running = true;
+}
+
+void KitEngine::updateTimeVariables() {
+ uint32 elapsed = _ticks - _lastScriptTick;
+ _lastScriptTick = _ticks;
+ _scriptTicks += elapsed;
+ _timerTicks += elapsed;
+ _kitVariables[19] += elapsed;
+}
+
+void KitEngine::readSystemVariables() {
+ float scale = _currentArea->getScale();
+ for (int axis = 0; axis < 3; axis++)
+ _kitVariables[axis] = int32(_position.getValue(axis) * scale);
+ _kitVariables[3] = (int(_pitch) % 360 + 360) % 360;
+ _kitVariables[4] = (int(90 - _yaw) % 360 + 360) % 360;
+ _kitVariables[5] = (int(_roll) % 360 + 360) % 360;
+ _kitVariables[7] = int32(_playerHeight * scale);
+ _kitVariables[8] = _currentArea->getAreaID();
+}
+
+void KitEngine::writeSystemVariables() {
+ float scale = _currentArea->getScale();
+ for (int axis = 0; axis < 3; axis++) {
+ if (_changedVariables & (1 << axis)) {
+ _position.setValue(axis, int16(_kitVariables[axis]) / scale);
+ _lastPosition = _position;
+ }
+ }
+ if (_changedVariables & (1 << 3))
+ _pitch = int16(_kitVariables[3]) / 5 * 5;
+ if (_changedVariables & (1 << 4))
+ _yaw = 90 - int16(_kitVariables[4]) / 5 * 5;
+ if (_changedVariables & (1 << 5))
+ _roll = int16(_kitVariables[5]) / 5 * 5;
+ if ((_changedVariables & (1 << 7)) && int16(_kitVariables[7]) > 0)
+ _playerHeight = int16(_kitVariables[7]) / scale;
+ if (_changedVariables & (1 << 6))
+ _flyMode = _kitVariables[6] != 0 || _noClipMode;
+ if (_changedVariables & 0x38)
+ updateCamera();
+ _changedVariables = 0;
+}
+
+void KitEngine::setScriptVariable(byte index, uint32 value) {
+ _kitVariables[index] = value;
+ if (index < 8)
+ _changedVariables |= 1 << index;
+}
+
+void KitEngine::beginScriptFrame() {
+ readSystemVariables();
+ _timerTriggered = _timerTicks >= _timerInterval;
+ if (_timerTriggered)
+ _timerTicks = 0;
+ _scriptQueue.clear();
+ _scriptQueueIndex = 0;
+ _scriptFrameActive = true;
+ if (_initialScriptPending) {
+ _scriptQueue.push_back(ScriptEntry(&_globalConditions[_initialCondition - 1].script, false));
+ _initialScriptPending = false;
+ }
+ for (auto *script : _suspendedScripts) {
+ if (script->object) {
+ script->events = script->object->flags & 0x38;
+ script->object->flags &= ~0x38;
+ }
+ _scriptQueue.push_back(ScriptEntry(script, true));
+ }
+ _suspendedScripts.clear();
+
+ AreaData &area = _areaData[_currentArea->getAreaID()];
+ for (uint16 id : area.objectOrder) {
+ ObjectData &object = area.objects[id];
+ if (object.type != 16 && !object.condition.empty())
+ _scriptQueue.push_back(ScriptEntry(&object.script, false));
+ }
+ for (auto &condition : area.conditions)
+ _scriptQueue.push_back(ScriptEntry(&condition.script, false));
+ for (uint i = 0; i < _globalConditions.size(); i++) {
+ if (i + 1 != _initialCondition)
+ _scriptQueue.push_back(ScriptEntry(&_globalConditions[i].script, false));
+ }
+ for (uint16 id : area.objectOrder) {
+ ObjectData &object = area.objects[id];
+ if (object.type == 16)
+ _scriptQueue.push_back(ScriptEntry(&object.script, false));
+ }
+}
+
+void KitEngine::updateScripts() {
+ if (_scriptDelayed) {
+ if (int32(_scriptTicks - _delayUntil) < 0)
+ return;
+ _scriptDelayed = false;
+ }
+ if (!_scriptFrameActive) {
+ if (_playerWasCrushed) {
+ _kitVariables[12]++;
+ _playerWasCrushed = false;
+ _avoidRenderingFrames = 0;
+ }
+ beginScriptFrame();
+ }
+
+ uint budget = 4096;
+ uint16 areaID = _currentArea->getAreaID();
+ while (_scriptQueueIndex < _scriptQueue.size()) {
+ ScriptEntry &entry = _scriptQueue[_scriptQueueIndex];
+ ScriptState &script = *entry.script;
+ ObjectData *object = script.object;
+ bool animator = object && object->type == 16;
+ if (script.area && script.area != areaID) {
+ if (!animator)
+ script.running = false;
+ _scriptQueueIndex++;
+ continue;
+ }
+ if (!entry.resume) {
+ if ((animator && (object->flags & 2)) || (!animator && script.running) ||
+ (object && !animator && !(object->flags & 0x38))) {
+ _scriptQueueIndex++;
+ continue;
+ }
+ if (!script.running)
+ startScript(script);
+ entry.resume = true;
+ }
+ FCLExecutionResult result = executeCode(script, budget);
+ if (result == kFCLPaused) {
+ writeSystemVariables();
+ return;
+ }
+ if (result == kFCLFinished) {
+ script.running = false;
+ if (animator)
+ object->flags |= 2;
+ } else if (!animator) {
+ _suspendedScripts.push_back(&script);
+ }
+ _scriptQueueIndex++;
+ if (_gameStateControl == kFreescapeGameStateRestart)
+ break;
+ if (areaID != _currentArea->getAreaID()) {
+ for (uint i = _scriptQueueIndex; i < _scriptQueue.size(); i++) {
+ ScriptEntry &pending = _scriptQueue[i];
+ if (!pending.resume || !pending.script->running)
+ continue;
+ if (!pending.script->area)
+ _suspendedScripts.push_back(pending.script);
+ else if (!pending.script->object || pending.script->object->type != 16)
+ pending.script->running = false;
+ }
+ break;
+ }
+ }
+ writeSystemVariables();
+ _scriptFrameActive = false;
+}
+
+int32 KitEngine::getVariableOrConstant(int32 operand, Token::Type type) const {
+ return type == Token::VARIABLE ? int32(_kitVariables[operand]) : operand;
+}
+
+void KitEngine::setScriptPredicate(ScriptState &script, bool value) {
+ if (script.booleanOp == Token::AND)
+ value = script.previousPredicate && value;
+ else if (script.booleanOp == Token::OR)
+ value = script.previousPredicate || value;
+ script.predicate = value;
+ script.booleanOp = Token::UNKNOWN;
+}
+
+KitEngine::ObjectData *KitEngine::scriptObject(uint16 area, uint16 id) {
+ if (!area)
+ area = _currentArea->getAreaID();
+ auto found = _areaData.find(area);
+ if (found == _areaData.end())
+ return nullptr;
+ auto object = found->_value.objects.find(id);
+ return object == found->_value.objects.end() ? nullptr : &object->_value;
+}
+
+void KitEngine::collectObjects(uint16 area, uint16 id, Common::Array<uint16> &objects) {
+ Common::Array<uint16> pending, visited;
+ pending.push_back(id);
+ while (!pending.empty()) {
+ id = pending.back();
+ pending.pop_back();
+ if (Common::find(visited.begin(), visited.end(), id) != visited.end())
+ continue;
+ visited.push_back(id);
+ ObjectData *object = scriptObject(area, id);
+ if (!object)
+ continue;
+ if (object->type == kGroupType) {
+ for (int i = int(object->members.size()) - 1; i >= 0; i--)
+ pending.push_back(object->members[i]);
+ } else {
+ objects.push_back(id);
+ }
+ }
+}
+
+void KitEngine::setObjectStatus(uint16 area, uint16 id, Token::Type operation) {
+ Common::Array<uint16> objects;
+ collectObjects(area, id, objects);
+ for (uint16 member : objects) {
+ ObjectData &object = *scriptObject(area, member);
+ switch (operation) {
+ case Token::INVIS: object.flags |= 2; break;
+ case Token::VIS: object.flags &= ~2; break;
+ case Token::TOGVIS: object.flags ^= 2; break;
+ case Token::DESTROY: object.flags |= 3; break;
+ default: break;
+ }
+ Object *geometry = _areaMap[area]->objectWithID(member);
+ if (!geometry)
+ continue;
+ if (object.flags & 1)
+ geometry->destroy();
+ if (object.flags & 2)
+ geometry->makeInvisible();
+ else
+ geometry->makeVisible();
+ _areaMap[area]->getSortedObjects().clear();
+ }
+}
+
+FCLExecutionResult KitEngine::executeCode(ScriptState &script, uint &budget) {
+ bool animator = script.object && script.object->type == 16;
+ while (script.ip < script.code->size()) {
+ if (!budget)
+ return kFCLPaused;
+ budget--;
+ const FCLInstructionVector &code = *script.code;
+ uint32 ip = script.ip++;
+ const FCLInstruction &instruction = code[ip];
+ Token::Type op = instruction.getType();
+ int32 source = getVariableOrConstant(instruction._source, instruction._sourceType);
+ int32 destination = getVariableOrConstant(instruction._destination, instruction._destinationType);
+ int32 additional = getVariableOrConstant(instruction._additional, instruction._additionalType);
+ bool hasDestination = instruction._destinationType != Token::UNKNOWN;
+ switch (op) {
+ case Token::NOP: case Token::ENDIF:
+ break;
+ case Token::IF:
+ script.predicate = true;
+ script.booleanOp = Token::UNKNOWN;
+ break;
+ case Token::AND: case Token::OR:
+ script.previousPredicate = script.predicate;
+ script.booleanOp = op;
+ break;
+ case Token::THEN: case Token::ELSE:
+ if (op == Token::ELSE || !script.predicate) {
+ int depth = 0;
+ while (script.ip < code.size()) {
+ Token::Type next = code[script.ip++].getType();
+ if (next == Token::IF)
+ depth++;
+ else if (next == Token::ENDIF) {
+ if (!depth)
+ break;
+ depth--;
+ } else if (next == Token::ELSE && !depth && op == Token::THEN) {
+ break;
+ }
+ }
+ }
+ break;
+ case Token::CONDITIONAL:
+ // RUNVGA retains these flags until the object's execution yields or ends.
+ setScriptPredicate(script, checkConditional(instruction,
+ script.events & 16, script.events & 32, _timerTriggered, script.events & 8));
+ break;
+ case Token::SETVAR: case Token::ADDVAR: case Token::SUBVAR: case Token::ANDV: case Token::ORV: case Token::NOTV: {
+ uint32 value = destination;
+ switch (op) {
+ case Token::ADDVAR: value += uint32(source); break;
+ case Token::SUBVAR: value = uint32(source) - value; break;
+ case Token::ANDV: value &= uint32(source); break;
+ case Token::ORV: value |= uint32(source); break;
+ case Token::NOTV: value = ~uint32(source); break;
+ default: break;
+ }
+ if (instruction._sourceType == Token::VARIABLE)
+ setScriptVariable(instruction._source, value);
+ setScriptPredicate(script, value != 0);
+ break;
+ }
+ case Token::VAREQ: case Token::VARGT: case Token::VARLT:
+ setScriptPredicate(script, op == Token::VAREQ ? source == destination : op == Token::VARGT ? source > destination : source < destination);
+ break;
+ case Token::INVIS: case Token::VIS: case Token::TOGVIS: case Token::DESTROY:
+ case Token::INVISQ: case Token::VISQ: case Token::DESTROYEDQ: {
+ uint16 area = hasDestination && source ? source : _currentArea->getAreaID();
+ uint16 object = hasDestination ? destination : source;
+ if (op == Token::INVIS || op == Token::VIS || op == Token::TOGVIS || op == Token::DESTROY)
+ setObjectStatus(area, object, op);
+ else {
+ Common::Array<uint16> objects;
+ collectObjects(area, object, objects);
+ bool result = false;
+ for (uint16 member : objects) {
+ byte flags = scriptObject(area, member)->flags;
+ result = op == Token::DESTROYEDQ ? (flags & 1) != 0 : op == Token::INVISQ ? (flags & 2) != 0 : !(flags & 2);
+ }
+ setScriptPredicate(script, result);
+ }
+ break;
+ }
+ case Token::GETXPOS: case Token::GETYPOS: case Token::GETZPOS: {
+ ObjectData *object = scriptObject(additional, destination);
+ if (object)
+ setScriptVariable(instruction._source & 0xff, int32(object->origin.getValue(op - Token::GETXPOS)));
+ break;
+ }
+ case Token::EXECUTE: {
+ ObjectData *object = scriptObject(0, source);
+ if (!object || object->type == kGroupType)
+ return kFCLFinished;
+ // EXECUTE replaces the code, retaining the original object's event flags.
+ script.code = &object->condition;
+ script.ip = 0;
+ script.loops.clear();
+ break;
+ }
+ case Token::GOTO: {
+ uint16 area = instruction._sourceType != Token::UNKNOWN ? source : _currentArea->getAreaID();
+ if (!_areaMap.contains(area) || !_areaMap[area]->entranceWithID(destination & 0x7fff)) {
+ warning("Invalid 3D Construction Kit GOTO (%d, %u)", destination, area);
+ return kFCLFinished;
+ }
+ writeSystemVariables();
+ uint16 previous = _currentArea->getAreaID();
+ gotoArea(area, destination & 0x7fff);
+ if (previous != area)
+ return kFCLFinished;
+ break;
+ }
+ case Token::MODE:
+ setScriptVariable(6, CLIP<int32>(source, 1, 3) - 1);
+ break;
+ case Token::ENDGAME:
+ _gameStateControl = kFreescapeGameStateRestart;
+ return kFCLFinished;
+ case Token::END:
+ return animator ? kFCLYielded : kFCLFinished;
+ case Token::WAIT:
+ return kFCLYielded;
+ case Token::DELAY:
+ if (uint16(source)) {
+ _delayUntil = _scriptTicks + uint16(source);
+ _scriptDelayed = true;
+ return kFCLPaused;
+ }
+ break;
+ case Token::REDRAW:
+ _scriptSurface.fillRect(_viewArea, 0);
+ return animator ? kFCLYielded : kFCLPaused;
+ case Token::LOOP: {
+ int depth = 0;
+ uint32 end = script.ip;
+ while (end < code.size()) {
+ Token::Type next = code[end].getType();
+ if (next == Token::LOOP)
+ depth++;
+ else if (next == Token::AGAIN) {
+ if (!depth)
+ break;
+ depth--;
+ }
+ end++;
+ }
+ FCLLoop &loop = script.loops[end];
+ loop.start = script.ip;
+ loop.remaining = uint16(source);
+ break;
+ }
+ case Token::AGAIN: {
+ auto loop = script.loops.find(ip);
+ if (loop == script.loops.end())
+ return kFCLFinished;
+ if (--loop->_value.remaining != 0) {
+ script.ip = loop->_value.start;
+ } else {
+ script.loops.erase(loop);
+ }
+ break;
+ }
+ case Token::STARTANIM: case Token::STOPANIM: case Token::TRIGANIM: {
+ ObjectData *object = scriptObject(hasDestination ? source : 0, uint16(hasDestination ? destination : source) | 0x4000);
+ if (object && object->type == 16) {
+ if (op == Token::STARTANIM)
+ object->flags &= ~2;
+ else if (op == Token::STOPANIM)
+ object->flags |= 2;
+ else
+ object->flags |= 1;
+ }
+ if (animator && op == Token::STOPANIM)
+ return kFCLYielded;
+ break;
+ }
+ case Token::START:
+ if (animator)
+ script.restart = script.ip;
+ break;
+ case Token::RESTART:
+ if (animator)
+ script.ip = script.restart;
+ break;
+ case Token::INCLUDE: case Token::REMOVE:
+ if (animator) {
+ Common::Array<uint16> objects;
+ collectObjects(script.area, source, objects);
+ for (uint16 member : objects) {
+ ObjectData &object = *scriptObject(script.area, member);
+ if (!(object.flags & 0x80))
+ continue;
+ if (op == Token::INCLUDE && !object.animator) {
+ object.animator = script.object->id;
+ script.object->animatedObjects.push_back(member);
+ } else if (op == Token::REMOVE && object.animator == script.object->id) {
+ auto &members = script.object->animatedObjects;
+ for (uint i = 0; i < members.size(); i++) {
+ if (members[i] == member) {
+ members.remove_at(i);
+ break;
+ }
+ }
+ object.animator = 0;
+ }
+ }
+ }
+ break;
+ case Token::WAITTRIG:
+ if (animator) {
+ if (!(script.object->flags & 1)) {
+ script.ip = ip;
+ return kFCLYielded;
+ }
+ script.object->flags &= ~1;
+ }
+ break;
+ case Token::MOVE: case Token::MOVETO:
+ if (animator) {
+ setScriptPredicate(script, moveAnimation(script,
+ Math::Vector3d(int16(source), int16(destination), int16(additional)), op == Token::MOVETO));
+ return kFCLYielded;
+ }
+ break;
+ case Token::PRINT:
+ printMessage(source, instruction._text);
+ break;
+ case Token::UPDATEI:
+ updateIndicators();
+ break;
+ case Token::SOUND: case Token::SYNCSND:
+ if (!_soundWarning) {
+ warning("3D Construction Kit sound playback is not implemented");
+ _soundWarning = true;
+ }
+ break;
+ default:
+ error("Unhandled 16-bit FCL instruction %d at ip: %u", op, ip);
+ }
+ }
+ return kFCLFinished;
+}
+
+bool KitEngine::executeObjectConditions(GeometricObject *obj, bool shot, bool collided, bool activated) {
+ ObjectData *object = scriptObject(0, obj->getObjectID());
+ if (object)
+ object->flags |= (shot ? 16 : 0) | (collided ? 32 : 0) | (activated ? 8 : 0);
+ return false;
+}
+
+bool KitEngine::moveAnimation(ScriptState &script, Math::Vector3d movement, bool absolute) {
+ const auto &members = script.object->animatedObjects;
+ if (members.empty())
+ return true;
+ Math::Vector3d minimum(8191, 8191, 8191), maximum;
+ for (uint16 id : members) {
+ ObjectData &object = *scriptObject(script.area, id);
+ for (int axis = 0; axis < 3; axis++) {
+ minimum.setValue(axis, MIN(minimum.getValue(axis), object.origin.getValue(axis)));
+ maximum.setValue(axis, MAX(maximum.getValue(axis), object.origin.getValue(axis) + object.size.getValue(axis)));
+ }
+ }
+ if (absolute)
+ movement -= minimum;
+ bool unobstructed = true;
+ for (int axis = 0; axis < 3; axis++) {
+ float delta = movement.getValue(axis);
+ float clipped = CLIP(delta, -minimum.getValue(axis), 8191 - maximum.getValue(axis));
+ if (delta != clipped)
+ unobstructed = false;
+ movement.setValue(axis, clipped);
+ }
+ // The runner collides the combined header bounds of all included objects.
+ AreaData &area = _areaData[script.area];
+ for (uint16 id : area.objectOrder) {
+ ObjectData &object = area.objects[id];
+ if ((id & 0xc000) || (object.flags & 3) || object.type == kGroupType ||
+ object.animator == script.object->id)
+ continue;
+ bool overlaps = true;
+ for (int axis = 0; axis < 3; axis++) {
+ float delta = movement.getValue(axis);
+ if (minimum.getValue(axis) + delta >= object.origin.getValue(axis) + object.size.getValue(axis) ||
+ maximum.getValue(axis) + delta <= object.origin.getValue(axis))
+ overlaps = false;
+ }
+ if (!overlaps)
+ continue;
+ object.flags |= 32;
+ unobstructed = false;
+ for (int axis = 0; axis < 3; axis++) {
+ float delta = movement.getValue(axis);
+ if (delta >= 0) {
+ float gap = object.origin.getValue(axis) - maximum.getValue(axis);
+ if (gap >= 0 && gap < delta)
+ movement.setValue(axis, gap);
+ } else {
+ float gap = minimum.getValue(axis) - object.origin.getValue(axis) - object.size.getValue(axis);
+ if (gap >= 0 && gap < -delta)
+ movement.setValue(axis, -gap);
+ }
+ }
+ }
+ float scale = script.area == 255 ? 1 : _areaMap[script.area]->getScale();
+ for (uint16 id : members) {
+ ObjectData &object = *scriptObject(script.area, id);
+ object.origin += movement;
+ Object *geometry = _areaMap[script.area]->objectWithID(id);
+ if (geometry && geometry->isGeometric())
+ static_cast<GeometricObject *>(geometry)->offsetOrigin(object.origin / scale);
+ }
+ _areaMap[script.area]->getSortedObjects().clear();
+ return unobstructed;
+}
+
+void KitEngine::updatePlayerMovement(float deltaTime) {
+ if (_scriptFrameActive)
+ return;
+ float height = _position.y();
+ FreescapeEngine::updatePlayerMovement(deltaTime);
+ if (_hasFallen) {
+ _kitVariables[10] += MAX<int>(0, height - _position.y() - _maxFallingDistance);
+ _hasFallen = false;
+ _avoidRenderingFrames = 0;
+ }
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/language/instruction16bit.h b/engines/freescape/language/instruction16bit.h
new file mode 100644
index 00000000000..7b1795c3a9b
--- /dev/null
+++ b/engines/freescape/language/instruction16bit.h
@@ -0,0 +1,48 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef FREESCAPE_INSTRUCTION16BIT_H
+#define FREESCAPE_INSTRUCTION16BIT_H
+
+#include "common/hashmap.h"
+#include "freescape/language/instruction.h"
+
+namespace Freescape {
+
+struct FCLLoop {
+ uint32 start = 0;
+ uint16 remaining = 0;
+};
+
+struct FCLExecutionState {
+ const FCLInstructionVector *source = nullptr, *code = nullptr;
+ uint32 ip = 0, restart = 0;
+ Common::HashMap<uint32, FCLLoop> loops;
+ bool running = false;
+ bool predicate = true, previousPredicate = true;
+ Token::Type booleanOp = Token::UNKNOWN;
+};
+
+enum FCLExecutionResult { kFCLFinished, kFCLYielded, kFCLPaused };
+
+} // namespace Freescape
+
+#endif
diff --git a/engines/freescape/language/token.h b/engines/freescape/language/token.h
index 942802dd346..f3992c743cc 100644
--- a/engines/freescape/language/token.h
+++ b/engines/freescape/language/token.h
@@ -78,6 +78,8 @@ public:
TRIGANIM,
UPDATEI,
VAREQ,
+ VARGT,
+ VARLT,
IFGTEQ,
IFLTEQ,
VISQ,
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index 933e7c7e52e..16915b5fa6c 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -53,13 +53,16 @@ MODULE_OBJS := \
games/eclipse/cpc.o \
games/eclipse/zx.o \
games/3dck/3dck.o \
+ games/3dck/ui.o \
games/palettes.o \
gfx.o \
loaders/8bitImage.o \
loaders/8bitBinaryLoader.o \
loaders/c64.o \
language/8bitDetokeniser.o \
+ language/16bitDetokeniser.o \
language/instruction.o \
+ language/instruction16bit.o \
metaengine.o \
movement.o \
objects/geometricobject.o \
Commit: c567de577ac1ff7a8292633d8c8c13be34e222d6
https://github.com/scummvm/scummvm/commit/c567de577ac1ff7a8292633d8c8c13be34e222d6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-07T07:23:53+02:00
Commit Message:
FREESCAPE: refactored instructions code into functions for the 3dck
Changed paths:
engines/freescape/games/3dck/3dck.h
engines/freescape/language/instruction16bit.cpp
diff --git a/engines/freescape/games/3dck/3dck.h b/engines/freescape/games/3dck/3dck.h
index 1dd0a9837d6..421bd1dd373 100644
--- a/engines/freescape/games/3dck/3dck.h
+++ b/engines/freescape/games/3dck/3dck.h
@@ -104,12 +104,41 @@ private:
void startScript(ScriptState &script);
void beginScriptFrame();
FCLExecutionResult executeCode(ScriptState &script, uint &budget);
+ void executeIfThenElse(const FCLInstruction &instruction, ScriptState &script);
+ void executeConditional(const FCLInstruction &instruction, ScriptState &script);
+ void executeSetVariable(const FCLInstruction &instruction, ScriptState &script);
+ void executeIncrementVariable(const FCLInstruction &instruction, ScriptState &script);
+ void executeDecrementVariable(const FCLInstruction &instruction, ScriptState &script);
+ void executeAndVariable(const FCLInstruction &instruction, ScriptState &script);
+ void executeOrVariable(const FCLInstruction &instruction, ScriptState &script);
+ void executeNotVariable(const FCLInstruction &instruction, ScriptState &script);
+ void executeVariableComparison(const FCLInstruction &instruction, ScriptState &script);
+ void executeObjectStatus(const FCLInstruction &instruction);
+ bool checkObjectStatus(const FCLInstruction &instruction);
+ void executeGetPosition(const FCLInstruction &instruction);
+ bool executeExecute(const FCLInstruction &instruction, ScriptState &script);
+ bool executeGoto(const FCLInstruction &instruction);
+ void executeMode(const FCLInstruction &instruction);
+ bool executeDelay(const FCLInstruction &instruction);
+ void executeLoop(const FCLInstruction &instruction, ScriptState &script);
+ bool executeAgain(ScriptState &script, uint32 ip);
+ void executeStartAnim(const FCLInstruction &instruction);
+ void executeStopAnim(const FCLInstruction &instruction);
+ void executeTriggerAnim(const FCLInstruction &instruction);
+ void executeInclude(const FCLInstruction &instruction, ScriptState &script);
+ void executeRemove(const FCLInstruction &instruction, ScriptState &script);
+ bool executeWaitTrigger(ScriptState &script, uint32 ip);
+ void executeMove(const FCLInstruction &instruction, ScriptState &script);
+ void executeSound(const FCLInstruction &instruction);
void readSystemVariables();
void writeSystemVariables();
void setScriptVariable(byte index, uint32 value);
+ void setVariableResult(const FCLInstruction &instruction, ScriptState &script, uint32 value);
int32 getVariableOrConstant(int32 operand, Token::Type type) const;
void setScriptPredicate(ScriptState &script, bool value);
+ void getObjectReference(const FCLInstruction &instruction, uint16 &area, uint16 &id) const;
ObjectData *scriptObject(uint16 area, uint16 id);
+ ObjectData *scriptAnimator(const FCLInstruction &instruction);
void collectObjects(uint16 area, uint16 id, Common::Array<uint16> &objects);
void setObjectStatus(uint16 area, uint16 id, Token::Type operation);
bool moveAnimation(ScriptState &script, Math::Vector3d movement, bool absolute);
diff --git a/engines/freescape/language/instruction16bit.cpp b/engines/freescape/language/instruction16bit.cpp
index 581c84323fd..d923c963486 100644
--- a/engines/freescape/language/instruction16bit.cpp
+++ b/engines/freescape/language/instruction16bit.cpp
@@ -316,114 +316,77 @@ FCLExecutionResult KitEngine::executeCode(ScriptState &script, uint &budget) {
if (!budget)
return kFCLPaused;
budget--;
- const FCLInstructionVector &code = *script.code;
uint32 ip = script.ip++;
- const FCLInstruction &instruction = code[ip];
- Token::Type op = instruction.getType();
- int32 source = getVariableOrConstant(instruction._source, instruction._sourceType);
- int32 destination = getVariableOrConstant(instruction._destination, instruction._destinationType);
- int32 additional = getVariableOrConstant(instruction._additional, instruction._additionalType);
- bool hasDestination = instruction._destinationType != Token::UNKNOWN;
- switch (op) {
- case Token::NOP: case Token::ENDIF:
+ const FCLInstruction &instruction = (*script.code)[ip];
+ switch (instruction.getType()) {
+ case Token::NOP:
+ case Token::ENDIF:
break;
case Token::IF:
script.predicate = true;
script.booleanOp = Token::UNKNOWN;
break;
- case Token::AND: case Token::OR:
+ case Token::AND:
+ case Token::OR:
script.previousPredicate = script.predicate;
- script.booleanOp = op;
- break;
- case Token::THEN: case Token::ELSE:
- if (op == Token::ELSE || !script.predicate) {
- int depth = 0;
- while (script.ip < code.size()) {
- Token::Type next = code[script.ip++].getType();
- if (next == Token::IF)
- depth++;
- else if (next == Token::ENDIF) {
- if (!depth)
- break;
- depth--;
- } else if (next == Token::ELSE && !depth && op == Token::THEN) {
- break;
- }
- }
- }
+ script.booleanOp = instruction.getType();
+ break;
+ case Token::THEN:
+ case Token::ELSE:
+ executeIfThenElse(instruction, script);
break;
case Token::CONDITIONAL:
- // RUNVGA retains these flags until the object's execution yields or ends.
- setScriptPredicate(script, checkConditional(instruction,
- script.events & 16, script.events & 32, _timerTriggered, script.events & 8));
- break;
- case Token::SETVAR: case Token::ADDVAR: case Token::SUBVAR: case Token::ANDV: case Token::ORV: case Token::NOTV: {
- uint32 value = destination;
- switch (op) {
- case Token::ADDVAR: value += uint32(source); break;
- case Token::SUBVAR: value = uint32(source) - value; break;
- case Token::ANDV: value &= uint32(source); break;
- case Token::ORV: value |= uint32(source); break;
- case Token::NOTV: value = ~uint32(source); break;
- default: break;
- }
- if (instruction._sourceType == Token::VARIABLE)
- setScriptVariable(instruction._source, value);
- setScriptPredicate(script, value != 0);
+ executeConditional(instruction, script);
break;
- }
- case Token::VAREQ: case Token::VARGT: case Token::VARLT:
- setScriptPredicate(script, op == Token::VAREQ ? source == destination : op == Token::VARGT ? source > destination : source < destination);
- break;
- case Token::INVIS: case Token::VIS: case Token::TOGVIS: case Token::DESTROY:
- case Token::INVISQ: case Token::VISQ: case Token::DESTROYEDQ: {
- uint16 area = hasDestination && source ? source : _currentArea->getAreaID();
- uint16 object = hasDestination ? destination : source;
- if (op == Token::INVIS || op == Token::VIS || op == Token::TOGVIS || op == Token::DESTROY)
- setObjectStatus(area, object, op);
- else {
- Common::Array<uint16> objects;
- collectObjects(area, object, objects);
- bool result = false;
- for (uint16 member : objects) {
- byte flags = scriptObject(area, member)->flags;
- result = op == Token::DESTROYEDQ ? (flags & 1) != 0 : op == Token::INVISQ ? (flags & 2) != 0 : !(flags & 2);
- }
- setScriptPredicate(script, result);
- }
+ case Token::SETVAR:
+ executeSetVariable(instruction, script);
break;
- }
- case Token::GETXPOS: case Token::GETYPOS: case Token::GETZPOS: {
- ObjectData *object = scriptObject(additional, destination);
- if (object)
- setScriptVariable(instruction._source & 0xff, int32(object->origin.getValue(op - Token::GETXPOS)));
+ case Token::ADDVAR:
+ executeIncrementVariable(instruction, script);
break;
- }
- case Token::EXECUTE: {
- ObjectData *object = scriptObject(0, source);
- if (!object || object->type == kGroupType)
- return kFCLFinished;
- // EXECUTE replaces the code, retaining the original object's event flags.
- script.code = &object->condition;
- script.ip = 0;
- script.loops.clear();
+ case Token::SUBVAR:
+ executeDecrementVariable(instruction, script);
break;
- }
- case Token::GOTO: {
- uint16 area = instruction._sourceType != Token::UNKNOWN ? source : _currentArea->getAreaID();
- if (!_areaMap.contains(area) || !_areaMap[area]->entranceWithID(destination & 0x7fff)) {
- warning("Invalid 3D Construction Kit GOTO (%d, %u)", destination, area);
+ case Token::ANDV:
+ executeAndVariable(instruction, script);
+ break;
+ case Token::ORV:
+ executeOrVariable(instruction, script);
+ break;
+ case Token::NOTV:
+ executeNotVariable(instruction, script);
+ break;
+ case Token::VAREQ:
+ case Token::VARGT:
+ case Token::VARLT:
+ executeVariableComparison(instruction, script);
+ break;
+ case Token::INVIS:
+ case Token::VIS:
+ case Token::TOGVIS:
+ case Token::DESTROY:
+ executeObjectStatus(instruction);
+ break;
+ case Token::INVISQ:
+ case Token::VISQ:
+ case Token::DESTROYEDQ:
+ setScriptPredicate(script, checkObjectStatus(instruction));
+ break;
+ case Token::GETXPOS:
+ case Token::GETYPOS:
+ case Token::GETZPOS:
+ executeGetPosition(instruction);
+ break;
+ case Token::EXECUTE:
+ if (!executeExecute(instruction, script))
return kFCLFinished;
- }
- writeSystemVariables();
- uint16 previous = _currentArea->getAreaID();
- gotoArea(area, destination & 0x7fff);
- if (previous != area)
+ break;
+ case Token::GOTO:
+ if (!executeGoto(instruction))
return kFCLFinished;
break;
- }
case Token::MODE:
- setScriptVariable(6, CLIP<int32>(source, 1, 3) - 1);
+ executeMode(instruction);
break;
case Token::ENDGAME:
_gameStateControl = kFreescapeGameStateRestart;
@@ -433,59 +396,30 @@ FCLExecutionResult KitEngine::executeCode(ScriptState &script, uint &budget) {
case Token::WAIT:
return kFCLYielded;
case Token::DELAY:
- if (uint16(source)) {
- _delayUntil = _scriptTicks + uint16(source);
- _scriptDelayed = true;
+ if (executeDelay(instruction))
return kFCLPaused;
- }
break;
case Token::REDRAW:
_scriptSurface.fillRect(_viewArea, 0);
return animator ? kFCLYielded : kFCLPaused;
- case Token::LOOP: {
- int depth = 0;
- uint32 end = script.ip;
- while (end < code.size()) {
- Token::Type next = code[end].getType();
- if (next == Token::LOOP)
- depth++;
- else if (next == Token::AGAIN) {
- if (!depth)
- break;
- depth--;
- }
- end++;
- }
- FCLLoop &loop = script.loops[end];
- loop.start = script.ip;
- loop.remaining = uint16(source);
+ case Token::LOOP:
+ executeLoop(instruction, script);
break;
- }
- case Token::AGAIN: {
- auto loop = script.loops.find(ip);
- if (loop == script.loops.end())
+ case Token::AGAIN:
+ if (!executeAgain(script, ip))
return kFCLFinished;
- if (--loop->_value.remaining != 0) {
- script.ip = loop->_value.start;
- } else {
- script.loops.erase(loop);
- }
break;
- }
- case Token::STARTANIM: case Token::STOPANIM: case Token::TRIGANIM: {
- ObjectData *object = scriptObject(hasDestination ? source : 0, uint16(hasDestination ? destination : source) | 0x4000);
- if (object && object->type == 16) {
- if (op == Token::STARTANIM)
- object->flags &= ~2;
- else if (op == Token::STOPANIM)
- object->flags |= 2;
- else
- object->flags |= 1;
- }
- if (animator && op == Token::STOPANIM)
+ case Token::STARTANIM:
+ executeStartAnim(instruction);
+ break;
+ case Token::STOPANIM:
+ executeStopAnim(instruction);
+ if (animator)
return kFCLYielded;
break;
- }
+ case Token::TRIGANIM:
+ executeTriggerAnim(instruction);
+ break;
case Token::START:
if (animator)
script.restart = script.ip;
@@ -494,65 +428,318 @@ FCLExecutionResult KitEngine::executeCode(ScriptState &script, uint &budget) {
if (animator)
script.ip = script.restart;
break;
- case Token::INCLUDE: case Token::REMOVE:
- if (animator) {
- Common::Array<uint16> objects;
- collectObjects(script.area, source, objects);
- for (uint16 member : objects) {
- ObjectData &object = *scriptObject(script.area, member);
- if (!(object.flags & 0x80))
- continue;
- if (op == Token::INCLUDE && !object.animator) {
- object.animator = script.object->id;
- script.object->animatedObjects.push_back(member);
- } else if (op == Token::REMOVE && object.animator == script.object->id) {
- auto &members = script.object->animatedObjects;
- for (uint i = 0; i < members.size(); i++) {
- if (members[i] == member) {
- members.remove_at(i);
- break;
- }
- }
- object.animator = 0;
- }
- }
- }
+ case Token::INCLUDE:
+ if (animator)
+ executeInclude(instruction, script);
+ break;
+ case Token::REMOVE:
+ if (animator)
+ executeRemove(instruction, script);
break;
case Token::WAITTRIG:
- if (animator) {
- if (!(script.object->flags & 1)) {
- script.ip = ip;
- return kFCLYielded;
- }
- script.object->flags &= ~1;
- }
+ if (animator && executeWaitTrigger(script, ip))
+ return kFCLYielded;
break;
- case Token::MOVE: case Token::MOVETO:
+ case Token::MOVE:
+ case Token::MOVETO:
if (animator) {
- setScriptPredicate(script, moveAnimation(script,
- Math::Vector3d(int16(source), int16(destination), int16(additional)), op == Token::MOVETO));
+ executeMove(instruction, script);
return kFCLYielded;
}
break;
case Token::PRINT:
- printMessage(source, instruction._text);
+ printMessage(getVariableOrConstant(instruction._source, instruction._sourceType), instruction._text);
break;
case Token::UPDATEI:
updateIndicators();
break;
- case Token::SOUND: case Token::SYNCSND:
- if (!_soundWarning) {
- warning("3D Construction Kit sound playback is not implemented");
- _soundWarning = true;
- }
+ case Token::SOUND:
+ case Token::SYNCSND:
+ executeSound(instruction);
break;
default:
- error("Unhandled 16-bit FCL instruction %d at ip: %u", op, ip);
+ error("Unhandled 16-bit FCL instruction %d at ip: %u", instruction.getType(), ip);
}
}
return kFCLFinished;
}
+void KitEngine::executeIfThenElse(const FCLInstruction &instruction, ScriptState &script) {
+ if (instruction.getType() == Token::THEN && script.predicate)
+ return;
+ const FCLInstructionVector &code = *script.code;
+ int depth = 0;
+ while (script.ip < code.size()) {
+ Token::Type next = code[script.ip++].getType();
+ if (next == Token::IF) {
+ depth++;
+ } else if (next == Token::ENDIF) {
+ if (!depth)
+ break;
+ depth--;
+ } else if (next == Token::ELSE && !depth && instruction.getType() == Token::THEN) {
+ break;
+ }
+ }
+}
+
+void KitEngine::executeConditional(const FCLInstruction &instruction, ScriptState &script) {
+ // RUNVGA retains these flags until the object's execution yields or ends.
+ setScriptPredicate(script, checkConditional(instruction,
+ script.events & 16, script.events & 32, _timerTriggered, script.events & 8));
+}
+
+void KitEngine::setVariableResult(const FCLInstruction &instruction, ScriptState &script, uint32 value) {
+ if (instruction._sourceType == Token::VARIABLE)
+ setScriptVariable(instruction._source, value);
+ setScriptPredicate(script, value != 0);
+}
+
+void KitEngine::executeSetVariable(const FCLInstruction &instruction, ScriptState &script) {
+ uint32 value = getVariableOrConstant(instruction._destination, instruction._destinationType);
+ setVariableResult(instruction, script, value);
+}
+
+void KitEngine::executeIncrementVariable(const FCLInstruction &instruction, ScriptState &script) {
+ uint32 source = getVariableOrConstant(instruction._source, instruction._sourceType);
+ uint32 destination = getVariableOrConstant(instruction._destination, instruction._destinationType);
+ setVariableResult(instruction, script, source + destination);
+}
+
+void KitEngine::executeDecrementVariable(const FCLInstruction &instruction, ScriptState &script) {
+ uint32 source = getVariableOrConstant(instruction._source, instruction._sourceType);
+ uint32 destination = getVariableOrConstant(instruction._destination, instruction._destinationType);
+ setVariableResult(instruction, script, source - destination);
+}
+
+void KitEngine::executeAndVariable(const FCLInstruction &instruction, ScriptState &script) {
+ uint32 source = getVariableOrConstant(instruction._source, instruction._sourceType);
+ uint32 destination = getVariableOrConstant(instruction._destination, instruction._destinationType);
+ setVariableResult(instruction, script, source & destination);
+}
+
+void KitEngine::executeOrVariable(const FCLInstruction &instruction, ScriptState &script) {
+ uint32 source = getVariableOrConstant(instruction._source, instruction._sourceType);
+ uint32 destination = getVariableOrConstant(instruction._destination, instruction._destinationType);
+ setVariableResult(instruction, script, source | destination);
+}
+
+void KitEngine::executeNotVariable(const FCLInstruction &instruction, ScriptState &script) {
+ uint32 value = getVariableOrConstant(instruction._source, instruction._sourceType);
+ setVariableResult(instruction, script, ~value);
+}
+
+void KitEngine::executeVariableComparison(const FCLInstruction &instruction, ScriptState &script) {
+ int32 source = getVariableOrConstant(instruction._source, instruction._sourceType);
+ int32 destination = getVariableOrConstant(instruction._destination, instruction._destinationType);
+ switch (instruction.getType()) {
+ case Token::VAREQ:
+ setScriptPredicate(script, source == destination);
+ break;
+ case Token::VARGT:
+ setScriptPredicate(script, source > destination);
+ break;
+ case Token::VARLT:
+ setScriptPredicate(script, source < destination);
+ break;
+ default:
+ break;
+ }
+}
+
+void KitEngine::getObjectReference(const FCLInstruction &instruction, uint16 &area, uint16 &id) const {
+ int32 source = getVariableOrConstant(instruction._source, instruction._sourceType);
+ area = _currentArea->getAreaID();
+ id = source;
+ if (instruction._destinationType != Token::UNKNOWN) {
+ if (source)
+ area = source;
+ id = getVariableOrConstant(instruction._destination, instruction._destinationType);
+ }
+}
+
+void KitEngine::executeObjectStatus(const FCLInstruction &instruction) {
+ uint16 area, id;
+ getObjectReference(instruction, area, id);
+ setObjectStatus(area, id, instruction.getType());
+}
+
+bool KitEngine::checkObjectStatus(const FCLInstruction &instruction) {
+ uint16 area, id;
+ getObjectReference(instruction, area, id);
+ Common::Array<uint16> objects;
+ collectObjects(area, id, objects);
+ byte mask = instruction.getType() == Token::DESTROYEDQ ? 1 : 2;
+ bool expected = instruction.getType() != Token::VISQ;
+ bool result = false;
+ for (uint16 member : objects)
+ result = ((scriptObject(area, member)->flags & mask) != 0) == expected;
+ return result;
+}
+
+void KitEngine::executeGetPosition(const FCLInstruction &instruction) {
+ uint16 area = getVariableOrConstant(instruction._additional, instruction._additionalType);
+ uint16 id = getVariableOrConstant(instruction._destination, instruction._destinationType);
+ ObjectData *object = scriptObject(area, id);
+ if (object) {
+ int axis = instruction.getType() - Token::GETXPOS;
+ setScriptVariable(instruction._source & 0xff, int32(object->origin.getValue(axis)));
+ }
+}
+
+bool KitEngine::executeExecute(const FCLInstruction &instruction, ScriptState &script) {
+ uint16 id = getVariableOrConstant(instruction._source, instruction._sourceType);
+ ObjectData *object = scriptObject(0, id);
+ if (!object || object->type == kGroupType)
+ return false;
+ // EXECUTE replaces the code, retaining the original object's event flags.
+ script.code = &object->condition;
+ script.ip = 0;
+ script.loops.clear();
+ return true;
+}
+
+bool KitEngine::executeGoto(const FCLInstruction &instruction) {
+ uint16 area = _currentArea->getAreaID();
+ if (instruction._sourceType != Token::UNKNOWN)
+ area = getVariableOrConstant(instruction._source, instruction._sourceType);
+ int32 entrance = getVariableOrConstant(instruction._destination, instruction._destinationType);
+ if (!_areaMap.contains(area) || !_areaMap[area]->entranceWithID(entrance & 0x7fff)) {
+ warning("Invalid 3D Construction Kit GOTO (%d, %u)", entrance, area);
+ return false;
+ }
+ writeSystemVariables();
+ uint16 previous = _currentArea->getAreaID();
+ gotoArea(area, entrance & 0x7fff);
+ return previous == area;
+}
+
+void KitEngine::executeMode(const FCLInstruction &instruction) {
+ int32 mode = getVariableOrConstant(instruction._source, instruction._sourceType);
+ setScriptVariable(6, CLIP<int32>(mode, 1, 3) - 1);
+}
+
+bool KitEngine::executeDelay(const FCLInstruction &instruction) {
+ uint16 delay = getVariableOrConstant(instruction._source, instruction._sourceType);
+ if (!delay)
+ return false;
+ _delayUntil = _scriptTicks + delay;
+ _scriptDelayed = true;
+ return true;
+}
+
+void KitEngine::executeLoop(const FCLInstruction &instruction, ScriptState &script) {
+ const FCLInstructionVector &code = *script.code;
+ int depth = 0;
+ uint32 end = script.ip;
+ while (end < code.size()) {
+ Token::Type next = code[end].getType();
+ if (next == Token::LOOP) {
+ depth++;
+ } else if (next == Token::AGAIN) {
+ if (!depth)
+ break;
+ depth--;
+ }
+ end++;
+ }
+ FCLLoop &loop = script.loops[end];
+ loop.start = script.ip;
+ loop.remaining = getVariableOrConstant(instruction._source, instruction._sourceType);
+}
+
+bool KitEngine::executeAgain(ScriptState &script, uint32 ip) {
+ auto loop = script.loops.find(ip);
+ if (loop == script.loops.end())
+ return false;
+ if (--loop->_value.remaining != 0)
+ script.ip = loop->_value.start;
+ else
+ script.loops.erase(loop);
+ return true;
+}
+
+KitEngine::ObjectData *KitEngine::scriptAnimator(const FCLInstruction &instruction) {
+ uint16 area, id;
+ getObjectReference(instruction, area, id);
+ ObjectData *object = scriptObject(area, id | 0x4000);
+ return object && object->type == 16 ? object : nullptr;
+}
+
+void KitEngine::executeStartAnim(const FCLInstruction &instruction) {
+ ObjectData *object = scriptAnimator(instruction);
+ if (object)
+ object->flags &= ~2;
+}
+
+void KitEngine::executeStopAnim(const FCLInstruction &instruction) {
+ ObjectData *object = scriptAnimator(instruction);
+ if (object)
+ object->flags |= 2;
+}
+
+void KitEngine::executeTriggerAnim(const FCLInstruction &instruction) {
+ ObjectData *object = scriptAnimator(instruction);
+ if (object)
+ object->flags |= 1;
+}
+
+void KitEngine::executeInclude(const FCLInstruction &instruction, ScriptState &script) {
+ uint16 id = getVariableOrConstant(instruction._source, instruction._sourceType);
+ Common::Array<uint16> objects;
+ collectObjects(script.area, id, objects);
+ for (uint16 member : objects) {
+ ObjectData &object = *scriptObject(script.area, member);
+ if (!(object.flags & 0x80) || object.animator)
+ continue;
+ object.animator = script.object->id;
+ script.object->animatedObjects.push_back(member);
+ }
+}
+
+void KitEngine::executeRemove(const FCLInstruction &instruction, ScriptState &script) {
+ uint16 id = getVariableOrConstant(instruction._source, instruction._sourceType);
+ Common::Array<uint16> objects;
+ collectObjects(script.area, id, objects);
+ for (uint16 member : objects) {
+ ObjectData &object = *scriptObject(script.area, member);
+ if (!(object.flags & 0x80) || object.animator != script.object->id)
+ continue;
+ auto &members = script.object->animatedObjects;
+ for (uint i = 0; i < members.size(); i++) {
+ if (members[i] == member) {
+ members.remove_at(i);
+ break;
+ }
+ }
+ object.animator = 0;
+ }
+}
+
+bool KitEngine::executeWaitTrigger(ScriptState &script, uint32 ip) {
+ if (!(script.object->flags & 1)) {
+ script.ip = ip;
+ return true;
+ }
+ script.object->flags &= ~1;
+ return false;
+}
+
+void KitEngine::executeMove(const FCLInstruction &instruction, ScriptState &script) {
+ int16 x = getVariableOrConstant(instruction._source, instruction._sourceType);
+ int16 y = getVariableOrConstant(instruction._destination, instruction._destinationType);
+ int16 z = getVariableOrConstant(instruction._additional, instruction._additionalType);
+ bool absolute = instruction.getType() == Token::MOVETO;
+ setScriptPredicate(script, moveAnimation(script, Math::Vector3d(x, y, z), absolute));
+}
+
+void KitEngine::executeSound(const FCLInstruction &instruction) {
+ if (!_soundWarning) {
+ warning("3D Construction Kit sound playback is not implemented");
+ _soundWarning = true;
+ }
+}
+
bool KitEngine::executeObjectConditions(GeometricObject *obj, bool shot, bool collided, bool activated) {
ObjectData *object = scriptObject(0, obj->getObjectID());
if (object)
Commit: effb13c82171488c60039c446f493f164a24efa5
https://github.com/scummvm/scummvm/commit/effb13c82171488c60039c446f493f164a24efa5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-07T07:23:53+02:00
Commit Message:
FREESCAPE: mouselook unlocked for 3dck
Changed paths:
engines/freescape/games/3dck/3dck.cpp
engines/freescape/games/3dck/ui.cpp
diff --git a/engines/freescape/games/3dck/3dck.cpp b/engines/freescape/games/3dck/3dck.cpp
index b11e91cda74..6c80aafa23c 100644
--- a/engines/freescape/games/3dck/3dck.cpp
+++ b/engines/freescape/games/3dck/3dck.cpp
@@ -413,8 +413,7 @@ void KitEngine::gotoArea(uint16 areaID, int entranceID) {
_timerTicks = 0;
_scriptSurface.fillRect(_viewArea, 0);
resetInput();
- _shootMode = true;
- g_system->lockMouse(false);
+ g_system->lockMouse(true);
readSystemVariables();
}
diff --git a/engines/freescape/games/3dck/ui.cpp b/engines/freescape/games/3dck/ui.cpp
index 76757e87e9f..ce116adce4f 100644
--- a/engines/freescape/games/3dck/ui.cpp
+++ b/engines/freescape/games/3dck/ui.cpp
@@ -119,7 +119,10 @@ bool KitEngine::handleInput(const Common::Event &event) {
switch (event.customType) {
case kActionEscape:
return false;
- case kActionChangeMode: case kActionSkip:
+ case kActionChangeMode:
+ _kitVariables[15] = ' ';
+ return false;
+ case kActionSkip:
_kitVariables[15] = ' ';
return true;
case kActionMoveUp: control = 0; break;
@@ -148,20 +151,24 @@ bool KitEngine::handleInput(const Common::Event &event) {
interact(event.customType == kActionShoot);
return true;
}
- if (_scriptFrameActive)
- return true;
+ // Track held movement keys during DELAY; movement itself waits.
+ bool movement = event.customType == kActionMoveUp || event.customType == kActionMoveDown ||
+ event.customType == kActionMoveLeft || event.customType == kActionMoveRight;
+ return _scriptFrameActive && !movement;
} else if (_scriptFrameActive && event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_END &&
(event.customType == kActionRiseOrFlyUp || event.customType == kActionLowerOrFlyDown)) {
_moveUp = _moveDown = false;
return true;
} else if (event.type == Common::EVENT_LBUTTONDOWN || event.type == Common::EVENT_RBUTTONDOWN) {
- _crossairPosition = getNormalizedPosition(event.mouse);
+ Common::Point mouse = getNormalizedPosition(event.mouse);
+ if (_shootMode)
+ _crossairPosition = mouse;
int buttons = g_system->getEventManager()->getButtonState();
_kitVariables[16] = ((buttons & Common::EventManager::LBUTTON) ? 1 : 0) |
((buttons & Common::EventManager::RBUTTON) ? 2 : 0);
_kitVariables[16] |= event.type == Common::EVENT_LBUTTONDOWN ? 1 : 2;
- _kitVariables[17] = _crossairPosition.x;
- _kitVariables[18] = _crossairPosition.y;
+ _kitVariables[17] = mouse.x;
+ _kitVariables[18] = mouse.y;
if (!_scriptFrameActive)
interact(event.type == Common::EVENT_LBUTTONDOWN);
return true;
Commit: 01bb2300a0026e1c0892189d900c939d867aa375
https://github.com/scummvm/scummvm/commit/01bb2300a0026e1c0892189d900c939d867aa375
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-07T07:23:53+02:00
Commit Message:
FREESCAPE: added 8bit opcodes and scripting for 3dck
Changed paths:
A engines/freescape/games/3dck/8bit.cpp
A engines/freescape/games/3dck/8bit.h
A engines/freescape/games/3dck/8bitUI.cpp
A engines/freescape/language/8bitKitDetokeniser.cpp
A engines/freescape/language/8bitKitDetokeniser.h
A engines/freescape/language/instruction8bitKit.cpp
engines/freescape/detection.cpp
engines/freescape/gfx.cpp
engines/freescape/gfx.h
engines/freescape/language/instruction.h
engines/freescape/language/token.h
engines/freescape/metaengine.cpp
engines/freescape/module.mk
diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index 17be9887e18..cbd4f199658 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -1156,6 +1156,15 @@ const ADGameDescription gameDescriptions[] = {
GUIO3(GUIO_NOMIDI, GUIO_RENDEREGA, GAMEOPTION_WASD_CONTROLS)
},
// 3D Construction Kit games
+ {
+ "3dkit",
+ "A Chance in Hell",
+ AD_ENTRY1s("Datafile0.bin", "8b4d53e7758b69a8df43947baddcf94a", 5589),
+ Common::EN_ANY,
+ Common::kPlatformAmstradCPC,
+ ADGF_UNSTABLE,
+ GUIO2(GUIO_NOMIDI, GUIO_RENDERCPC)
+ },
{
"3dkit",
"Cube",
diff --git a/engines/freescape/games/3dck/8bit.cpp b/engines/freescape/games/3dck/8bit.cpp
new file mode 100644
index 00000000000..aac773e5d14
--- /dev/null
+++ b/engines/freescape/games/3dck/8bit.cpp
@@ -0,0 +1,384 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/algorithm.h"
+#include "common/substream.h"
+#include "math/utils.h"
+
+#include "freescape/games/3dck/8bit.h"
+#include "freescape/language/8bitKitDetokeniser.h"
+
+namespace Freescape {
+
+static void requireBytes(Common::SeekableReadStream &file, uint32 size) {
+ if (file.err() || file.pos() > file.size() || size > file.size() - file.pos())
+ error("Truncated 8-bit 3D Construction Kit data");
+}
+
+Kit8Engine::Kit8Engine(OSystem *syst, const ADGameDescription *gd) : FreescapeEngine(syst, gd) {
+ _screenW = 320;
+ _screenH = 200;
+ _fullscreenViewArea = Common::Rect(_screenW, _screenH);
+ _playerHeightNumber = _playerHeightMaxNumber = 0;
+ _playerWidth = _playerDepth = 16;
+ _soundIndexShoot = -1;
+}
+
+void Kit8Engine::loadAssets() {
+ Common::File file;
+ if (!file.open(_gameDescription->filesDescriptions[0].fileName))
+ error("Unable to open 8-bit 3D Construction Kit data");
+ requireBytes(file, 160);
+ if (file.readUint32BE() != MKTAG('K', 'I', 'T', 'C') || file.readUint16LE() != file.size())
+ error("Unsupported 8-bit 3D Construction Kit data format");
+ uint16 procedures = file.readUint16LE();
+ uint16 conditions = file.readUint16LE();
+ uint16 messages = file.readUint16LE();
+ byte areaCount = file.readByte();
+ _climbHeight = file.readByte();
+ _fallHeight = file.readByte();
+ _walkSpeed = file.readByte();
+ byte turnSpeed = file.readByte();
+ _startArea = file.readByte();
+ _startEntrance = file.readByte();
+ file.skip(1);
+ _activationRange = file.readByte();
+ int x = 8 * file.readByte();
+ int y = 8 * file.readByte();
+ int width = 8 * file.readByte();
+ int height = 8 * file.readByte();
+ if (!width || !height || x + width > _screenW || y + height > _screenH || !_walkSpeed || !turnSpeed)
+ error("Invalid 8-bit 3D Construction Kit display or movement settings");
+ _viewArea = Common::Rect(x, y, x + width, y + height);
+ // CPC projection scales: 125 * 64 / (extent - 1), with a depth scale of 18.
+ int xScale = 8000 / (width - 1);
+ int yScale = 8000 / (height - 1);
+ if (xScale > 127 || yScale > 127)
+ error("Unsupported 8-bit 3D Construction Kit viewport size");
+ _fieldOfView = 2 * Math::rad2deg(atan(18.0f / xScale));
+ _viewAspectRatio = float(yScale) / xScale;
+ _angleRotations.clear();
+ _angleRotations.push_back(5 * turnSpeed);
+ _angleRotationIndex = 0;
+ _playerSteps.clear();
+ _playerSteps.push_back(_walkSpeed);
+ _playerStepIndex = 0;
+ file.seek(0x30);
+ file.read(_instruments, sizeof(_instruments));
+ file.seek(0xa0);
+ requireBytes(file, 2 * areaCount);
+ Common::Array<uint16> offsets;
+ for (uint i = 0; i < areaCount; i++)
+ offsets.push_back(file.readUint16LE());
+ Common::sort(offsets.begin(), offsets.end());
+ if (offsets.empty() || offsets.back() > file.size() - 12 || messages < file.pos() ||
+ messages >= procedures || procedures >= conditions || conditions >= offsets.front())
+ error("Invalid 8-bit 3D Construction Kit table offsets");
+
+ Common::SeekableSubReadStream messageData(&file, messages, procedures);
+ requireBytes(messageData, 1);
+ byte messageCount = messageData.readByte();
+ for (uint i = 0; i < messageCount; i++) {
+ requireBytes(messageData, 2);
+ byte id = messageData.readByte();
+ byte length = messageData.readByte();
+ requireBytes(messageData, length);
+ if (_kitMessages.contains(id))
+ error("Duplicate 8-bit 3D Construction Kit message %u", id);
+ Common::String text;
+ while (length--)
+ text += char(messageData.readByte());
+ _kitMessages[id] = text;
+ }
+ Common::SeekableSubReadStream procedureData(&file, procedures, conditions);
+ _procedures = loadConditions(procedureData);
+ Common::SeekableSubReadStream conditionData(&file, conditions, offsets.front());
+ _globalConditions = loadConditions(conditionData);
+ for (uint i = 0; i < offsets.size(); i++) {
+ uint32 end = i + 1 < offsets.size() ? offsets[i + 1] : file.size();
+ if (offsets[i] >= end || end > uint32(file.size()))
+ error("Invalid 8-bit 3D Construction Kit area offset");
+ Common::SeekableSubReadStream areaData(&file, offsets[i], end);
+ Area *area = loadArea(areaData);
+ if (_areaMap.contains(area->getAreaID()))
+ error("Duplicate 8-bit 3D Construction Kit area %u", area->getAreaID());
+ _areaMap[area->getAreaID()] = area;
+ }
+ for (auto &entry : _areaMap) {
+ if (entry._key == 255)
+ continue;
+ for (byte id : _areaData[entry._key].globals) {
+ if (!_areaMap.contains(255) || !_areaMap[255]->objectWithID(id) || entry._value->objectWithID(id))
+ error("Invalid 8-bit 3D Construction Kit global object %u", id);
+ entry._value->addObjectFromArea(id, _areaMap[255]);
+ }
+ }
+ if (!_areaMap.contains(_startArea) || !_areaMap[_startArea]->entranceWithID(_startEntrance))
+ error("Invalid 8-bit 3D Construction Kit starting entrance");
+ loadPresentation();
+}
+
+Common::Array<Kit8Engine::ConditionData> Kit8Engine::loadConditions(Common::SeekableReadStream &file) {
+ requireBytes(file, 1);
+ byte count = file.readByte();
+ Common::Array<ConditionData> conditions;
+ for (uint i = 0; i < count; i++) {
+ requireBytes(file, 2);
+ ConditionData condition;
+ condition.id = file.readByte();
+ byte length = file.readByte();
+ requireBytes(file, length);
+ for (const auto &existing : conditions) {
+ if (existing.id == condition.id)
+ error("Duplicate 8-bit 3D Construction Kit condition %u", condition.id);
+ }
+ Common::Array<byte> code;
+ code.resize(length);
+ file.read(code.data(), length);
+ Common::String source = detokenise8bitKitCondition(code, condition.code);
+ debugC(1, kFreescapeDebugParser, "Condition %u:\n%s", condition.id, source.c_str());
+ conditions.push_back(condition);
+ }
+ return conditions;
+}
+
+Area *Kit8Engine::loadArea(Common::SeekableReadStream &file) {
+ requireBytes(file, 12);
+ byte id = file.readByte();
+ byte count = file.readByte();
+ byte flags = file.readByte();
+ uint16 conditions = file.readUint16LE();
+ file.skip(2);
+ AreaData &data = _areaData[id];
+ for (uint i = 0; i < 4; i++) {
+ data.palette[i] = file.readByte();
+ if (data.palette[i] > 26)
+ error("Invalid 8-bit 3D Construction Kit palette");
+ }
+ byte scale = file.readByte();
+ if (!scale || conditions < file.pos() || conditions > file.size())
+ error("Invalid 8-bit 3D Construction Kit area header");
+ ObjectMap *objects = new ObjectMap;
+ ObjectMap *entrances = new ObjectMap;
+ for (uint i = 0; i < count; i++) {
+ requireBytes(file, 9);
+ uint32 start = file.pos();
+ byte header[9];
+ file.read(header, sizeof(header));
+ byte type = header[0] & 0x0f;
+ byte objectID = header[7];
+ byte size = header[8];
+ if (size < 9 || start + size > conditions)
+ error("Invalid 8-bit 3D Construction Kit object size");
+ if (objectID == 255) {
+ for (uint j = 9; j < size; j++)
+ data.globals.push_back(file.readByte());
+ continue;
+ }
+ if (objects->contains(objectID) || entrances->contains(objectID))
+ error("Duplicate 8-bit 3D Construction Kit object %u", objectID);
+ Object *object = nullptr;
+ if (type == kEntranceType) {
+ if (size != 12)
+ error("Invalid 8-bit 3D Construction Kit entrance size");
+ Math::Vector3d origin, rotation;
+ for (int axis = 0; axis < 3; axis++) {
+ byte fine = file.readByte();
+ origin.setValue(axis, header[axis + 1] == 255 ? -1 : 32 * header[axis + 1] + fine / 2.0f);
+ rotation.setValue(axis, header[axis + 4] == 255 ? -1 : 5 * header[axis + 4]);
+ }
+ object = new Entrance(objectID, origin, rotation, FCLInstructionVector(), "");
+ } else if (type == kSensorType) {
+ if (size != 10)
+ error("Invalid 8-bit 3D Construction Kit sensor size");
+ byte color = file.readByte();
+ object = new Sensor(objectID, Math::Vector3d(32 * header[1], 32 * header[2], 32 * header[3]),
+ Math::Vector3d(), color & 15, header[5], 32 * header[4], 0, header[0], FCLInstructionVector(), "");
+ } else
+ object = loadGeometricObject(file, header);
+ file.seek(start + size);
+ object->_loadIndex = (id == 255 ? 0 : 0x4000) + i;
+ if (id != 255)
+ object->scale(scale);
+ if (type == kEntranceType)
+ (*entrances)[objectID] = object;
+ else
+ (*objects)[objectID] = object;
+ }
+ if (file.pos() != conditions)
+ error("Invalid 8-bit 3D Construction Kit object table length");
+ if (id != 255)
+ data.conditions = loadConditions(file);
+ Area *area = new Area(id, flags, objects, entrances, false);
+ area->_scale = scale;
+ area->_name = Common::String::format("Area %u", id);
+ area->_groundColor = 255;
+ return area;
+}
+
+GeometricObject *Kit8Engine::loadGeometricObject(Common::SeekableReadStream &file, const byte header[9]) {
+ ObjectType type = ObjectType(header[0] & 0x0f);
+ int colorCount = GeometricObject::numberOfColoursForObjectOfType(type);
+ int ordinateCount = GeometricObject::numberOfOrdinatesForType(type);
+ if (type > kHexagonType || header[8] != 9 + colorCount / 2 + ordinateCount)
+ error("Unsupported 8-bit 3D Construction Kit object type %u", type);
+ requireBytes(file, colorCount / 2 + ordinateCount);
+ Math::Vector3d origin(32 * header[1], 32 * header[2], 32 * header[3]);
+ Math::Vector3d size(32 * header[4], 32 * header[5], 32 * header[6]);
+ Common::Array<byte> *colors = new Common::Array<byte>;
+ for (int i = 0; i < colorCount / 2; i++) {
+ byte color = file.readByte();
+ colors->push_back(color & 15);
+ colors->push_back(color >> 4);
+ }
+ Common::Array<float> *ordinates = nullptr;
+ if (ordinateCount) {
+ static const byte pyramidAxes[3][2] = {{1, 2}, {0, 2}, {0, 1}};
+ ordinates = new Common::Array<float>;
+ for (int i = 0; i < ordinateCount; i++) {
+ int axis = i % 3;
+ if (GeometricObject::isPyramid(type))
+ axis = pyramidAxes[(type - kEastPyramidType) / 2][i % 2];
+ // Kit points use 1/64 of the corresponding bounding-box dimension.
+ float ordinate = size.getValue(axis) * file.readByte() / 64.0f;
+ if (GeometricObject::isPolygon(type))
+ ordinate += origin.getValue(axis);
+ ordinates->push_back(ordinate);
+ }
+ }
+ return new GeometricObject(type, header[7], header[0], origin, size,
+ colors, nullptr, ordinates, FCLInstructionVector(), "");
+}
+
+void Kit8Engine::initGameState() {
+ FreescapeEngine::initGameState();
+ memset(_variables, 0, sizeof(_variables));
+ _changedVariables = 0;
+ _currentKey = 255;
+ _textColor = 7;
+ _variables[121] = _variables[125] = 255;
+ _variables[127] = 0x9c;
+ _scriptStack.clear();
+ _conditions = nullptr;
+ _initialScriptPending = true;
+ _scriptFrameActive = false;
+ _zero = _carry = _previousZero = false;
+ _shotObject = _hitObject = _activatedObject = 0;
+ _fallen = _crushed = _pendingTimer = _timerTriggered = false;
+ _crossVisible = true;
+ _timerTicks = _timerInterval = _delayUntil = 0;
+ _lastTime = g_system->getMillis();
+ _scriptSurface.fillRect(_fullscreenViewArea, 255);
+ _currentArea = nullptr;
+ _movementMode = 1;
+ _playerHeight = 0;
+}
+
+void Kit8Engine::gotoArea(uint16 areaID, int entranceID) {
+ if (!_areaMap.contains(areaID) || areaID == 255)
+ error("Unknown 8-bit 3D Construction Kit area %u", areaID);
+ float oldScale = _currentArea ? _currentArea->getScale() : 1;
+ _currentArea = _areaMap[areaID];
+ float scale = _currentArea->getScale();
+ Math::Vector3d position = _position * (oldScale / scale);
+ _playerHeight = 0;
+ setMovementMode(_movementMode);
+ _position = position;
+ Entrance *entrance = static_cast<Entrance *>(_currentArea->entranceWithID(entranceID));
+ if (entrance) {
+ Math::Vector3d origin = entrance->getOrigin(), rotation = entrance->getRotation();
+ for (int axis = 0; axis < 3; axis++) {
+ if (origin.getValue(axis) >= 0)
+ _position.setValue(axis, origin.getValue(axis) + (axis == 1 ? _playerHeight : 0));
+ }
+ if (rotation.x() >= 0)
+ _pitch = rotation.x() > 180 ? rotation.x() - 360 : rotation.x();
+ if (rotation.y() >= 0)
+ _yaw = 90 - rotation.y();
+ if (rotation.z() >= 0)
+ _roll = rotation.z();
+ }
+ AreaData &data = _areaData[areaID];
+ memcpy(_palette, data.palette, sizeof(_palette));
+ for (byte id : data.globals) {
+ Object *object = _currentArea->objectWithID(id);
+ object->restore();
+ object->makeVisible();
+ }
+ applyPalette();
+ _sensors = _currentArea->getSensors();
+ _gfx->_scale = scale;
+ _gotoExecuted = true;
+ _lastPosition = _position;
+ float pitch = _pitch;
+ resetInput();
+ _pitch = pitch;
+ updateCamera();
+ g_system->lockMouse(true);
+ readSystemVariables();
+}
+
+void Kit8Engine::setMovementMode(byte mode) {
+ if (mode > 4)
+ return;
+ _position.y() -= _playerHeight;
+ _movementMode = mode;
+ _flyMode = mode >= 3;
+ float scale = _currentArea ? _currentArea->getScale() : 1;
+ _playerHeight = _flyMode ? 0 : (mode == 0 ? 64 : 128) - 26 / scale;
+ _position.y() += _playerHeight;
+ _playerSteps[0] = mode == 0 ? _walkSpeed / 2 : mode == 2 ? 2 * _walkSpeed : _walkSpeed;
+ _stepUpDistance = 32 * _climbHeight;
+ _maxFallingDistance = 32 * _fallHeight;
+ _lastPosition = _position;
+}
+
+void Kit8Engine::checkIfStillInArea() {
+ float scale = _currentArea->getScale();
+ _position.x() = CLIP(_position.x(), 0.0f, 4063.5f / scale);
+ _position.y() = CLIP(_position.y(), 0.0f, 2015.5f / scale);
+ _position.z() = CLIP(_position.z(), 0.0f, 4063.5f / scale);
+}
+
+void Kit8Engine::updatePlayerMovement(float deltaTime) {
+ if (_scriptFrameActive || _initialScriptPending)
+ return;
+ Math::Vector3d front = _cameraFront;
+ if (_movementMode == 3)
+ _cameraFront = directionToVector(0, _yaw, false);
+ FreescapeEngine::updatePlayerMovement(deltaTime);
+ _cameraFront = front;
+}
+
+void Kit8Engine::checkSensors() {
+ // TODO: sensor firing.
+ if (_scriptFrameActive || !_currentArea)
+ return;
+ for (auto *object : _sensors) {
+ Sensor *sensor = static_cast<Sensor *>(object);
+ Math::Vector3d diff = _position - sensor->getOrigin();
+ bool detected = !sensor->isInvisible() && !sensor->isDestroyed() &&
+ ABS(diff.x()) + ABS(diff.y()) + ABS(diff.z()) < sensor->_firingRange;
+ sensor->shouldShoot(detected);
+ }
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/games/3dck/8bit.h b/engines/freescape/games/3dck/8bit.h
new file mode 100644
index 00000000000..541b460b2ab
--- /dev/null
+++ b/engines/freescape/games/3dck/8bit.h
@@ -0,0 +1,122 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef FREESCAPE_GAMES_3DCK_8BIT_H
+#define FREESCAPE_GAMES_3DCK_8BIT_H
+
+#include "freescape/freescape.h"
+
+namespace Freescape {
+
+class Kit8Engine : public FreescapeEngine {
+public:
+ Kit8Engine(OSystem *syst, const ADGameDescription *gd);
+
+ void loadAssets() override;
+ void initGameState() override;
+ void gotoArea(uint16 areaID, int entranceID) override;
+ void checkIfStillInArea() override;
+ bool checkIfGameEnded() override { return false; }
+ void borderScreen() override {}
+ void drawUI() override;
+ bool handleInput(const Common::Event &event) override;
+ void updatePlayerMovement(float deltaTime) override;
+ void updateTimeVariables() override;
+ void checkSensors() override;
+ void updateScripts() override;
+ bool executeObjectConditions(GeometricObject *obj, bool shot, bool collided, bool activated) override;
+ void executeLocalGlobalConditions(bool shot, bool collided, bool timer) override {}
+ bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override { return false; }
+ bool canSaveGameStateCurrently(Common::U32String *msg = nullptr) override { return false; }
+
+private:
+ struct ConditionData {
+ byte id;
+ FCLInstructionVector code;
+ };
+
+ struct AreaData {
+ byte palette[4];
+ Common::Array<byte> globals;
+ Common::Array<ConditionData> conditions;
+ };
+
+ struct ScriptFrame {
+ const FCLInstructionVector *code;
+ uint ip;
+ ScriptFrame(const FCLInstructionVector &instructions) : code(&instructions), ip(0) {}
+ };
+
+ Common::Array<ConditionData> loadConditions(Common::SeekableReadStream &file);
+ Area *loadArea(Common::SeekableReadStream &file);
+ GeometricObject *loadGeometricObject(Common::SeekableReadStream &file, const byte header[9]);
+ void loadPresentation();
+ void applyPalette();
+ void setMovementMode(byte mode);
+ void readSystemVariables();
+ void writeSystemVariables();
+ void startScript(const FCLInstructionVector &code);
+ bool executeCode(uint &budget);
+ void executeArithmetic(const FCLInstruction &instruction);
+ void executeComparison(const FCLInstruction &instruction);
+ void executeConditional(const FCLInstruction &instruction);
+ void executeObjectStatus(const FCLInstruction &instruction);
+ void executeGoto(const FCLInstruction &instruction);
+ void executeMode(const FCLInstruction &instruction);
+ void executeCall(const FCLInstruction &instruction);
+ void executeSound(const FCLInstruction &instruction);
+ void executeColour(const FCLInstruction &instruction);
+ void setPredicate(bool value);
+ Object *scriptObject(uint16 area, uint16 id);
+ void interact(bool shot);
+ void printMessage(byte id, byte x, byte y);
+ void printText(const Common::String &text, byte x, byte y, byte color);
+ void updateInstruments();
+
+ Common::HashMap<uint16, AreaData> _areaData;
+ Common::Array<ConditionData> _globalConditions, _procedures;
+ Common::HashMap<byte, Common::String> _kitMessages;
+ Common::Array<ScriptFrame> _scriptStack;
+ const Common::Array<ConditionData> *_conditions = nullptr;
+ uint _conditionIndex = 0;
+ bool _initialScriptPending = true, _scriptFrameActive = false, _globalPhase = false;
+ bool _executing = true, _zero = false, _carry = false, _previousZero = false;
+ Token::Type _booleanOp = Token::UNKNOWN;
+ byte _variables[128] = {};
+ uint16 _changedVariables = 0;
+ byte _currentKey = 255;
+ byte _palette[4] = {};
+ byte _colorPatterns[15][4] = {};
+ byte _instruments[8][6] = {};
+ byte _textColor = 7, _movementMode = 1;
+ byte _climbHeight = 0, _fallHeight = 0, _walkSpeed = 0, _activationRange = 0;
+ byte _shotObject = 0, _hitObject = 0, _activatedObject = 0;
+ bool _fallen = false, _crushed = false, _crossVisible = true;
+ bool _timerTriggered = false, _pendingTimer = false, _soundWarning = false;
+ uint32 _lastTime = 0, _timerTicks = 0, _timerInterval = 0, _delayUntil = 0;
+ byte _fontData[96][8] = {};
+ bool _hasFont = false;
+ Graphics::ManagedSurface _scriptSurface, _overlaySurface, _borderSurface;
+};
+
+} // namespace Freescape
+
+#endif
diff --git a/engines/freescape/games/3dck/8bitUI.cpp b/engines/freescape/games/3dck/8bitUI.cpp
new file mode 100644
index 00000000000..fb0f707e75f
--- /dev/null
+++ b/engines/freescape/games/3dck/8bitUI.cpp
@@ -0,0 +1,234 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/endian.h"
+#include "graphics/fonts/dosfont.h"
+#include "math/utils.h"
+
+#include "freescape/games/3dck/8bit.h"
+
+namespace Freescape {
+
+static const byte kCPCInks[27] = {
+ 20, 4, 21, 28, 24, 29, 12, 5, 13, 22, 6, 23, 30, 0,
+ 31, 14, 7, 15, 18, 2, 19, 26, 25, 27, 10, 3, 11
+};
+
+void Kit8Engine::loadPresentation() {
+ static const byte patterns[15][4] = {
+ {0x0f, 0x0f, 0x0f, 0x0f}, {0xf0, 0xf0, 0xf0, 0xf0}, {0xff, 0xff, 0xff, 0xff},
+ {0x0a, 0x05, 0x0a, 0x05}, {0xa0, 0x50, 0xa0, 0x50}, {0xaa, 0x55, 0xaa, 0x55},
+ {0xa5, 0x5a, 0xa5, 0x5a}, {0xaf, 0x5f, 0xaf, 0x5f}, {0xfa, 0xf5, 0xfa, 0xf5},
+ {0x02, 0x08, 0x02, 0x08}, {0x20, 0x80, 0x20, 0x80}, {0x22, 0x88, 0x22, 0x88},
+ {0x2d, 0x87, 0x2d, 0x87}, {0x2f, 0x8f, 0x2f, 0x8f}, {0xff, 0x66, 0x66, 0xff}
+ };
+ memcpy(_colorPatterns, patterns, sizeof(_colorPatterns));
+ _colorMap.resize(ARRAYSIZE(_colorPatterns));
+ for (uint i = 0; i < _colorMap.size(); i++)
+ _colorMap[i] = _colorPatterns[i];
+ _gfx->_keyColor = 0;
+ _scriptSurface.create(_screenW, _screenH, Graphics::PixelFormat::createFormatCLUT8());
+ _scriptSurface.fillRect(_fullscreenViewArea, 255);
+ _overlaySurface.create(_screenW, _screenH, _gfx->_texturePixelFormat);
+ _borderSurface.create(_screenW, _screenH, Graphics::PixelFormat::createFormatCLUT8());
+ _borderSurface.fillRect(_fullscreenViewArea, 0);
+
+ Common::File file;
+ if (file.open("BORDER.DAT")) {
+ if (file.size() != 16384 && file.size() != 16512)
+ error("Invalid 3D Construction Kit CPC border size");
+ file.seek(file.size() - 16384);
+ Common::Array<byte> screen;
+ screen.resize(16384);
+ file.read(screen.data(), screen.size());
+ for (int y = 0; y < _screenH; y++) {
+ for (int x = 0; x < _screenW; x++)
+ _borderSurface.setPixel(x, y, getCPCPixelMode1(screen[(y & 7) * 2048 + (y >> 3) * 80 + (x >> 2)], x & 3));
+ }
+ file.close();
+ }
+ _borderSurface.fillRect(_viewArea, 255);
+ // A saved editor data file omits the runner's font and border.
+ if (file.open("DISC.BIN") && file.size() == 25216) {
+ byte header[128];
+ file.read(header, sizeof(header));
+ if (READ_LE_UINT16(header + 21) == 0x3e00 && READ_LE_UINT16(header + 24) == 25088) {
+ file.seek(128 + 0x9c00 - 0x3e00);
+ _hasFont = file.read(_fontData, sizeof(_fontData)) == sizeof(_fontData);
+ }
+ }
+}
+
+void Kit8Engine::applyPalette() {
+ _gfx->_fourColorBackground = kCPCInks[_palette[0]];
+ _gfx->_underFireBackgroundColor = kCPCInks[_palette[2]];
+ _gfx->_paperColor = kCPCInks[_palette[1]];
+ _gfx->_inkColor = kCPCInks[_palette[3]];
+ _currentArea->_usualBackgroundColor = encodeCPCDirectColor(kCPCInks[_palette[0]]);
+ _currentArea->_skyColor = _currentArea->_usualBackgroundColor;
+ _currentArea->_underFireBackgroundColor = encodeCPCDirectColor(kCPCInks[_palette[3]]);
+}
+
+void Kit8Engine::printText(const Common::String &text, byte x, byte y, byte color) {
+ if (x >= 40 || y >= 25)
+ return;
+ Graphics::DosFont font;
+ byte background = ((color >> 3) & 1) | ((color >> 1) & 2);
+ for (uint i = 0; i < text.size() && x < 40; i++, x++) {
+ byte chr = text[i];
+ _scriptSurface.fillRect(Common::Rect(8 * x, 8 * y, 8 * x + 8, 8 * y + 8), background);
+ if (_hasFont && chr >= 32 && chr < 128) {
+ for (int row = 0; row < 8; row++) {
+ for (int col = 0; col < 8; col++) {
+ if (_fontData[chr - 32][row] & (0x80 >> col))
+ _scriptSurface.setPixel(8 * x + col, 8 * y + row, color & 3);
+ }
+ }
+ } else
+ font.drawChar(_scriptSurface.surfacePtr(), chr, 8 * x, 8 * y, color & 3);
+ }
+}
+
+void Kit8Engine::printMessage(byte id, byte x, byte y) {
+ if (_kitMessages.contains(id))
+ printText(_kitMessages[id], x, y, _textColor);
+}
+
+void Kit8Engine::updateInstruments() {
+ for (const auto &instrument : _instruments) {
+ byte type = instrument[0], x = instrument[1], y = instrument[2], length = instrument[3];
+ byte variable = instrument[4] & 127, color = instrument[5];
+ if (!type || type > 3 || x >= 40 || y >= 25 || !length)
+ continue;
+ uint16 value = _variables[variable];
+ if (type == 1) {
+ if (length > 5 || x + length > 40)
+ continue;
+ if (length > 3)
+ value |= _variables[(variable + 1) & 127] << 8;
+ printText(Common::String::format("%0*u", length, value), x, y, color);
+ } else {
+ if ((type == 2 && x + length > 40) || (type == 3 && y + length > 25))
+ continue;
+ Common::Rect bar(8 * x, 8 * y, 8 * (x + (type == 2 ? length : 1)), 8 * (y + (type == 3 ? length : 1)));
+ _scriptSurface.fillRect(bar, (color >> 2) & 3);
+ int filled = MIN<int>(value, 8 * length);
+ if (type == 2)
+ bar.right = bar.left + filled;
+ else
+ bar.top = bar.bottom - filled;
+ if (!bar.isEmpty())
+ _scriptSurface.fillRect(bar, color & 3);
+ }
+ }
+}
+
+void Kit8Engine::drawUI() {
+ uint32 colors[4];
+ for (uint i = 0; i < 4; i++) {
+ byte r, g, b;
+ _gfx->selectColorFromFourColorPalette(i, r, g, b);
+ colors[i] = _overlaySurface.format.ARGBToColor(255, r, g, b);
+ }
+ for (int y = 0; y < _screenH; y++) {
+ for (int x = 0; x < _screenW; x++) {
+ byte pen = _scriptSurface.getPixel(x, y);
+ if (pen == 255)
+ pen = _borderSurface.getPixel(x, y);
+ _overlaySurface.setPixel(x, y, pen == 255 ? 0 : colors[pen]);
+ }
+ }
+ drawFullscreenSurface(_overlaySurface.surfacePtr());
+ if (_crossVisible) {
+ _gfx->setViewport(_fullscreenViewArea);
+ _gfx->renderCrossair(_crossairPosition);
+ }
+}
+
+bool Kit8Engine::handleInput(const Common::Event &event) {
+ if (event.type == Common::EVENT_KEYDOWN || event.type == Common::EVENT_KEYUP) {
+ byte key = event.kbd.ascii < 128 ? event.kbd.ascii : 255;
+ if (key >= 'a' && key <= 'z')
+ key -= 'a' - 'A';
+ if (event.type == Common::EVENT_KEYDOWN)
+ _currentKey = key;
+ else if (_currentKey == key)
+ _currentKey = 255;
+ if (!_scriptFrameActive)
+ _variables[121] = _currentKey;
+ } else if (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START) {
+ switch (event.customType) {
+ case kActionShoot:
+ case kActionActivate:
+ if (!_scriptFrameActive)
+ interact(event.customType == kActionShoot);
+ return true;
+ case kActionSkip:
+ _variables[121] = ' ';
+ return true;
+ case kActionInfoMenu:
+ _variables[121] = 'I';
+ return true;
+ case kActionEscape:
+ case kActionChangeMode:
+ return false;
+ default:
+ break;
+ }
+ bool movement = event.customType == kActionMoveUp || event.customType == kActionMoveDown ||
+ event.customType == kActionMoveLeft || event.customType == kActionMoveRight;
+ return _scriptFrameActive && !movement;
+ } else if (event.type == Common::EVENT_LBUTTONDOWN || event.type == Common::EVENT_RBUTTONDOWN) {
+ if (_shootMode)
+ _crossairPosition = getNormalizedPosition(event.mouse);
+ if (!_scriptFrameActive)
+ interact(event.type == Common::EVENT_LBUTTONDOWN);
+ return true;
+ } else if (_scriptFrameActive && event.type == Common::EVENT_MOUSEMOVE)
+ return true;
+ return false;
+}
+
+void Kit8Engine::interact(bool shot) {
+ if (!_viewArea.contains(_crossairPosition) || (shot && !_variables[125]))
+ return;
+ float x = 2.0f * (_crossairPosition.x - _viewArea.left) / _viewArea.width() - 1;
+ float y = 1 - 2.0f * (_crossairPosition.y - _viewArea.top) / _viewArea.height();
+ float projection = tan(Math::deg2rad(_fieldOfView) / 2);
+ Math::Vector3d direction = directionToVector(_pitch + Math::rad2deg(atan(y * projection / _viewAspectRatio)),
+ _yaw - Math::rad2deg(atan(x * projection)), false);
+ Object *object = _currentArea->checkCollisionRay(Math::Ray(_position, direction), 8192, true);
+ if (shot) {
+ if (_variables[125] != 255)
+ _variables[125]--;
+ _shootingFrames = 3;
+ }
+ if (!object || !object->isGeometric())
+ return;
+ if (!shot) {
+ Math::Vector3d diff = object->getOrigin() + object->getSize() / 2 - _position;
+ if (ABS(diff.x()) + ABS(diff.y()) + ABS(diff.z()) > 32.0f * _activationRange / _currentArea->getScale())
+ return;
+ }
+ executeObjectConditions(static_cast<GeometricObject *>(object), shot, false, !shot);
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/gfx.cpp b/engines/freescape/gfx.cpp
index bcfa6e3c884..df4021d3b84 100644
--- a/engines/freescape/gfx.cpp
+++ b/engines/freescape/gfx.cpp
@@ -42,6 +42,7 @@ Renderer::Renderer(int screenW, int screenH, Common::RenderMode renderMode, bool
_screenW = screenW;
_screenH = screenH;
_keyColor = -1;
+ _fourColorBackground = -1;
_inkColor = -1;
_paperColor = -1;
_underFireBackgroundColor = -1;
@@ -517,9 +518,10 @@ bool Renderer::getRGBAtHercules(uint8 index, uint8 &r1, uint8 &g1, uint8 &b1, ui
void Renderer::selectColorFromFourColorPalette(uint8 index, uint8 &r1, uint8 &g1, uint8 &b1) {
if (index == 0) {
- r1 = 0;
- g1 = 0;
- b1 = 0;
+ if (_fourColorBackground >= 0)
+ readFromPalette(_fourColorBackground, r1, g1, b1);
+ else
+ r1 = g1 = b1 = 0;
} else if (index == 1) {
readFromPalette(_underFireBackgroundColor, r1, g1, b1);
} else if (index == 2) {
diff --git a/engines/freescape/gfx.h b/engines/freescape/gfx.h
index cdf0bded4ad..efbea62f0a9 100644
--- a/engines/freescape/gfx.h
+++ b/engines/freescape/gfx.h
@@ -304,6 +304,7 @@ public:
int _inkColor;
int _paperColor;
int _underFireBackgroundColor;
+ int _fourColorBackground;
Common::Point _shakeOffset;
byte _stipples[16][128];
diff --git a/engines/freescape/language/8bitKitDetokeniser.cpp b/engines/freescape/language/8bitKitDetokeniser.cpp
new file mode 100644
index 00000000000..05e38de0d34
--- /dev/null
+++ b/engines/freescape/language/8bitKitDetokeniser.cpp
@@ -0,0 +1,130 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/textconsole.h"
+#include "freescape/language/8bitKitDetokeniser.h"
+
+namespace Freescape {
+
+struct KitOpcode {
+ byte opcode;
+ Token::Type token;
+ const char *name;
+ byte minArgs, maxArgs;
+ byte event;
+};
+
+static const KitOpcode kKitOpcodes[] = {
+ {0x00, Token::SETVAR, "SETV", 2, 2, 0},
+ {0x01, Token::ADDVAR, "ADDV", 2, 2, 0},
+ {0x02, Token::ADCV, "ADCV", 2, 2, 0},
+ {0x03, Token::ANDV, "ANDV", 2, 2, 0},
+ {0x04, Token::SUBVAR, "SUBV", 2, 2, 0},
+ {0x05, Token::SBCV, "SBCV", 2, 2, 0},
+ {0x06, Token::ORV, "ORV", 2, 2, 0},
+ {0x07, Token::XORV, "XORV", 2, 2, 0},
+ {0x08, Token::TESTV, "TESTV", 2, 2, 0},
+ {0x09, Token::CMPV, "CMPV", 2, 2, 0},
+ {0x0a, Token::VIS, "VIS", 1, 2, 0},
+ {0x0b, Token::INVIS, "INVIS", 1, 2, 0},
+ {0x0c, Token::TOGVIS, "TOGVIS", 1, 2, 0},
+ {0x0d, Token::DESTROY, "DESTROY", 1, 2, 0},
+ {0x0e, Token::GOTO, "GOTO", 1, 2, 0},
+ {0x0f, Token::MODE, "MODE", 1, 1, 0},
+ {0x20, Token::CONDITIONAL, "IFSHOT", 1, 2, kConditionalShot},
+ {0x21, Token::CONDITIONAL, "IFHIT", 1, 2, kConditionalCollided},
+ {0x22, Token::CONDITIONAL, "IFACTIVE", 1, 2, kConditionalActivated},
+ {0x23, Token::CONDITIONAL, "IFSENSED", 1, 2, kConditionalSensed},
+ {0x24, Token::VISQ, "IFVIS", 1, 2, 0},
+ {0x25, Token::CONDITIONAL, "IFFALL", 0, 0, kConditionalFallen},
+ {0x26, Token::CONDITIONAL, "IFCRUSH", 0, 0, kConditionalCrushed},
+ {0x27, Token::CONDITIONAL, "IFTIMER", 0, 0, kConditionalTimeout},
+ {0x28, Token::IFEQ, "IFEQ", 0, 0, 0},
+ {0x29, Token::IFGT, "IFGT", 0, 0, 0},
+ {0x2a, Token::IFLT, "IFLT", 0, 0, 0},
+ {0x2b, Token::AND, "AND", 0, 0, 0},
+ {0x2c, Token::OR, "OR", 0, 0, 0},
+ {0x2d, Token::ELSE, "ELSE", 0, 0, 0},
+ {0x2e, Token::THEN, "THEN", 0, 0, 0},
+ {0x2f, Token::ENDIF, "ENDIF", 0, 0, 0},
+ {0x30, Token::TIMER, "TIMER", 1, 1, 0},
+ {0x31, Token::CROSS, "CROSS", 1, 1, 0},
+ {0x32, Token::ENDGAME, "ENDGAME", 0, 0, 0},
+ {0x33, Token::COLOUR, "COLOUR", 2, 2, 0},
+ {0x34, Token::SOUND, "SOUND", 1, 1, 0},
+ {0x35, Token::SYNCSND, "SYNCSND", 1, 1, 0},
+ {0x36, Token::DELAY, "DELAY", 1, 1, 0},
+ {0x37, Token::TEXTCOL, "TEXTCOL", 1, 1, 0},
+ {0x38, Token::PRINT, "PRINT", 3, 3, 0},
+ {0x39, Token::REDRAW, "REDRAW", 0, 0, 0},
+ {0x3a, Token::EXECUTE, "CALL", 1, 1, 0},
+ {0x3f, Token::END, "END", 0, 0, 0}
+};
+
+Common::String detokenise8bitKitCondition(const Common::Array<byte> &code, FCLInstructionVector &instructions) {
+ Common::String source;
+ for (uint pos = 0; pos < code.size();) {
+ byte raw = code[pos++];
+ if (raw == 0xff) {
+ instructions.push_back(FCLInstruction(Token::ENDOFFILE));
+ return source;
+ }
+ byte opcode = raw & 0x3f;
+ bool variableSource = opcode >= 0x10 && opcode <= 0x19;
+ if (variableSource)
+ opcode &= ~0x10;
+ const KitOpcode *entry = nullptr;
+ for (const auto &candidate : kKitOpcodes) {
+ if (candidate.opcode == opcode) {
+ entry = &candidate;
+ break;
+ }
+ }
+ if (!entry)
+ error("Unsupported 8-bit 3D Construction Kit opcode %02x", opcode);
+ uint count = raw >> 6;
+ if (count < entry->minArgs || count > entry->maxArgs || count > code.size() - pos)
+ error("Invalid 8-bit 3D Construction Kit %s operands", entry->name);
+ FCLInstruction instruction(entry->token);
+ if (count > 0)
+ instruction.setSource(code[pos], variableSource ? Token::VARIABLE : Token::CONSTANT);
+ if (count > 1)
+ instruction.setDestination(code[pos + 1], opcode <= 9 ? Token::VARIABLE : Token::CONSTANT);
+ if (count > 2)
+ instruction.setAdditional(code[pos + 2]);
+ if (entry->event) {
+ if (count > 1)
+ instruction.setAdditional(code[pos + 1]);
+ if (count > 0)
+ instruction.setDestination(code[pos]);
+ instruction.setSource(entry->event);
+ }
+ instructions.push_back(instruction);
+ source += entry->name;
+ for (uint arg = 0; arg < count; arg++)
+ source += Common::String::format(" %u", code[pos + arg]);
+ source += '\n';
+ pos += count;
+ }
+ error("Unterminated 8-bit 3D Construction Kit condition");
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/language/8bitKitDetokeniser.h b/engines/freescape/language/8bitKitDetokeniser.h
new file mode 100644
index 00000000000..dab26bb8abc
--- /dev/null
+++ b/engines/freescape/language/8bitKitDetokeniser.h
@@ -0,0 +1,33 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef FREESCAPE_8BIT_KIT_DETOKENISER_H
+#define FREESCAPE_8BIT_KIT_DETOKENISER_H
+
+#include "freescape/language/instruction.h"
+
+namespace Freescape {
+
+Common::String detokenise8bitKitCondition(const Common::Array<byte> &code, FCLInstructionVector &instructions);
+
+} // namespace Freescape
+
+#endif
diff --git a/engines/freescape/language/instruction.h b/engines/freescape/language/instruction.h
index c3feb0e0dec..0be0997da41 100644
--- a/engines/freescape/language/instruction.h
+++ b/engines/freescape/language/instruction.h
@@ -36,6 +36,9 @@ enum {
kConditionalTimeout = 1 << 1,
kConditionalCollided = 1 << 2,
kConditionalActivated = 1 << 3,
+ kConditionalSensed = 1 << 4,
+ kConditionalFallen = 1 << 5,
+ kConditionalCrushed = 1 << 6,
};
class FCLInstruction;
diff --git a/engines/freescape/language/instruction8bitKit.cpp b/engines/freescape/language/instruction8bitKit.cpp
new file mode 100644
index 00000000000..65fca4ee461
--- /dev/null
+++ b/engines/freescape/language/instruction8bitKit.cpp
@@ -0,0 +1,423 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "math/utils.h"
+
+#include "freescape/games/3dck/8bit.h"
+
+namespace Freescape {
+
+void Kit8Engine::readSystemVariables() {
+ float scale = 2 * _currentArea->getScale();
+ for (int axis = 0; axis < 3; axis++) {
+ uint16 position = int32(round(_position.getValue(axis) * scale));
+ _variables[112 + 2 * axis] = position;
+ _variables[113 + 2 * axis] = position >> 8;
+ }
+ _variables[118] = (int(round(_pitch / 5)) % 72 + 72) % 72;
+ _variables[119] = (int(round((90 - _yaw) / 5)) % 72 + 72) % 72;
+ _variables[120] = (int(round(_roll / 5)) % 72 + 72) % 72;
+ _variables[121] = _currentKey;
+ _variables[124] = _currentArea->getAreaID();
+ _changedVariables = 0;
+}
+
+void Kit8Engine::writeSystemVariables() {
+ float scale = 2 * _currentArea->getScale();
+ for (int axis = 0; axis < 3; axis++) {
+ if (_changedVariables & (3 << (2 * axis)))
+ _position.setValue(axis, (_variables[112 + 2 * axis] | (_variables[113 + 2 * axis] << 8)) / scale);
+ }
+ if (_changedVariables & (1 << 6)) {
+ _pitch = 5 * (_variables[118] % 72);
+ if (_pitch > 180)
+ _pitch -= 360;
+ }
+ if (_changedVariables & (1 << 7))
+ _yaw = 90 - 5 * _variables[119];
+ if (_changedVariables & (1 << 8))
+ _roll = 5 * _variables[120];
+ if (_changedVariables & 0x1c0)
+ updateCamera();
+ if (_changedVariables & 0x3f)
+ _lastPosition = _position;
+ _changedVariables = 0;
+}
+
+void Kit8Engine::updateTimeVariables() {
+ uint32 now = g_system->getMillis();
+ uint32 elapsed = (now - _lastTime) / 20;
+ _lastTime += 20 * elapsed;
+ uint16 counter = (_variables[122] | (_variables[123] << 8)) + elapsed;
+ _variables[122] = counter;
+ _variables[123] = counter >> 8;
+ _timerTicks += elapsed;
+ uint32 interval = MAX<uint32>(1, _timerInterval);
+ if (_timerTicks >= interval) {
+ _pendingTimer = true;
+ _timerTicks %= interval;
+ }
+}
+
+void Kit8Engine::startScript(const FCLInstructionVector &code) {
+ if (_scriptStack.size() >= 64)
+ error("8-bit 3D Construction Kit procedure stack overflow");
+ _scriptStack.push_back(ScriptFrame(code));
+ _executing = true;
+ _booleanOp = Token::UNKNOWN;
+}
+
+void Kit8Engine::updateScripts() {
+ _fallen |= _hasFallen;
+ _crushed |= _playerWasCrushed;
+ _hasFallen = _playerWasCrushed = false;
+ _avoidRenderingFrames = 0;
+ if (_delayUntil && int32(_delayUntil - g_system->getMillis()) > 0)
+ return;
+ _delayUntil = 0;
+ if (!_scriptFrameActive) {
+ _scriptFrameActive = true;
+ readSystemVariables();
+ _timerTriggered = _pendingTimer;
+ _pendingTimer = false;
+ _scriptSurface.fillRect(_viewArea, 255);
+ _conditions = &_globalConditions;
+ _conditionIndex = 0;
+ _globalPhase = true;
+ if (_initialScriptPending) {
+ _initialScriptPending = false;
+ for (const auto &condition : _globalConditions) {
+ if (condition.id == 1) {
+ startScript(condition.code);
+ break;
+ }
+ }
+ }
+ }
+ uint budget = 4096;
+ while (budget) {
+ if (_scriptStack.empty()) {
+ if (_conditionIndex == _conditions->size()) {
+ if (!_globalPhase)
+ break;
+ _globalPhase = false;
+ _conditions = &_areaData[_currentArea->getAreaID()].conditions;
+ _conditionIndex = 0;
+ continue;
+ }
+ const ConditionData &condition = (*_conditions)[_conditionIndex++];
+ if (_globalPhase && condition.id == 1)
+ continue;
+ startScript(condition.code);
+ }
+ if (!executeCode(budget))
+ return;
+ }
+ writeSystemVariables();
+ if (!_scriptStack.empty() || _globalPhase || _conditionIndex < _conditions->size())
+ return;
+ updateInstruments();
+ _scriptFrameActive = false;
+ _shotObject = _hitObject = _activatedObject = 0;
+}
+
+bool Kit8Engine::executeCode(uint &budget) {
+ while (!_scriptStack.empty() && budget) {
+ ScriptFrame &frame = _scriptStack.back();
+ if (frame.ip == frame.code->size()) {
+ _scriptStack.pop_back();
+ continue;
+ }
+ const FCLInstruction &instruction = (*frame.code)[frame.ip++];
+ Token::Type op = instruction.getType();
+ budget--;
+ // Condition statements also execute while a THEN/ELSE branch is skipped.
+ switch (op) {
+ case Token::ENDOFFILE:
+ _scriptStack.pop_back();
+ continue;
+ case Token::CONDITIONAL:
+ executeConditional(instruction);
+ continue;
+ case Token::VISQ:
+ case Token::IFEQ:
+ case Token::IFGT:
+ case Token::IFLT:
+ executeComparison(instruction);
+ continue;
+ case Token::AND:
+ case Token::OR:
+ _previousZero = _zero;
+ _booleanOp = op;
+ continue;
+ case Token::THEN:
+ _executing = _zero;
+ continue;
+ case Token::ELSE:
+ _executing = !_executing;
+ continue;
+ case Token::ENDIF:
+ _executing = true;
+ continue;
+ default:
+ break;
+ }
+ if (!_executing)
+ continue;
+ switch (op) {
+ case Token::SETVAR:
+ case Token::ADDVAR:
+ case Token::ADCV:
+ case Token::SUBVAR:
+ case Token::SBCV:
+ case Token::ANDV:
+ case Token::ORV:
+ case Token::XORV:
+ case Token::TESTV:
+ case Token::CMPV:
+ executeArithmetic(instruction);
+ break;
+ case Token::VIS:
+ case Token::INVIS:
+ case Token::TOGVIS:
+ case Token::DESTROY:
+ executeObjectStatus(instruction);
+ break;
+ case Token::GOTO:
+ executeGoto(instruction);
+ break;
+ case Token::MODE:
+ executeMode(instruction);
+ break;
+ case Token::EXECUTE:
+ executeCall(instruction);
+ break;
+ case Token::END:
+ _scriptStack.pop_back();
+ break;
+ case Token::ENDGAME:
+ _gameStateControl = kFreescapeGameStateRestart;
+ _scriptStack.clear();
+ return false;
+ case Token::TIMER:
+ _timerInterval = instruction._source;
+ break;
+ case Token::CROSS:
+ _crossVisible = instruction._source != 0;
+ break;
+ case Token::COLOUR:
+ executeColour(instruction);
+ break;
+ case Token::SOUND:
+ case Token::SYNCSND:
+ executeSound(instruction);
+ break;
+ case Token::DELAY:
+ _delayUntil = g_system->getMillis() + 20 * (instruction._source ? instruction._source : 256);
+ return false;
+ case Token::TEXTCOL:
+ _textColor = instruction._source;
+ break;
+ case Token::PRINT:
+ printMessage(instruction._source, instruction._destination, instruction._additional);
+ break;
+ case Token::REDRAW:
+ writeSystemVariables();
+ _scriptSurface.fillRect(_viewArea, 255);
+ updateInstruments();
+ return false;
+ default:
+ error("Unsupported 8-bit 3D Construction Kit instruction %u", op);
+ }
+ }
+ return _scriptStack.empty();
+}
+
+void Kit8Engine::setPredicate(bool value) {
+ if (_booleanOp == Token::AND)
+ value = _previousZero && value;
+ else if (_booleanOp == Token::OR)
+ value = _previousZero || value;
+ _zero = value;
+ _booleanOp = Token::UNKNOWN;
+}
+
+void Kit8Engine::executeArithmetic(const FCLInstruction &instruction) {
+ byte index = instruction._destination & 127;
+ byte source = instruction._sourceType == Token::VARIABLE ? _variables[instruction._source & 127] : instruction._source;
+ int destination = _variables[index];
+ int result = destination;
+ bool store = true;
+ switch (instruction.getType()) {
+ case Token::SETVAR: result = source; break;
+ case Token::ADDVAR: result += source; break;
+ case Token::ADCV: result += source + (_carry ? 1 : 0); break;
+ case Token::SUBVAR: result -= source; break;
+ case Token::SBCV: result -= source + (_carry ? 1 : 0); break;
+ case Token::ANDV: result &= source; break;
+ case Token::ORV: result |= source; break;
+ case Token::XORV: result ^= source; break;
+ case Token::TESTV:
+ result &= source;
+ store = false;
+ break;
+ case Token::CMPV:
+ result = source - destination;
+ store = false;
+ break;
+ default:
+ break;
+ }
+ if (store) {
+ _variables[index] = result;
+ if (index >= 112 && index <= 120)
+ _changedVariables |= 1 << (index - 112);
+ }
+ _carry = result < 0 || result > 255;
+ if (instruction.getType() == Token::SETVAR) {
+ // SETV retains the CPC operand decoder's zero flag.
+ setPredicate(instruction._sourceType != Token::VARIABLE);
+ } else
+ setPredicate(byte(result) == 0);
+}
+
+Object *Kit8Engine::scriptObject(uint16 area, uint16 id) {
+ if (!_areaMap.contains(area))
+ return nullptr;
+ Object *object = _areaMap[area]->objectWithID(id);
+ if (!object)
+ object = _areaMap[area]->entranceWithID(id);
+ return object;
+}
+
+void Kit8Engine::executeComparison(const FCLInstruction &instruction) {
+ if (instruction.getType() == Token::VISQ) {
+ uint16 area = instruction._destinationType == Token::UNKNOWN ? _currentArea->getAreaID() : instruction._destination;
+ Object *object = scriptObject(area, instruction._source);
+ _carry = false;
+ setPredicate(!object || (!object->isInvisible() && !object->isDestroyed()));
+ } else if (instruction.getType() == Token::IFGT)
+ setPredicate(!_zero && _carry);
+ else if (instruction.getType() == Token::IFLT)
+ setPredicate(!_zero && !_carry);
+ else
+ setPredicate(_zero);
+}
+
+void Kit8Engine::executeConditional(const FCLInstruction &instruction) {
+ int area = instruction._additionalType == Token::UNKNOWN ? _currentArea->getAreaID() : instruction._additional;
+ int id = instruction._destination;
+ int value = 0;
+ switch (instruction._source) {
+ case kConditionalShot: value = _shotObject; break;
+ case kConditionalCollided: value = _hitObject; break;
+ case kConditionalActivated: value = _activatedObject; break;
+ case kConditionalTimeout:
+ _carry = false;
+ setPredicate(_timerTriggered);
+ return;
+ case kConditionalFallen:
+ setPredicate(_fallen);
+ _fallen = false;
+ return;
+ case kConditionalCrushed:
+ setPredicate(_crushed);
+ _crushed = false;
+ return;
+ case kConditionalSensed: {
+ Object *object = scriptObject(area, id);
+ _carry = false;
+ setPredicate(!object || (object->getType() == kSensorType && static_cast<Sensor *>(object)->isShooting()));
+ return;
+ }
+ default:
+ break;
+ }
+ if (area != _currentArea->getAreaID()) {
+ value = _currentArea->getAreaID();
+ id = area;
+ }
+ _carry = value < id;
+ setPredicate(value == id);
+}
+
+void Kit8Engine::executeObjectStatus(const FCLInstruction &instruction) {
+ uint16 area = instruction._destinationType == Token::UNKNOWN ? _currentArea->getAreaID() : instruction._destination;
+ Object *object = scriptObject(area, instruction._source);
+ if (!object || object->isDestroyed())
+ return;
+ switch (instruction.getType()) {
+ case Token::VIS: object->makeVisible(); break;
+ case Token::INVIS: object->makeInvisible(); break;
+ case Token::TOGVIS: object->toggleVisibility(); break;
+ case Token::DESTROY:
+ object->makeInvisible();
+ object->destroy();
+ break;
+ default:
+ break;
+ }
+}
+
+void Kit8Engine::executeGoto(const FCLInstruction &instruction) {
+ uint16 area = instruction._destinationType == Token::UNKNOWN ? _currentArea->getAreaID() : instruction._destination;
+ writeSystemVariables();
+ gotoArea(area, instruction._source);
+}
+
+void Kit8Engine::executeMode(const FCLInstruction &instruction) {
+ writeSystemVariables();
+ setMovementMode(instruction._source);
+ readSystemVariables();
+}
+
+void Kit8Engine::executeCall(const FCLInstruction &instruction) {
+ for (const auto &procedure : _procedures) {
+ if (procedure.id == instruction._source) {
+ startScript(procedure.code);
+ return;
+ }
+ }
+}
+
+void Kit8Engine::executeColour(const FCLInstruction &instruction) {
+ _palette[instruction._source & 3] = MIN<int>(26, instruction._destination);
+ applyPalette();
+}
+
+void Kit8Engine::executeSound(const FCLInstruction &instruction) {
+ if (instruction._source && !_soundWarning) {
+ warning("8-bit 3D Construction Kit sound effects are not implemented");
+ _soundWarning = true;
+ }
+}
+
+bool Kit8Engine::executeObjectConditions(GeometricObject *object, bool shot, bool collided, bool activated) {
+ if (shot)
+ _shotObject = object->getObjectID();
+ if (collided)
+ _hitObject = object->getObjectID();
+ if (activated)
+ _activatedObject = object->getObjectID();
+ return false;
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/language/token.h b/engines/freescape/language/token.h
index f3992c743cc..82712ec7a77 100644
--- a/engines/freescape/language/token.h
+++ b/engines/freescape/language/token.h
@@ -99,7 +99,19 @@ public:
TOGGLEBIT,
SWAPJET,
BITNOTEQ,
- VARNOTEQ
+ VARNOTEQ,
+ ADCV,
+ SBCV,
+ XORV,
+ TESTV,
+ CMPV,
+ IFEQ,
+ IFGT,
+ IFLT,
+ CROSS,
+ COLOUR,
+ TEXTCOL,
+ TIMER
};
Type getType();
diff --git a/engines/freescape/metaengine.cpp b/engines/freescape/metaengine.cpp
index 7ca7b9ed389..9359f73db00 100644
--- a/engines/freescape/metaengine.cpp
+++ b/engines/freescape/metaengine.cpp
@@ -32,6 +32,7 @@
#include "freescape/games/driller/driller.h"
#include "freescape/games/eclipse/eclipse.h"
#include "freescape/games/3dck/3dck.h"
+#include "freescape/games/3dck/8bit.h"
#include "freescape/detection.h"
@@ -210,7 +211,10 @@ Common::Error FreescapeMetaEngine::createInstance(OSystem *syst, Engine **engine
} else if (Common::String(gd->gameId) == "castlemaster" || Common::String(gd->gameId) == "castlemaster2") {
*engine = (Engine *)new Freescape::CastleEngine(syst, gd);
} else if (Common::String(gd->gameId) == "3dkit") {
- *engine = new Freescape::KitEngine(syst, gd);
+ if (gd->platform == Common::kPlatformAmstradCPC)
+ *engine = new Freescape::Kit8Engine(syst, gd);
+ else
+ *engine = new Freescape::KitEngine(syst, gd);
} else
*engine = new Freescape::FreescapeEngine(syst, gd);
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index 16915b5fa6c..528f17feda9 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -53,6 +53,8 @@ MODULE_OBJS := \
games/eclipse/cpc.o \
games/eclipse/zx.o \
games/3dck/3dck.o \
+ games/3dck/8bit.o \
+ games/3dck/8bitUI.o \
games/3dck/ui.o \
games/palettes.o \
gfx.o \
@@ -60,8 +62,10 @@ MODULE_OBJS := \
loaders/8bitBinaryLoader.o \
loaders/c64.o \
language/8bitDetokeniser.o \
+ language/8bitKitDetokeniser.o \
language/16bitDetokeniser.o \
language/instruction.o \
+ language/instruction8bitKit.o \
language/instruction16bit.o \
metaengine.o \
movement.o \
Commit: ce36579e2f48f3b7aeb9e306792f53e639f8a601
https://github.com/scummvm/scummvm/commit/ce36579e2f48f3b7aeb9e306792f53e639f8a601
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-07T07:23:53+02:00
Commit Message:
FREESCAPE: refactored language scripting for freescape/3dck
Changed paths:
A engines/freescape/language/detokeniser.cpp
A engines/freescape/language/detokeniser.h
A engines/freescape/language/detokeniser_3dck16.cpp
A engines/freescape/language/detokeniser_3dck8.cpp
A engines/freescape/language/detokeniser_freescape.cpp
A engines/freescape/language/execution.h
A engines/freescape/language/execution_3dck16.cpp
A engines/freescape/language/execution_3dck16.h
A engines/freescape/language/execution_3dck8.cpp
A engines/freescape/language/execution_3dck8.h
A engines/freescape/language/execution_freescape.cpp
A engines/freescape/language/variables.h
R engines/freescape/language/16bitDetokeniser.cpp
R engines/freescape/language/16bitDetokeniser.h
R engines/freescape/language/8bitDetokeniser.cpp
R engines/freescape/language/8bitDetokeniser.h
R engines/freescape/language/8bitKitDetokeniser.cpp
R engines/freescape/language/8bitKitDetokeniser.h
R engines/freescape/language/instruction16bit.cpp
R engines/freescape/language/instruction16bit.h
R engines/freescape/language/instruction8bitKit.cpp
engines/freescape/freescape.cpp
engines/freescape/freescape.h
engines/freescape/games/3dck/3dck.cpp
engines/freescape/games/3dck/3dck.h
engines/freescape/games/3dck/8bit.cpp
engines/freescape/games/3dck/8bit.h
engines/freescape/games/3dck/8bitUI.cpp
engines/freescape/games/castle/amiga.cpp
engines/freescape/games/castle/atari.cpp
engines/freescape/games/castle/c64.cpp
engines/freescape/games/castle/castle.cpp
engines/freescape/games/castle/cpc.cpp
engines/freescape/games/castle/dos.cpp
engines/freescape/games/castle/zx.cpp
engines/freescape/games/dark/amiga.cpp
engines/freescape/games/dark/atari.cpp
engines/freescape/games/dark/c64.cpp
engines/freescape/games/dark/cpc.cpp
engines/freescape/games/dark/dark.cpp
engines/freescape/games/dark/dos.cpp
engines/freescape/games/dark/zx.cpp
engines/freescape/games/driller/amiga.cpp
engines/freescape/games/driller/atari.cpp
engines/freescape/games/driller/c64.cpp
engines/freescape/games/driller/cpc.cpp
engines/freescape/games/driller/dos.cpp
engines/freescape/games/driller/driller.cpp
engines/freescape/games/driller/zx.cpp
engines/freescape/games/eclipse/amiga.cpp
engines/freescape/games/eclipse/atari.cpp
engines/freescape/games/eclipse/c64.cpp
engines/freescape/games/eclipse/cpc.cpp
engines/freescape/games/eclipse/dos.cpp
engines/freescape/games/eclipse/eclipse.cpp
engines/freescape/games/eclipse/zx.cpp
engines/freescape/language/instruction.cpp
engines/freescape/language/instruction.h
engines/freescape/loaders/8bitBinaryLoader.cpp
engines/freescape/module.mk
engines/freescape/objects/entrance.h
engines/freescape/objects/geometricobject.cpp
engines/freescape/objects/group.cpp
diff --git a/engines/freescape/freescape.cpp b/engines/freescape/freescape.cpp
index 09d618480ef..bfbed6299a5 100644
--- a/engines/freescape/freescape.cpp
+++ b/engines/freescape/freescape.cpp
@@ -29,7 +29,7 @@
#include "math/utils.h"
#include "freescape/freescape.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
#include "freescape/objects/sensor.h"
#include "freescape/sweepAABB.h"
#include "freescape/doodle.h"
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index c73ef2591ab..d5e4775752a 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -39,7 +39,7 @@
#include "freescape/area.h"
#include "freescape/font.h"
#include "freescape/gfx.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
#include "freescape/objects/entrance.h"
#include "freescape/objects/geometricobject.h"
#include "freescape/objects/sensor.h"
@@ -488,12 +488,11 @@ public:
bool checkConditional(const FCLInstruction &instruction, bool shot, bool collided, bool timer, bool activated);
bool checkIfGreaterOrEqual(FCLInstruction &instruction);
bool checkIfLessOrEqual(FCLInstruction &instruction);
- void executeExecute(FCLInstruction &instruction);
+ void executeCall(FCLInstruction &instruction);
void executeIncrementVariable(FCLInstruction &instruction);
void executeDecrementVariable(FCLInstruction &instruction);
void executeSetVariable(FCLInstruction &instruction);
void executeGoto(FCLInstruction &instruction);
- void executeIfThenElse(FCLInstruction &instruction);
virtual void executeMakeInvisible(FCLInstruction &instruction);
void executeMakeVisible(FCLInstruction &instruction);
void executeToggleVisibility(FCLInstruction &instruction);
diff --git a/engines/freescape/games/3dck/3dck.cpp b/engines/freescape/games/3dck/3dck.cpp
index 6c80aafa23c..24c178415e2 100644
--- a/engines/freescape/games/3dck/3dck.cpp
+++ b/engines/freescape/games/3dck/3dck.cpp
@@ -24,7 +24,7 @@
#include "math/utils.h"
#include "freescape/games/3dck/3dck.h"
-#include "freescape/language/16bitDetokeniser.h"
+#include "freescape/language/detokeniser.h"
namespace Freescape {
@@ -235,7 +235,7 @@ Common::Array<KitEngine::ConditionData> KitEngine::loadConditions(Common::Seekab
uint16 words = file.readUint16BE() & 0x7fff;
ConditionData condition;
condition.name = name;
- Common::String source = detokenise16bitCondition(readCode(file, 2 * words), condition.condition);
+ Common::String source = detokeniseKit16Condition(readCode(file, 2 * words), condition.condition);
debugC(1, kFreescapeDebugParser, "3DCK condition %s:\n%s", name, source.c_str());
conditions.push_back(condition);
}
@@ -362,7 +362,7 @@ Object *KitEngine::loadObject(Common::SeekableReadStream &file, ObjectData &data
// Entrances can retain editor data after their header.
data.extra = readWords(payload, (payload.size() - payload.pos()) / 2);
} else {
- Common::String source = detokenise16bitCondition(readCode(payload, payload.size() - payload.pos()), data.condition);
+ Common::String source = detokeniseKit16Condition(readCode(payload, payload.size() - payload.pos()), data.condition);
debugC(1, kFreescapeDebugParser, "3DCK object %u condition:\n%s", data.id, source.c_str());
}
file.seek(end);
diff --git a/engines/freescape/games/3dck/3dck.h b/engines/freescape/games/3dck/3dck.h
index 421bd1dd373..e09933f47be 100644
--- a/engines/freescape/games/3dck/3dck.h
+++ b/engines/freescape/games/3dck/3dck.h
@@ -23,7 +23,7 @@
#define FREESCAPE_GAMES_3DCK_H
#include "freescape/freescape.h"
-#include "freescape/language/instruction16bit.h"
+#include "freescape/language/execution_3dck16.h"
namespace Freescape {
@@ -49,7 +49,7 @@ public:
private:
struct ObjectData;
- struct ScriptState : FCLExecutionState {
+ struct ScriptState : FCLKit16ExecutionState {
ObjectData *object = nullptr;
uint16 area = 0;
byte events = 0;
@@ -116,7 +116,7 @@ private:
void executeObjectStatus(const FCLInstruction &instruction);
bool checkObjectStatus(const FCLInstruction &instruction);
void executeGetPosition(const FCLInstruction &instruction);
- bool executeExecute(const FCLInstruction &instruction, ScriptState &script);
+ bool executeCall(const FCLInstruction &instruction, ScriptState &script);
bool executeGoto(const FCLInstruction &instruction);
void executeMode(const FCLInstruction &instruction);
bool executeDelay(const FCLInstruction &instruction);
@@ -135,7 +135,6 @@ private:
void setScriptVariable(byte index, uint32 value);
void setVariableResult(const FCLInstruction &instruction, ScriptState &script, uint32 value);
int32 getVariableOrConstant(int32 operand, Token::Type type) const;
- void setScriptPredicate(ScriptState &script, bool value);
void getObjectReference(const FCLInstruction &instruction, uint16 &area, uint16 &id) const;
ObjectData *scriptObject(uint16 area, uint16 id);
ObjectData *scriptAnimator(const FCLInstruction &instruction);
diff --git a/engines/freescape/games/3dck/8bit.cpp b/engines/freescape/games/3dck/8bit.cpp
index aac773e5d14..a23aa372257 100644
--- a/engines/freescape/games/3dck/8bit.cpp
+++ b/engines/freescape/games/3dck/8bit.cpp
@@ -24,7 +24,7 @@
#include "math/utils.h"
#include "freescape/games/3dck/8bit.h"
-#include "freescape/language/8bitKitDetokeniser.h"
+#include "freescape/language/detokeniser.h"
namespace Freescape {
@@ -153,7 +153,7 @@ Common::Array<Kit8Engine::ConditionData> Kit8Engine::loadConditions(Common::Seek
Common::Array<byte> code;
code.resize(length);
file.read(code.data(), length);
- Common::String source = detokenise8bitKitCondition(code, condition.code);
+ Common::String source = detokeniseKit8Condition(code, condition.condition);
debugC(1, kFreescapeDebugParser, "Condition %u:\n%s", condition.id, source.c_str());
conditions.push_back(condition);
}
@@ -270,23 +270,7 @@ GeometricObject *Kit8Engine::loadGeometricObject(Common::SeekableReadStream &fil
void Kit8Engine::initGameState() {
FreescapeEngine::initGameState();
- memset(_variables, 0, sizeof(_variables));
- _changedVariables = 0;
- _currentKey = 255;
- _textColor = 7;
- _variables[121] = _variables[125] = 255;
- _variables[127] = 0x9c;
- _scriptStack.clear();
- _conditions = nullptr;
- _initialScriptPending = true;
- _scriptFrameActive = false;
- _zero = _carry = _previousZero = false;
- _shotObject = _hitObject = _activatedObject = 0;
- _fallen = _crushed = _pendingTimer = _timerTriggered = false;
- _crossVisible = true;
- _timerTicks = _timerInterval = _delayUntil = 0;
- _lastTime = g_system->getMillis();
- _scriptSurface.fillRect(_fullscreenViewArea, 255);
+ resetScripts();
_currentArea = nullptr;
_movementMode = 1;
_playerHeight = 0;
diff --git a/engines/freescape/games/3dck/8bit.h b/engines/freescape/games/3dck/8bit.h
index 541b460b2ab..0bb35b8c734 100644
--- a/engines/freescape/games/3dck/8bit.h
+++ b/engines/freescape/games/3dck/8bit.h
@@ -23,6 +23,7 @@
#define FREESCAPE_GAMES_3DCK_8BIT_H
#include "freescape/freescape.h"
+#include "freescape/language/execution_3dck8.h"
namespace Freescape {
@@ -48,9 +49,11 @@ public:
bool canSaveGameStateCurrently(Common::U32String *msg = nullptr) override { return false; }
private:
+ typedef FCLKit8ExecutionState ScriptState;
+
struct ConditionData {
byte id;
- FCLInstructionVector code;
+ FCLInstructionVector condition;
};
struct AreaData {
@@ -59,12 +62,6 @@ private:
Common::Array<ConditionData> conditions;
};
- struct ScriptFrame {
- const FCLInstructionVector *code;
- uint ip;
- ScriptFrame(const FCLInstructionVector &instructions) : code(&instructions), ip(0) {}
- };
-
Common::Array<ConditionData> loadConditions(Common::SeekableReadStream &file);
Area *loadArea(Common::SeekableReadStream &file);
GeometricObject *loadGeometricObject(Common::SeekableReadStream &file, const byte header[9]);
@@ -73,18 +70,20 @@ private:
void setMovementMode(byte mode);
void readSystemVariables();
void writeSystemVariables();
- void startScript(const FCLInstructionVector &code);
- bool executeCode(uint &budget);
- void executeArithmetic(const FCLInstruction &instruction);
- void executeComparison(const FCLInstruction &instruction);
- void executeConditional(const FCLInstruction &instruction);
+ void resetScripts();
+ void beginScriptFrame();
+ void startScript(ScriptState &script, const FCLInstructionVector &code);
+ FCLExecutionResult executeCode(ScriptState &script, uint &budget);
+ void executeArithmetic(const FCLInstruction &instruction, ScriptState &script);
+ void executeComparison(const FCLInstruction &instruction, ScriptState &script);
+ void executeConditional(const FCLInstruction &instruction, ScriptState &script);
void executeObjectStatus(const FCLInstruction &instruction);
void executeGoto(const FCLInstruction &instruction);
void executeMode(const FCLInstruction &instruction);
- void executeCall(const FCLInstruction &instruction);
+ void executeCall(const FCLInstruction &instruction, ScriptState &script);
void executeSound(const FCLInstruction &instruction);
void executeColour(const FCLInstruction &instruction);
- void setPredicate(bool value);
+ void getObjectReference(const FCLInstruction &instruction, uint16 &area, uint16 &id) const;
Object *scriptObject(uint16 area, uint16 id);
void interact(bool shot);
void printMessage(byte id, byte x, byte y);
@@ -94,13 +93,11 @@ private:
Common::HashMap<uint16, AreaData> _areaData;
Common::Array<ConditionData> _globalConditions, _procedures;
Common::HashMap<byte, Common::String> _kitMessages;
- Common::Array<ScriptFrame> _scriptStack;
- const Common::Array<ConditionData> *_conditions = nullptr;
+ ScriptState _script;
+ const Common::Array<ConditionData> *_activeConditions = nullptr;
uint _conditionIndex = 0;
bool _initialScriptPending = true, _scriptFrameActive = false, _globalPhase = false;
- bool _executing = true, _zero = false, _carry = false, _previousZero = false;
- Token::Type _booleanOp = Token::UNKNOWN;
- byte _variables[128] = {};
+ byte _kitVariables[128] = {};
uint16 _changedVariables = 0;
byte _currentKey = 255;
byte _palette[4] = {};
diff --git a/engines/freescape/games/3dck/8bitUI.cpp b/engines/freescape/games/3dck/8bitUI.cpp
index fb0f707e75f..06a87a06ca5 100644
--- a/engines/freescape/games/3dck/8bitUI.cpp
+++ b/engines/freescape/games/3dck/8bitUI.cpp
@@ -118,12 +118,12 @@ void Kit8Engine::updateInstruments() {
byte variable = instrument[4] & 127, color = instrument[5];
if (!type || type > 3 || x >= 40 || y >= 25 || !length)
continue;
- uint16 value = _variables[variable];
+ uint16 value = _kitVariables[variable];
if (type == 1) {
if (length > 5 || x + length > 40)
continue;
if (length > 3)
- value |= _variables[(variable + 1) & 127] << 8;
+ value |= _kitVariables[(variable + 1) & 127] << 8;
printText(Common::String::format("%0*u", length, value), x, y, color);
} else {
if ((type == 2 && x + length > 40) || (type == 3 && y + length > 25))
@@ -173,7 +173,7 @@ bool Kit8Engine::handleInput(const Common::Event &event) {
else if (_currentKey == key)
_currentKey = 255;
if (!_scriptFrameActive)
- _variables[121] = _currentKey;
+ _kitVariables[121] = _currentKey;
} else if (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START) {
switch (event.customType) {
case kActionShoot:
@@ -182,10 +182,10 @@ bool Kit8Engine::handleInput(const Common::Event &event) {
interact(event.customType == kActionShoot);
return true;
case kActionSkip:
- _variables[121] = ' ';
+ _kitVariables[121] = ' ';
return true;
case kActionInfoMenu:
- _variables[121] = 'I';
+ _kitVariables[121] = 'I';
return true;
case kActionEscape:
case kActionChangeMode:
@@ -208,7 +208,7 @@ bool Kit8Engine::handleInput(const Common::Event &event) {
}
void Kit8Engine::interact(bool shot) {
- if (!_viewArea.contains(_crossairPosition) || (shot && !_variables[125]))
+ if (!_viewArea.contains(_crossairPosition) || (shot && !_kitVariables[125]))
return;
float x = 2.0f * (_crossairPosition.x - _viewArea.left) / _viewArea.width() - 1;
float y = 1 - 2.0f * (_crossairPosition.y - _viewArea.top) / _viewArea.height();
@@ -217,8 +217,8 @@ void Kit8Engine::interact(bool shot) {
_yaw - Math::rad2deg(atan(x * projection)), false);
Object *object = _currentArea->checkCollisionRay(Math::Ray(_position, direction), 8192, true);
if (shot) {
- if (_variables[125] != 255)
- _variables[125]--;
+ if (_kitVariables[125] != 255)
+ _kitVariables[125]--;
_shootingFrames = 3;
}
if (!object || !object->isGeometric())
diff --git a/engines/freescape/games/castle/amiga.cpp b/engines/freescape/games/castle/amiga.cpp
index 81edcd87f09..553738357e3 100644
--- a/engines/freescape/games/castle/amiga.cpp
+++ b/engines/freescape/games/castle/amiga.cpp
@@ -27,7 +27,7 @@
#include "freescape/freescape.h"
#include "freescape/games/castle/castle.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/games/castle/atari.cpp b/engines/freescape/games/castle/atari.cpp
index 142c7c80fe6..c4183cf2be2 100644
--- a/engines/freescape/games/castle/atari.cpp
+++ b/engines/freescape/games/castle/atari.cpp
@@ -25,7 +25,7 @@
#include "freescape/copylock.h"
#include "freescape/freescape.h"
#include "freescape/games/castle/castle.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/games/castle/c64.cpp b/engines/freescape/games/castle/c64.cpp
index c024a0e105f..6cf1461de86 100644
--- a/engines/freescape/games/castle/c64.cpp
+++ b/engines/freescape/games/castle/c64.cpp
@@ -27,7 +27,7 @@
#include "freescape/freescape.h"
#include "freescape/games/castle/c64.music.h"
#include "freescape/games/castle/castle.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 5a1c88b47fc..a5bcca3a1bb 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -36,7 +36,7 @@
#include "freescape/gfx.h"
#include "freescape/games/castle/castle.h"
#include "freescape/games/castle/c64.music.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
#include "freescape/music.h"
namespace Freescape {
@@ -1600,7 +1600,7 @@ void CastleEngine::drawFullscreenGameOverAndWait() {
}
}
-// Same as FreescapeEngine::executeExecute but updates the spirits destroyed counter
+// Same as FreescapeEngine::executeCall but updates the spirits destroyed counter
void CastleEngine::executeDestroy(FCLInstruction &instruction) {
uint16 objectID = 0;
uint16 areaID = _currentArea->getAreaID();
diff --git a/engines/freescape/games/castle/cpc.cpp b/engines/freescape/games/castle/cpc.cpp
index 922d4e60982..e8ed9c1a5fc 100644
--- a/engines/freescape/games/castle/cpc.cpp
+++ b/engines/freescape/games/castle/cpc.cpp
@@ -26,7 +26,7 @@
#include "freescape/freescape.h"
#include "freescape/games/castle/ay.music.h"
#include "freescape/games/castle/castle.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/games/castle/dos.cpp b/engines/freescape/games/castle/dos.cpp
index 871a1e11be5..df1635e2144 100644
--- a/engines/freescape/games/castle/dos.cpp
+++ b/engines/freescape/games/castle/dos.cpp
@@ -26,7 +26,7 @@
#include "freescape/freescape.h"
#include "freescape/games/castle/castle.h"
#include "freescape/games/castle/opl.music.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/games/castle/zx.cpp b/engines/freescape/games/castle/zx.cpp
index d3d1abedeb1..a1330cd40d8 100644
--- a/engines/freescape/games/castle/zx.cpp
+++ b/engines/freescape/games/castle/zx.cpp
@@ -23,7 +23,7 @@
#include "freescape/freescape.h"
#include "freescape/games/castle/castle.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/games/dark/amiga.cpp b/engines/freescape/games/dark/amiga.cpp
index 7833ea42ebd..50e70684017 100644
--- a/engines/freescape/games/dark/amiga.cpp
+++ b/engines/freescape/games/dark/amiga.cpp
@@ -26,7 +26,7 @@
#include "freescape/copylock.h"
#include "freescape/freescape.h"
#include "freescape/games/dark/dark.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/games/dark/atari.cpp b/engines/freescape/games/dark/atari.cpp
index 3ed35c03197..fc3af8daadd 100644
--- a/engines/freescape/games/dark/atari.cpp
+++ b/engines/freescape/games/dark/atari.cpp
@@ -24,7 +24,7 @@
#include "freescape/freescape.h"
#include "freescape/games/dark/dark.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
#include "freescape/wb.h"
namespace Freescape {
diff --git a/engines/freescape/games/dark/c64.cpp b/engines/freescape/games/dark/c64.cpp
index 67470fb8aea..b872c054e94 100644
--- a/engines/freescape/games/dark/c64.cpp
+++ b/engines/freescape/games/dark/c64.cpp
@@ -24,7 +24,7 @@
#include "freescape/freescape.h"
#include "freescape/games/dark/dark.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/games/dark/cpc.cpp b/engines/freescape/games/dark/cpc.cpp
index 1e6cf14c7cb..95de8547a03 100644
--- a/engines/freescape/games/dark/cpc.cpp
+++ b/engines/freescape/games/dark/cpc.cpp
@@ -24,7 +24,7 @@
#include "freescape/freescape.h"
#include "freescape/games/dark/dark.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/games/dark/dark.cpp b/engines/freescape/games/dark/dark.cpp
index 3132718f8c7..1a1348feaf5 100644
--- a/engines/freescape/games/dark/dark.cpp
+++ b/engines/freescape/games/dark/dark.cpp
@@ -26,7 +26,7 @@
#include "freescape/freescape.h"
#include "freescape/games/dark/dark.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
#include "freescape/objects/global.h"
#include "freescape/wb.h"
#include "freescape/objects/connections.h"
diff --git a/engines/freescape/games/dark/dos.cpp b/engines/freescape/games/dark/dos.cpp
index ff06bf4cea9..ca0a3588861 100644
--- a/engines/freescape/games/dark/dos.cpp
+++ b/engines/freescape/games/dark/dos.cpp
@@ -25,7 +25,7 @@
#include "freescape/freescape.h"
#include "freescape/games/dark/dark.h"
#include "freescape/games/dark/opl.music.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/games/dark/zx.cpp b/engines/freescape/games/dark/zx.cpp
index 63e843dee59..512513bc3c0 100644
--- a/engines/freescape/games/dark/zx.cpp
+++ b/engines/freescape/games/dark/zx.cpp
@@ -23,7 +23,7 @@
#include "freescape/freescape.h"
#include "freescape/games/dark/dark.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/games/driller/amiga.cpp b/engines/freescape/games/driller/amiga.cpp
index 0eb595d7e7c..88863517853 100644
--- a/engines/freescape/games/driller/amiga.cpp
+++ b/engines/freescape/games/driller/amiga.cpp
@@ -23,7 +23,7 @@
#include "freescape/freescape.h"
#include "freescape/games/driller/driller.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/games/driller/atari.cpp b/engines/freescape/games/driller/atari.cpp
index a64a251bd06..771f05102df 100644
--- a/engines/freescape/games/driller/atari.cpp
+++ b/engines/freescape/games/driller/atari.cpp
@@ -24,7 +24,7 @@
#include "freescape/freescape.h"
#include "freescape/games/driller/driller.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/games/driller/c64.cpp b/engines/freescape/games/driller/c64.cpp
index d36fb058e68..7d82ada1112 100644
--- a/engines/freescape/games/driller/c64.cpp
+++ b/engines/freescape/games/driller/c64.cpp
@@ -23,7 +23,7 @@
#include "freescape/freescape.h"
#include "freescape/games/driller/driller.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/games/driller/cpc.cpp b/engines/freescape/games/driller/cpc.cpp
index ba1f861c8c3..a0a38e08d7f 100644
--- a/engines/freescape/games/driller/cpc.cpp
+++ b/engines/freescape/games/driller/cpc.cpp
@@ -24,7 +24,7 @@
#include "freescape/freescape.h"
#include "freescape/games/driller/driller.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/games/driller/dos.cpp b/engines/freescape/games/driller/dos.cpp
index c487684e2de..4fcf7a81256 100644
--- a/engines/freescape/games/driller/dos.cpp
+++ b/engines/freescape/games/driller/dos.cpp
@@ -25,7 +25,7 @@
#include "freescape/freescape.h"
#include "freescape/games/driller/driller.h"
#include "freescape/games/driller/opl.music.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/games/driller/driller.cpp b/engines/freescape/games/driller/driller.cpp
index 5b061e73995..9a64ae2a8f5 100644
--- a/engines/freescape/games/driller/driller.cpp
+++ b/engines/freescape/games/driller/driller.cpp
@@ -31,7 +31,8 @@
#include "freescape/freescape.h"
#include "freescape/games/driller/driller.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/detokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
@@ -327,7 +328,7 @@ void DrillerEngine::loadAssets() {
conditionArray.push_back(0x7f);
conditionArray.push_back(0x0);
- Common::String conditionSource = detokenise8bitCondition(conditionArray, instructions, false);
+ Common::String conditionSource = detokeniseFreescapeCondition(conditionArray, instructions, false);
debugC(1, kFreescapeDebugParser, "%s", conditionSource.c_str());
_areaMap[18]->_conditions.push_back(instructions);
_areaMap[18]->_conditionSources.push_back(conditionSource);
diff --git a/engines/freescape/games/driller/zx.cpp b/engines/freescape/games/driller/zx.cpp
index 3d61cbcef66..d76197d1fa0 100644
--- a/engines/freescape/games/driller/zx.cpp
+++ b/engines/freescape/games/driller/zx.cpp
@@ -22,7 +22,7 @@
#include "freescape/freescape.h"
#include "freescape/games/driller/driller.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/games/eclipse/amiga.cpp b/engines/freescape/games/eclipse/amiga.cpp
index 0602c5b670a..8f98cb2ae50 100644
--- a/engines/freescape/games/eclipse/amiga.cpp
+++ b/engines/freescape/games/eclipse/amiga.cpp
@@ -25,7 +25,7 @@
#include "freescape/freescape.h"
#include "freescape/games/eclipse/eclipse.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/games/eclipse/atari.cpp b/engines/freescape/games/eclipse/atari.cpp
index ec9f23cf361..0490151db1f 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -26,7 +26,7 @@
#include "freescape/freescape.h"
#include "freescape/games/eclipse/eclipse.h"
#include "freescape/wb.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/games/eclipse/c64.cpp b/engines/freescape/games/eclipse/c64.cpp
index a6ff892308f..30ef6458d62 100644
--- a/engines/freescape/games/eclipse/c64.cpp
+++ b/engines/freescape/games/eclipse/c64.cpp
@@ -25,7 +25,7 @@
#include "freescape/games/eclipse/c64.music.h"
#include "freescape/games/eclipse/c64.sfx.h"
#include "freescape/games/eclipse/eclipse.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/games/eclipse/cpc.cpp b/engines/freescape/games/eclipse/cpc.cpp
index bf50e68e373..386787a0a9c 100644
--- a/engines/freescape/games/eclipse/cpc.cpp
+++ b/engines/freescape/games/eclipse/cpc.cpp
@@ -26,7 +26,7 @@
#include "freescape/freescape.h"
#include "freescape/games/eclipse/ay.music.h"
#include "freescape/games/eclipse/eclipse.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/games/eclipse/dos.cpp b/engines/freescape/games/eclipse/dos.cpp
index f342960b15f..1a35418e301 100644
--- a/engines/freescape/games/eclipse/dos.cpp
+++ b/engines/freescape/games/eclipse/dos.cpp
@@ -26,7 +26,7 @@
#include "freescape/freescape.h"
#include "freescape/games/eclipse/eclipse.h"
#include "freescape/games/eclipse/opl.music.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/games/eclipse/eclipse.cpp b/engines/freescape/games/eclipse/eclipse.cpp
index efc5d2b81d2..0eeac037612 100644
--- a/engines/freescape/games/eclipse/eclipse.cpp
+++ b/engines/freescape/games/eclipse/eclipse.cpp
@@ -37,7 +37,7 @@
#include "freescape/games/eclipse/opl.music.h"
#include "freescape/games/eclipse/eclipse.h"
#include "freescape/objects/entrance.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/games/eclipse/zx.cpp b/engines/freescape/games/eclipse/zx.cpp
index 512639cfa2e..2a06e8d08b0 100644
--- a/engines/freescape/games/eclipse/zx.cpp
+++ b/engines/freescape/games/eclipse/zx.cpp
@@ -25,7 +25,7 @@
#include "freescape/freescape.h"
#include "freescape/games/eclipse/ay.music.h"
#include "freescape/games/eclipse/eclipse.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
diff --git a/engines/freescape/language/16bitDetokeniser.cpp b/engines/freescape/language/16bitDetokeniser.cpp
deleted file mode 100644
index 4e8585e2f12..00000000000
--- a/engines/freescape/language/16bitDetokeniser.cpp
+++ /dev/null
@@ -1,181 +0,0 @@
-/* ScummVM - Graphic Adventure Engine
- *
- * ScummVM is the legal property of its developers, whose names
- * are too numerous to list here. Please refer to the COPYRIGHT
- * file distributed with this source distribution.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-#include "common/endian.h"
-#include "common/textconsole.h"
-
-#include "freescape/language/16bitDetokeniser.h"
-
-namespace Freescape {
-
-static const struct {
- byte opcode;
- Token::Type type;
- byte minArgs, maxArgs;
- const char *name;
-} opcodeTable[] = {
- {0x00, Token::NOP, 0, 0, "NOP"},
- {0x01, Token::CONDITIONAL, 0, 0, "ACTIVATED?"},
- {0x02, Token::CONDITIONAL, 0, 0, "COLLIDED?"},
- {0x03, Token::CONDITIONAL, 0, 0, "SHOT?"},
- {0x04, Token::CONDITIONAL, 0, 0, "TIMER?"},
- {0x10, Token::SETVAR, 2, 2, "SETVAR"},
- {0x11, Token::ADDVAR, 2, 2, "ADDVAR"},
- {0x12, Token::SUBVAR, 2, 2, "SUBVAR"},
- {0x13, Token::ANDV, 2, 2, "ANDV"},
- {0x14, Token::ORV, 2, 2, "ORV"},
- {0x15, Token::NOTV, 1, 1, "NOTV"},
- {0x16, Token::VAREQ, 2, 2, "VAR=?"},
- {0x17, Token::VARGT, 2, 2, "VAR>?"},
- {0x18, Token::VARLT, 2, 2, "VAR<?"},
- {0x2f, Token::DESTROYEDQ, 1, 2, "DESTROYED?"},
- {0x30, Token::INVIS, 1, 2, "INVIS"},
- {0x31, Token::VIS, 1, 2, "VIS"},
- {0x32, Token::TOGVIS, 1, 2, "TOGVIS"},
- {0x33, Token::DESTROY, 1, 2, "DESTROY"},
- {0x34, Token::INVISQ, 1, 2, "INVIS?"},
- {0x35, Token::VISQ, 1, 2, "VIS?"},
- {0x36, Token::MOVE, 3, 3, "MOVE"},
- {0x37, Token::GETXPOS, 2, 3, "GETXPOS"},
- {0x38, Token::GETYPOS, 2, 3, "GETYPOS"},
- {0x39, Token::GETZPOS, 2, 3, "GETZPOS"},
- {0x3a, Token::MOVETO, 3, 3, "MOVETO"},
- {0x40, Token::IF, 0, 0, "IF"},
- {0x41, Token::THEN, 0, 0, "THEN"},
- {0x42, Token::ELSE, 0, 0, "ELSE"},
- {0x43, Token::ENDIF, 0, 0, "ENDIF"},
- {0x44, Token::AND, 0, 0, "AND"},
- {0x45, Token::OR, 0, 0, "OR"},
- {0x50, Token::STARTANIM, 1, 2, "STARTANIM"},
- {0x51, Token::STOPANIM, 1, 2, "STOPANIM"},
- {0x52, Token::START, 0, 0, "START"},
- {0x53, Token::RESTART, 0, 0, "RESTART"},
- {0x54, Token::INCLUDE, 1, 1, "INCLUDE"},
- {0x55, Token::WAITTRIG, 0, 0, "WAITTRIG"},
- {0x56, Token::TRIGANIM, 1, 2, "TRIGANIM"},
- {0x57, Token::REMOVE, 1, 1, "REMOVE"},
- {0x60, Token::LOOP, 1, 1, "LOOP"},
- {0x61, Token::AGAIN, 2, 2, "AGAIN"},
- {0x70, Token::SOUND, 1, 1, "SOUND"},
- {0x71, Token::SYNCSND, 1, 1, "SYNCSND"},
- {0x80, Token::WAIT, 0, 0, "WAIT"},
- {0x81, Token::DELAY, 1, 1, "DELAY"},
- {0x82, Token::UPDATEI, 1, 1, "UPDATEI"},
- {0x83, Token::PRINT, 2, 255, "PRINT"},
- {0x84, Token::REDRAW, 0, 0, "REDRAW"},
- {0x85, Token::MODE, 1, 1, "MODE"},
- {0x86, Token::ENDGAME, 0, 0, "ENDGAME"},
- {0x87, Token::EXECUTE, 1, 1, "EXECUTE"},
- {0x90, Token::GOTO, 1, 2, "GOTO"},
- {0xff, Token::END, 0, 0, "END"}
-};
-
-Common::String detokenise16bitCondition(const Common::Array<byte> &tokenisedCondition, FCLInstructionVector &instructions) {
- Common::String detokenisedStream;
- int loops = 0;
- for (uint32 bytePointer = 0; bytePointer < tokenisedCondition.size();) {
- if (tokenisedCondition.size() - bytePointer < 2)
- error("Truncated 16-bit FCL instruction at %u", bytePointer);
- byte count = tokenisedCondition[bytePointer], opcode = tokenisedCondition[bytePointer + 1];
- if (tokenisedCondition.size() - bytePointer < 2U + 2U * count)
- error("Truncated 16-bit FCL instruction at %u", bytePointer);
- uint index = 0;
- while (index < ARRAYSIZE(opcodeTable) && opcodeTable[index].opcode != opcode)
- index++;
- if (index == ARRAYSIZE(opcodeTable))
- error("Unknown 16-bit FCL opcode %02x at %u", opcode, bytePointer);
- const auto &entry = opcodeTable[index];
- if (count < entry.minArgs || count > entry.maxArgs)
- error("Invalid argument count for 16-bit FCL opcode %02x at %u", opcode, bytePointer);
-
- FCLInstruction instruction(entry.type);
- int32 operands[3] = {};
- Token::Type types[3] = {Token::UNKNOWN, Token::UNKNOWN, Token::UNKNOWN};
- uint32 argumentPointer = bytePointer + 2;
- uint argumentCount = count;
- detokenisedStream += entry.name;
- if (entry.type == Token::PRINT) {
- uint16 length = READ_BE_UINT16(&tokenisedCondition[argumentPointer]);
- if (count != 2 + (length + 1) / 2)
- error("Invalid 16-bit FCL PRINT instruction at %u", bytePointer);
- instruction._text = Common::String(reinterpret_cast<const char *>(&tokenisedCondition[argumentPointer + 2]), length);
- detokenisedStream += Common::String::format(" (\"%s\", ", instruction._text.c_str());
- argumentPointer += 2 + ((length + 1) & ~1);
- argumentCount = 1;
- } else if (entry.type == Token::AGAIN) {
- argumentCount = 0; // The two words hold the runner's loop state.
- } else if (count) {
- detokenisedStream += " (";
- }
- for (uint i = 0; i < argumentCount; i++) {
- uint16 operand = READ_BE_UINT16(&tokenisedCondition[argumentPointer + 2 * i]);
- types[i] = operand & 0x8000 ? Token::VARIABLE : Token::CONSTANT;
- operands[i] = types[i] == Token::VARIABLE ? operand & 0xff : (operand & 0x4000 ? int32(operand) - 0x8000 : operand);
- if (i)
- detokenisedStream += ", ";
- detokenisedStream += Common::String::format(types[i] == Token::VARIABLE ? "v%d" : "%d", operands[i]);
- }
- if (argumentCount)
- detokenisedStream += ")";
- detokenisedStream += "\n";
-
- // Match the operand order used by the 8-bit detokeniser.
- switch (entry.type) {
- case Token::SETVAR: case Token::ADDVAR: case Token::SUBVAR: case Token::ANDV: case Token::ORV:
- case Token::GOTO:
- SWAP(operands[0], operands[1]);
- SWAP(types[0], types[1]);
- break;
- case Token::INVIS: case Token::VIS: case Token::TOGVIS: case Token::DESTROY:
- case Token::INVISQ: case Token::VISQ: case Token::DESTROYEDQ:
- case Token::STARTANIM: case Token::STOPANIM: case Token::TRIGANIM:
- if (count == 2) {
- SWAP(operands[0], operands[1]);
- SWAP(types[0], types[1]);
- }
- break;
- case Token::CONDITIONAL:
- operands[0] = opcode == 1 ? kConditionalActivated : opcode == 2 ? kConditionalCollided :
- opcode == 3 ? kConditionalShot : kConditionalTimeout;
- types[0] = Token::CONSTANT;
- break;
- case Token::LOOP:
- loops++;
- break;
- case Token::AGAIN:
- if (--loops < 0)
- error("16-bit FCL AGAIN without LOOP");
- break;
- default:
- break;
- }
- instruction.setSource(operands[0], types[0]);
- instruction.setDestination(operands[1], types[1]);
- instruction.setAdditional(operands[2], types[2]);
- instructions.push_back(instruction);
- bytePointer += 2 + 2 * count;
- }
- if (loops)
- error("Unterminated 16-bit FCL LOOP");
- return detokenisedStream;
-}
-
-} // namespace Freescape
diff --git a/engines/freescape/language/detokeniser.cpp b/engines/freescape/language/detokeniser.cpp
new file mode 100644
index 00000000000..ee7f5d8112b
--- /dev/null
+++ b/engines/freescape/language/detokeniser.cpp
@@ -0,0 +1,62 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "freescape/language/detokeniser.h"
+
+namespace Freescape {
+
+void normaliseKitOperands(FCLInstruction &instruction) {
+ switch (instruction.getType()) {
+ case Token::SETVAR:
+ case Token::ADDVAR:
+ case Token::ADCV:
+ case Token::SUBVAR:
+ case Token::SBCV:
+ case Token::ANDV:
+ case Token::ORV:
+ case Token::XORV:
+ case Token::TESTV:
+ case Token::CMPV:
+ case Token::GOTO:
+ break;
+ case Token::INVIS:
+ case Token::VIS:
+ case Token::TOGVIS:
+ case Token::DESTROY:
+ case Token::INVISQ:
+ case Token::VISQ:
+ case Token::DESTROYEDQ:
+ case Token::STARTANIM:
+ case Token::STOPANIM:
+ case Token::TRIGANIM:
+ if (instruction._destinationType != Token::UNKNOWN)
+ break;
+ return;
+ default:
+ return;
+ }
+ int32 source = instruction._source;
+ Token::Type sourceType = instruction._sourceType;
+ instruction.setSource(instruction._destination, instruction._destinationType);
+ instruction.setDestination(source, sourceType);
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/language/16bitDetokeniser.h b/engines/freescape/language/detokeniser.h
similarity index 54%
rename from engines/freescape/language/16bitDetokeniser.h
rename to engines/freescape/language/detokeniser.h
index 08b63b214ca..b31e54cd732 100644
--- a/engines/freescape/language/16bitDetokeniser.h
+++ b/engines/freescape/language/detokeniser.h
@@ -19,14 +19,36 @@
*
*/
-#ifndef FREESCAPE_16BITDETOKENISER_H
-#define FREESCAPE_16BITDETOKENISER_H
+#ifndef FREESCAPE_LANGUAGE_DETOKENISER_H
+#define FREESCAPE_LANGUAGE_DETOKENISER_H
#include "freescape/language/instruction.h"
namespace Freescape {
-Common::String detokenise16bitCondition(const Common::Array<byte> &tokenisedCondition, FCLInstructionVector &instructions);
+// Classic games retain their byte-oriented dialect on DOS, Amiga and Atari ST.
+Common::String detokeniseFreescapeCondition(const Common::Array<uint16> &tokenisedCondition, FCLInstructionVector &instructions, bool isAmigaAtari);
+Common::String detokeniseKit8Condition(const Common::Array<byte> &tokenisedCondition, FCLInstructionVector &instructions);
+Common::String detokeniseKit16Condition(const Common::Array<byte> &tokenisedCondition, FCLInstructionVector &instructions);
+
+void normaliseKitOperands(FCLInstruction &instruction);
+
+struct FCLOpcode {
+ byte opcode;
+ Token::Type type;
+ const char *name;
+ byte minArgs, maxArgs;
+ byte event;
+};
+
+template<uint N>
+const FCLOpcode *findFCLOpcode(const FCLOpcode (&opcodes)[N], byte opcode) {
+ for (const auto &entry : opcodes) {
+ if (entry.opcode == opcode)
+ return &entry;
+ }
+ return nullptr;
+}
} // namespace Freescape
diff --git a/engines/freescape/language/detokeniser_3dck16.cpp b/engines/freescape/language/detokeniser_3dck16.cpp
new file mode 100644
index 00000000000..964f90b8fdf
--- /dev/null
+++ b/engines/freescape/language/detokeniser_3dck16.cpp
@@ -0,0 +1,159 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/endian.h"
+#include "common/textconsole.h"
+
+#include "freescape/language/detokeniser.h"
+
+namespace Freescape {
+
+static const FCLOpcode kKitOpcodes[] = {
+ {0x00, Token::NOP, "NOP", 0, 0, 0},
+ {0x01, Token::CONDITIONAL, "ACTIVATED?", 0, 0, kConditionalActivated},
+ {0x02, Token::CONDITIONAL, "COLLIDED?", 0, 0, kConditionalCollided},
+ {0x03, Token::CONDITIONAL, "SHOT?", 0, 0, kConditionalShot},
+ {0x04, Token::CONDITIONAL, "TIMER?", 0, 0, kConditionalTimeout},
+ {0x10, Token::SETVAR, "SETVAR", 2, 2, 0},
+ {0x11, Token::ADDVAR, "ADDVAR", 2, 2, 0},
+ {0x12, Token::SUBVAR, "SUBVAR", 2, 2, 0},
+ {0x13, Token::ANDV, "ANDV", 2, 2, 0},
+ {0x14, Token::ORV, "ORV", 2, 2, 0},
+ {0x15, Token::NOTV, "NOTV", 1, 1, 0},
+ {0x16, Token::VAREQ, "VAR=?", 2, 2, 0},
+ {0x17, Token::VARGT, "VAR>?", 2, 2, 0},
+ {0x18, Token::VARLT, "VAR<?", 2, 2, 0},
+ {0x2f, Token::DESTROYEDQ, "DESTROYED?", 1, 2, 0},
+ {0x30, Token::INVIS, "INVIS", 1, 2, 0},
+ {0x31, Token::VIS, "VIS", 1, 2, 0},
+ {0x32, Token::TOGVIS, "TOGVIS", 1, 2, 0},
+ {0x33, Token::DESTROY, "DESTROY", 1, 2, 0},
+ {0x34, Token::INVISQ, "INVIS?", 1, 2, 0},
+ {0x35, Token::VISQ, "VIS?", 1, 2, 0},
+ {0x36, Token::MOVE, "MOVE", 3, 3, 0},
+ {0x37, Token::GETXPOS, "GETXPOS", 2, 3, 0},
+ {0x38, Token::GETYPOS, "GETYPOS", 2, 3, 0},
+ {0x39, Token::GETZPOS, "GETZPOS", 2, 3, 0},
+ {0x3a, Token::MOVETO, "MOVETO", 3, 3, 0},
+ {0x40, Token::IF, "IF", 0, 0, 0},
+ {0x41, Token::THEN, "THEN", 0, 0, 0},
+ {0x42, Token::ELSE, "ELSE", 0, 0, 0},
+ {0x43, Token::ENDIF, "ENDIF", 0, 0, 0},
+ {0x44, Token::AND, "AND", 0, 0, 0},
+ {0x45, Token::OR, "OR", 0, 0, 0},
+ {0x50, Token::STARTANIM, "STARTANIM", 1, 2, 0},
+ {0x51, Token::STOPANIM, "STOPANIM", 1, 2, 0},
+ {0x52, Token::START, "START", 0, 0, 0},
+ {0x53, Token::RESTART, "RESTART", 0, 0, 0},
+ {0x54, Token::INCLUDE, "INCLUDE", 1, 1, 0},
+ {0x55, Token::WAITTRIG, "WAITTRIG", 0, 0, 0},
+ {0x56, Token::TRIGANIM, "TRIGANIM", 1, 2, 0},
+ {0x57, Token::REMOVE, "REMOVE", 1, 1, 0},
+ {0x60, Token::LOOP, "LOOP", 1, 1, 0},
+ {0x61, Token::AGAIN, "AGAIN", 2, 2, 0},
+ {0x70, Token::SOUND, "SOUND", 1, 1, 0},
+ {0x71, Token::SYNCSND, "SYNCSND", 1, 1, 0},
+ {0x80, Token::WAIT, "WAIT", 0, 0, 0},
+ {0x81, Token::DELAY, "DELAY", 1, 1, 0},
+ {0x82, Token::UPDATEI, "UPDATEI", 1, 1, 0},
+ {0x83, Token::PRINT, "PRINT", 2, 255, 0},
+ {0x84, Token::REDRAW, "REDRAW", 0, 0, 0},
+ {0x85, Token::MODE, "MODE", 1, 1, 0},
+ {0x86, Token::ENDGAME, "ENDGAME", 0, 0, 0},
+ {0x87, Token::EXECUTE, "EXECUTE", 1, 1, 0},
+ {0x90, Token::GOTO, "GOTO", 1, 2, 0},
+ {0xff, Token::END, "END", 0, 0, 0}
+};
+
+Common::String detokeniseKit16Condition(const Common::Array<byte> &tokenisedCondition, FCLInstructionVector &instructions) {
+ Common::String detokenisedStream;
+ int loops = 0;
+ for (uint32 bytePointer = 0; bytePointer < tokenisedCondition.size();) {
+ if (tokenisedCondition.size() - bytePointer < 2)
+ error("Truncated 16-bit FCL instruction at %u", bytePointer);
+ byte count = tokenisedCondition[bytePointer], opcode = tokenisedCondition[bytePointer + 1];
+ if (tokenisedCondition.size() - bytePointer < 2U + 2U * count)
+ error("Truncated 16-bit FCL instruction at %u", bytePointer);
+ const FCLOpcode *entry = findFCLOpcode(kKitOpcodes, opcode);
+ if (!entry)
+ error("Unknown 16-bit FCL opcode %02x at %u", opcode, bytePointer);
+ if (count < entry->minArgs || count > entry->maxArgs)
+ error("Invalid argument count for 16-bit FCL opcode %02x at %u", opcode, bytePointer);
+
+ FCLInstruction instruction(entry->type);
+ int32 operands[3] = {};
+ Token::Type types[3] = {Token::UNKNOWN, Token::UNKNOWN, Token::UNKNOWN};
+ uint32 argumentPointer = bytePointer + 2;
+ uint argumentCount = count;
+ detokenisedStream += entry->name;
+ if (entry->type == Token::PRINT) {
+ uint16 length = READ_BE_UINT16(&tokenisedCondition[argumentPointer]);
+ if (count != 2 + (length + 1) / 2)
+ error("Invalid 16-bit FCL PRINT instruction at %u", bytePointer);
+ instruction._text = Common::String(reinterpret_cast<const char *>(&tokenisedCondition[argumentPointer + 2]), length);
+ detokenisedStream += Common::String::format(" (\"%s\", ", instruction._text.c_str());
+ argumentPointer += 2 + ((length + 1) & ~1);
+ argumentCount = 1;
+ } else if (entry->type == Token::AGAIN) {
+ argumentCount = 0; // The two words hold the runner's loop state.
+ } else if (count) {
+ detokenisedStream += " (";
+ }
+ for (uint i = 0; i < argumentCount; i++) {
+ uint16 operand = READ_BE_UINT16(&tokenisedCondition[argumentPointer + 2 * i]);
+ types[i] = operand & 0x8000 ? Token::VARIABLE : Token::CONSTANT;
+ operands[i] = types[i] == Token::VARIABLE ? operand & 0xff : (operand & 0x4000 ? int32(operand) - 0x8000 : operand);
+ if (i)
+ detokenisedStream += ", ";
+ detokenisedStream += Common::String::format(types[i] == Token::VARIABLE ? "v%d" : "%d", operands[i]);
+ }
+ if (argumentCount)
+ detokenisedStream += ")";
+ detokenisedStream += "\n";
+
+ switch (entry->type) {
+ case Token::CONDITIONAL:
+ operands[0] = entry->event;
+ types[0] = Token::CONSTANT;
+ break;
+ case Token::LOOP:
+ loops++;
+ break;
+ case Token::AGAIN:
+ if (--loops < 0)
+ error("16-bit FCL AGAIN without LOOP");
+ break;
+ default:
+ break;
+ }
+ instruction.setSource(operands[0], types[0]);
+ instruction.setDestination(operands[1], types[1]);
+ instruction.setAdditional(operands[2], types[2]);
+ normaliseKitOperands(instruction);
+ instructions.push_back(instruction);
+ bytePointer += 2 + 2 * count;
+ }
+ if (loops)
+ error("Unterminated 16-bit FCL LOOP");
+ return detokenisedStream;
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/language/8bitKitDetokeniser.cpp b/engines/freescape/language/detokeniser_3dck8.cpp
similarity index 79%
rename from engines/freescape/language/8bitKitDetokeniser.cpp
rename to engines/freescape/language/detokeniser_3dck8.cpp
index 05e38de0d34..be390e31052 100644
--- a/engines/freescape/language/8bitKitDetokeniser.cpp
+++ b/engines/freescape/language/detokeniser_3dck8.cpp
@@ -20,19 +20,12 @@
*/
#include "common/textconsole.h"
-#include "freescape/language/8bitKitDetokeniser.h"
-namespace Freescape {
+#include "freescape/language/detokeniser.h"
-struct KitOpcode {
- byte opcode;
- Token::Type token;
- const char *name;
- byte minArgs, maxArgs;
- byte event;
-};
+namespace Freescape {
-static const KitOpcode kKitOpcodes[] = {
+static const FCLOpcode kKitOpcodes[] = {
{0x00, Token::SETVAR, "SETV", 2, 2, 0},
{0x01, Token::ADDVAR, "ADDV", 2, 2, 0},
{0x02, Token::ADCV, "ADCV", 2, 2, 0},
@@ -79,10 +72,10 @@ static const KitOpcode kKitOpcodes[] = {
{0x3f, Token::END, "END", 0, 0, 0}
};
-Common::String detokenise8bitKitCondition(const Common::Array<byte> &code, FCLInstructionVector &instructions) {
+Common::String detokeniseKit8Condition(const Common::Array<byte> &tokenisedCondition, FCLInstructionVector &instructions) {
Common::String source;
- for (uint pos = 0; pos < code.size();) {
- byte raw = code[pos++];
+ for (uint pos = 0; pos < tokenisedCondition.size();) {
+ byte raw = tokenisedCondition[pos++];
if (raw == 0xff) {
instructions.push_back(FCLInstruction(Token::ENDOFFILE));
return source;
@@ -91,36 +84,31 @@ Common::String detokenise8bitKitCondition(const Common::Array<byte> &code, FCLIn
bool variableSource = opcode >= 0x10 && opcode <= 0x19;
if (variableSource)
opcode &= ~0x10;
- const KitOpcode *entry = nullptr;
- for (const auto &candidate : kKitOpcodes) {
- if (candidate.opcode == opcode) {
- entry = &candidate;
- break;
- }
- }
+ const FCLOpcode *entry = findFCLOpcode(kKitOpcodes, opcode);
if (!entry)
error("Unsupported 8-bit 3D Construction Kit opcode %02x", opcode);
uint count = raw >> 6;
- if (count < entry->minArgs || count > entry->maxArgs || count > code.size() - pos)
+ if (count < entry->minArgs || count > entry->maxArgs || count > tokenisedCondition.size() - pos)
error("Invalid 8-bit 3D Construction Kit %s operands", entry->name);
- FCLInstruction instruction(entry->token);
+ FCLInstruction instruction(entry->type);
if (count > 0)
- instruction.setSource(code[pos], variableSource ? Token::VARIABLE : Token::CONSTANT);
+ instruction.setSource(tokenisedCondition[pos], variableSource ? Token::VARIABLE : Token::CONSTANT);
if (count > 1)
- instruction.setDestination(code[pos + 1], opcode <= 9 ? Token::VARIABLE : Token::CONSTANT);
+ instruction.setDestination(tokenisedCondition[pos + 1], opcode <= 9 ? Token::VARIABLE : Token::CONSTANT);
if (count > 2)
- instruction.setAdditional(code[pos + 2]);
+ instruction.setAdditional(tokenisedCondition[pos + 2]);
if (entry->event) {
if (count > 1)
- instruction.setAdditional(code[pos + 1]);
+ instruction.setAdditional(tokenisedCondition[pos + 1]);
if (count > 0)
- instruction.setDestination(code[pos]);
+ instruction.setDestination(tokenisedCondition[pos]);
instruction.setSource(entry->event);
}
+ normaliseKitOperands(instruction);
instructions.push_back(instruction);
source += entry->name;
for (uint arg = 0; arg < count; arg++)
- source += Common::String::format(" %u", code[pos + arg]);
+ source += Common::String::format(" %u", tokenisedCondition[pos + arg]);
source += '\n';
pos += count;
}
diff --git a/engines/freescape/language/8bitDetokeniser.cpp b/engines/freescape/language/detokeniser_freescape.cpp
similarity index 98%
rename from engines/freescape/language/8bitDetokeniser.cpp
rename to engines/freescape/language/detokeniser_freescape.cpp
index 80a2f653346..b41d9f096fb 100644
--- a/engines/freescape/language/8bitDetokeniser.cpp
+++ b/engines/freescape/language/detokeniser_freescape.cpp
@@ -25,13 +25,12 @@
// https://web.archive.org/web/20200116141513/http://www.seasip.demon.co.uk/ZX/Driller/
#include "freescape/freescape.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/detokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
-uint8 k8bitVariableShield = 63;
-
-Common::String detokenise8bitCondition(Common::Array<uint16> &tokenisedCondition, FCLInstructionVector &instructions, bool isAmigaAtari) {
+Common::String detokeniseFreescapeCondition(const Common::Array<uint16> &tokenisedCondition, FCLInstructionVector &instructions, bool isAmigaAtari) {
Common::String detokenisedStream;
Common::Array<uint8>::size_type bytePointer = 0;
Common::Array<uint8>::size_type sizeOfTokenisedContent = tokenisedCondition.size();
diff --git a/engines/freescape/language/execution.h b/engines/freescape/language/execution.h
new file mode 100644
index 00000000000..fc44a51c1ae
--- /dev/null
+++ b/engines/freescape/language/execution.h
@@ -0,0 +1,63 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef FREESCAPE_LANGUAGE_EXECUTION_H
+#define FREESCAPE_LANGUAGE_EXECUTION_H
+
+#include "freescape/language/instruction.h"
+
+namespace Freescape {
+
+enum FCLExecutionResult {
+ kFCLFinished,
+ kFCLYielded, // Resume this script after the other scripts in the frame.
+ kFCLPaused // Resume this script before advancing the frame.
+};
+
+struct FCLExecutionFrame {
+ const FCLInstructionVector *code;
+ uint32 ip = 0;
+ explicit FCLExecutionFrame(const FCLInstructionVector *instructions = nullptr) : code(instructions) {}
+};
+
+struct FCLPredicateState {
+ bool value, previousValue;
+ Token::Type operation = Token::UNKNOWN;
+ explicit FCLPredicateState(bool initialValue = false) : value(initialValue), previousValue(initialValue) {}
+
+ void combine(Token::Type op) {
+ previousValue = value;
+ operation = op;
+ }
+
+ void set(bool result) {
+ if (operation == Token::AND)
+ result = previousValue && result;
+ else if (operation == Token::OR)
+ result = previousValue || result;
+ value = result;
+ operation = Token::UNKNOWN;
+ }
+};
+
+} // namespace Freescape
+
+#endif
diff --git a/engines/freescape/language/instruction16bit.cpp b/engines/freescape/language/execution_3dck16.cpp
similarity index 95%
rename from engines/freescape/language/instruction16bit.cpp
rename to engines/freescape/language/execution_3dck16.cpp
index d923c963486..271aa4ad2c6 100644
--- a/engines/freescape/language/instruction16bit.cpp
+++ b/engines/freescape/language/execution_3dck16.cpp
@@ -75,8 +75,7 @@ void KitEngine::startScript(ScriptState &script) {
script.code = script.source;
script.ip = script.restart = 0;
script.loops.clear();
- script.predicate = script.previousPredicate = true;
- script.booleanOp = Token::UNKNOWN;
+ script.predicate = FCLPredicateState(true);
script.events = script.object ? script.object->flags & 0x38 : 0;
if (script.object)
script.object->flags &= ~0x38;
@@ -245,15 +244,6 @@ int32 KitEngine::getVariableOrConstant(int32 operand, Token::Type type) const {
return type == Token::VARIABLE ? int32(_kitVariables[operand]) : operand;
}
-void KitEngine::setScriptPredicate(ScriptState &script, bool value) {
- if (script.booleanOp == Token::AND)
- value = script.previousPredicate && value;
- else if (script.booleanOp == Token::OR)
- value = script.previousPredicate || value;
- script.predicate = value;
- script.booleanOp = Token::UNKNOWN;
-}
-
KitEngine::ObjectData *KitEngine::scriptObject(uint16 area, uint16 id) {
if (!area)
area = _currentArea->getAreaID();
@@ -323,13 +313,12 @@ FCLExecutionResult KitEngine::executeCode(ScriptState &script, uint &budget) {
case Token::ENDIF:
break;
case Token::IF:
- script.predicate = true;
- script.booleanOp = Token::UNKNOWN;
+ script.predicate.value = true;
+ script.predicate.operation = Token::UNKNOWN;
break;
case Token::AND:
case Token::OR:
- script.previousPredicate = script.predicate;
- script.booleanOp = instruction.getType();
+ script.predicate.combine(instruction.getType());
break;
case Token::THEN:
case Token::ELSE:
@@ -370,7 +359,7 @@ FCLExecutionResult KitEngine::executeCode(ScriptState &script, uint &budget) {
case Token::INVISQ:
case Token::VISQ:
case Token::DESTROYEDQ:
- setScriptPredicate(script, checkObjectStatus(instruction));
+ script.predicate.set(checkObjectStatus(instruction));
break;
case Token::GETXPOS:
case Token::GETYPOS:
@@ -378,7 +367,7 @@ FCLExecutionResult KitEngine::executeCode(ScriptState &script, uint &budget) {
executeGetPosition(instruction);
break;
case Token::EXECUTE:
- if (!executeExecute(instruction, script))
+ if (!executeCall(instruction, script))
return kFCLFinished;
break;
case Token::GOTO:
@@ -465,7 +454,7 @@ FCLExecutionResult KitEngine::executeCode(ScriptState &script, uint &budget) {
}
void KitEngine::executeIfThenElse(const FCLInstruction &instruction, ScriptState &script) {
- if (instruction.getType() == Token::THEN && script.predicate)
+ if (instruction.getType() == Token::THEN && script.predicate.value)
return;
const FCLInstructionVector &code = *script.code;
int depth = 0;
@@ -485,14 +474,14 @@ void KitEngine::executeIfThenElse(const FCLInstruction &instruction, ScriptState
void KitEngine::executeConditional(const FCLInstruction &instruction, ScriptState &script) {
// RUNVGA retains these flags until the object's execution yields or ends.
- setScriptPredicate(script, checkConditional(instruction,
+ script.predicate.set(checkConditional(instruction,
script.events & 16, script.events & 32, _timerTriggered, script.events & 8));
}
void KitEngine::setVariableResult(const FCLInstruction &instruction, ScriptState &script, uint32 value) {
if (instruction._sourceType == Token::VARIABLE)
setScriptVariable(instruction._source, value);
- setScriptPredicate(script, value != 0);
+ script.predicate.set(value != 0);
}
void KitEngine::executeSetVariable(const FCLInstruction &instruction, ScriptState &script) {
@@ -534,13 +523,13 @@ void KitEngine::executeVariableComparison(const FCLInstruction &instruction, Scr
int32 destination = getVariableOrConstant(instruction._destination, instruction._destinationType);
switch (instruction.getType()) {
case Token::VAREQ:
- setScriptPredicate(script, source == destination);
+ script.predicate.set(source == destination);
break;
case Token::VARGT:
- setScriptPredicate(script, source > destination);
+ script.predicate.set(source > destination);
break;
case Token::VARLT:
- setScriptPredicate(script, source < destination);
+ script.predicate.set(source < destination);
break;
default:
break;
@@ -587,7 +576,7 @@ void KitEngine::executeGetPosition(const FCLInstruction &instruction) {
}
}
-bool KitEngine::executeExecute(const FCLInstruction &instruction, ScriptState &script) {
+bool KitEngine::executeCall(const FCLInstruction &instruction, ScriptState &script) {
uint16 id = getVariableOrConstant(instruction._source, instruction._sourceType);
ObjectData *object = scriptObject(0, id);
if (!object || object->type == kGroupType)
@@ -643,7 +632,7 @@ void KitEngine::executeLoop(const FCLInstruction &instruction, ScriptState &scri
}
end++;
}
- FCLLoop &loop = script.loops[end];
+ FCLKit16Loop &loop = script.loops[end];
loop.start = script.ip;
loop.remaining = getVariableOrConstant(instruction._source, instruction._sourceType);
}
@@ -730,7 +719,7 @@ void KitEngine::executeMove(const FCLInstruction &instruction, ScriptState &scri
int16 y = getVariableOrConstant(instruction._destination, instruction._destinationType);
int16 z = getVariableOrConstant(instruction._additional, instruction._additionalType);
bool absolute = instruction.getType() == Token::MOVETO;
- setScriptPredicate(script, moveAnimation(script, Math::Vector3d(x, y, z), absolute));
+ script.predicate.set(moveAnimation(script, Math::Vector3d(x, y, z), absolute));
}
void KitEngine::executeSound(const FCLInstruction &instruction) {
diff --git a/engines/freescape/language/instruction16bit.h b/engines/freescape/language/execution_3dck16.h
similarity index 69%
rename from engines/freescape/language/instruction16bit.h
rename to engines/freescape/language/execution_3dck16.h
index 7b1795c3a9b..0e9c92a9367 100644
--- a/engines/freescape/language/instruction16bit.h
+++ b/engines/freescape/language/execution_3dck16.h
@@ -19,30 +19,28 @@
*
*/
-#ifndef FREESCAPE_INSTRUCTION16BIT_H
-#define FREESCAPE_INSTRUCTION16BIT_H
+#ifndef FREESCAPE_LANGUAGE_EXECUTION_3DCK16_H
+#define FREESCAPE_LANGUAGE_EXECUTION_3DCK16_H
#include "common/hashmap.h"
-#include "freescape/language/instruction.h"
+#include "freescape/language/execution.h"
namespace Freescape {
-struct FCLLoop {
+struct FCLKit16Loop {
uint32 start = 0;
uint16 remaining = 0;
};
-struct FCLExecutionState {
- const FCLInstructionVector *source = nullptr, *code = nullptr;
- uint32 ip = 0, restart = 0;
- Common::HashMap<uint32, FCLLoop> loops;
+// Each condition or animator keeps its own continuation and predicate.
+struct FCLKit16ExecutionState : FCLExecutionFrame {
+ const FCLInstructionVector *source = nullptr;
+ uint32 restart = 0;
+ Common::HashMap<uint32, FCLKit16Loop> loops;
bool running = false;
- bool predicate = true, previousPredicate = true;
- Token::Type booleanOp = Token::UNKNOWN;
+ FCLPredicateState predicate = FCLPredicateState(true);
};
-enum FCLExecutionResult { kFCLFinished, kFCLYielded, kFCLPaused };
-
} // namespace Freescape
#endif
diff --git a/engines/freescape/language/instruction8bitKit.cpp b/engines/freescape/language/execution_3dck8.cpp
similarity index 58%
rename from engines/freescape/language/instruction8bitKit.cpp
rename to engines/freescape/language/execution_3dck8.cpp
index 65fca4ee461..5e4e31d73cf 100644
--- a/engines/freescape/language/instruction8bitKit.cpp
+++ b/engines/freescape/language/execution_3dck8.cpp
@@ -25,18 +25,37 @@
namespace Freescape {
+void Kit8Engine::resetScripts() {
+ memset(_kitVariables, 0, sizeof(_kitVariables));
+ _changedVariables = 0;
+ _currentKey = 255;
+ _textColor = 7;
+ _kitVariables[121] = _kitVariables[125] = 255;
+ _kitVariables[127] = 0x9c;
+ _script = ScriptState();
+ _activeConditions = nullptr;
+ _initialScriptPending = true;
+ _scriptFrameActive = false;
+ _shotObject = _hitObject = _activatedObject = 0;
+ _fallen = _crushed = _pendingTimer = _timerTriggered = false;
+ _crossVisible = true;
+ _timerTicks = _timerInterval = _delayUntil = 0;
+ _lastTime = g_system->getMillis();
+ _scriptSurface.fillRect(_fullscreenViewArea, 255);
+}
+
void Kit8Engine::readSystemVariables() {
float scale = 2 * _currentArea->getScale();
for (int axis = 0; axis < 3; axis++) {
uint16 position = int32(round(_position.getValue(axis) * scale));
- _variables[112 + 2 * axis] = position;
- _variables[113 + 2 * axis] = position >> 8;
+ _kitVariables[112 + 2 * axis] = position;
+ _kitVariables[113 + 2 * axis] = position >> 8;
}
- _variables[118] = (int(round(_pitch / 5)) % 72 + 72) % 72;
- _variables[119] = (int(round((90 - _yaw) / 5)) % 72 + 72) % 72;
- _variables[120] = (int(round(_roll / 5)) % 72 + 72) % 72;
- _variables[121] = _currentKey;
- _variables[124] = _currentArea->getAreaID();
+ _kitVariables[118] = (int(round(_pitch / 5)) % 72 + 72) % 72;
+ _kitVariables[119] = (int(round((90 - _yaw) / 5)) % 72 + 72) % 72;
+ _kitVariables[120] = (int(round(_roll / 5)) % 72 + 72) % 72;
+ _kitVariables[121] = _currentKey;
+ _kitVariables[124] = _currentArea->getAreaID();
_changedVariables = 0;
}
@@ -44,17 +63,17 @@ void Kit8Engine::writeSystemVariables() {
float scale = 2 * _currentArea->getScale();
for (int axis = 0; axis < 3; axis++) {
if (_changedVariables & (3 << (2 * axis)))
- _position.setValue(axis, (_variables[112 + 2 * axis] | (_variables[113 + 2 * axis] << 8)) / scale);
+ _position.setValue(axis, (_kitVariables[112 + 2 * axis] | (_kitVariables[113 + 2 * axis] << 8)) / scale);
}
if (_changedVariables & (1 << 6)) {
- _pitch = 5 * (_variables[118] % 72);
+ _pitch = 5 * (_kitVariables[118] % 72);
if (_pitch > 180)
_pitch -= 360;
}
if (_changedVariables & (1 << 7))
- _yaw = 90 - 5 * _variables[119];
+ _yaw = 90 - 5 * _kitVariables[119];
if (_changedVariables & (1 << 8))
- _roll = 5 * _variables[120];
+ _roll = 5 * _kitVariables[120];
if (_changedVariables & 0x1c0)
updateCamera();
if (_changedVariables & 0x3f)
@@ -66,9 +85,9 @@ void Kit8Engine::updateTimeVariables() {
uint32 now = g_system->getMillis();
uint32 elapsed = (now - _lastTime) / 20;
_lastTime += 20 * elapsed;
- uint16 counter = (_variables[122] | (_variables[123] << 8)) + elapsed;
- _variables[122] = counter;
- _variables[123] = counter >> 8;
+ uint16 counter = (_kitVariables[122] | (_kitVariables[123] << 8)) + elapsed;
+ _kitVariables[122] = counter;
+ _kitVariables[123] = counter >> 8;
_timerTicks += elapsed;
uint32 interval = MAX<uint32>(1, _timerInterval);
if (_timerTicks >= interval) {
@@ -77,12 +96,32 @@ void Kit8Engine::updateTimeVariables() {
}
}
-void Kit8Engine::startScript(const FCLInstructionVector &code) {
- if (_scriptStack.size() >= 64)
+void Kit8Engine::startScript(ScriptState &script, const FCLInstructionVector &code) {
+ if (script.stack.size() >= 64)
error("8-bit 3D Construction Kit procedure stack overflow");
- _scriptStack.push_back(ScriptFrame(code));
- _executing = true;
- _booleanOp = Token::UNKNOWN;
+ script.stack.push_back(FCLExecutionFrame(&code));
+ script.executing = true;
+ script.predicate.operation = Token::UNKNOWN;
+}
+
+void Kit8Engine::beginScriptFrame() {
+ _scriptFrameActive = true;
+ readSystemVariables();
+ _timerTriggered = _pendingTimer;
+ _pendingTimer = false;
+ _scriptSurface.fillRect(_viewArea, 255);
+ _activeConditions = &_globalConditions;
+ _conditionIndex = 0;
+ _globalPhase = true;
+ if (_initialScriptPending) {
+ _initialScriptPending = false;
+ for (const auto &condition : _globalConditions) {
+ if (condition.id == 1) {
+ startScript(_script, condition.condition);
+ break;
+ }
+ }
+ }
}
void Kit8Engine::updateScripts() {
@@ -93,57 +132,40 @@ void Kit8Engine::updateScripts() {
if (_delayUntil && int32(_delayUntil - g_system->getMillis()) > 0)
return;
_delayUntil = 0;
- if (!_scriptFrameActive) {
- _scriptFrameActive = true;
- readSystemVariables();
- _timerTriggered = _pendingTimer;
- _pendingTimer = false;
- _scriptSurface.fillRect(_viewArea, 255);
- _conditions = &_globalConditions;
- _conditionIndex = 0;
- _globalPhase = true;
- if (_initialScriptPending) {
- _initialScriptPending = false;
- for (const auto &condition : _globalConditions) {
- if (condition.id == 1) {
- startScript(condition.code);
- break;
- }
- }
- }
- }
+ if (!_scriptFrameActive)
+ beginScriptFrame();
uint budget = 4096;
while (budget) {
- if (_scriptStack.empty()) {
- if (_conditionIndex == _conditions->size()) {
+ if (_script.stack.empty()) {
+ if (_conditionIndex == _activeConditions->size()) {
if (!_globalPhase)
break;
_globalPhase = false;
- _conditions = &_areaData[_currentArea->getAreaID()].conditions;
+ _activeConditions = &_areaData[_currentArea->getAreaID()].conditions;
_conditionIndex = 0;
continue;
}
- const ConditionData &condition = (*_conditions)[_conditionIndex++];
+ const ConditionData &condition = (*_activeConditions)[_conditionIndex++];
if (_globalPhase && condition.id == 1)
continue;
- startScript(condition.code);
+ startScript(_script, condition.condition);
}
- if (!executeCode(budget))
+ if (executeCode(_script, budget) != kFCLFinished)
return;
}
writeSystemVariables();
- if (!_scriptStack.empty() || _globalPhase || _conditionIndex < _conditions->size())
+ if (!_script.stack.empty() || _globalPhase || _conditionIndex < _activeConditions->size())
return;
updateInstruments();
_scriptFrameActive = false;
_shotObject = _hitObject = _activatedObject = 0;
}
-bool Kit8Engine::executeCode(uint &budget) {
- while (!_scriptStack.empty() && budget) {
- ScriptFrame &frame = _scriptStack.back();
+FCLExecutionResult Kit8Engine::executeCode(ScriptState &script, uint &budget) {
+ while (!script.stack.empty() && budget) {
+ FCLExecutionFrame &frame = script.stack.back();
if (frame.ip == frame.code->size()) {
- _scriptStack.pop_back();
+ script.stack.pop_back();
continue;
}
const FCLInstruction &instruction = (*frame.code)[frame.ip++];
@@ -152,35 +174,34 @@ bool Kit8Engine::executeCode(uint &budget) {
// Condition statements also execute while a THEN/ELSE branch is skipped.
switch (op) {
case Token::ENDOFFILE:
- _scriptStack.pop_back();
+ script.stack.pop_back();
continue;
case Token::CONDITIONAL:
- executeConditional(instruction);
+ executeConditional(instruction, script);
continue;
case Token::VISQ:
case Token::IFEQ:
case Token::IFGT:
case Token::IFLT:
- executeComparison(instruction);
+ executeComparison(instruction, script);
continue;
case Token::AND:
case Token::OR:
- _previousZero = _zero;
- _booleanOp = op;
+ script.predicate.combine(op);
continue;
case Token::THEN:
- _executing = _zero;
+ script.executing = script.predicate.value;
continue;
case Token::ELSE:
- _executing = !_executing;
+ script.executing = !script.executing;
continue;
case Token::ENDIF:
- _executing = true;
+ script.executing = true;
continue;
default:
break;
}
- if (!_executing)
+ if (!script.executing)
continue;
switch (op) {
case Token::SETVAR:
@@ -193,7 +214,7 @@ bool Kit8Engine::executeCode(uint &budget) {
case Token::XORV:
case Token::TESTV:
case Token::CMPV:
- executeArithmetic(instruction);
+ executeArithmetic(instruction, script);
break;
case Token::VIS:
case Token::INVIS:
@@ -208,15 +229,15 @@ bool Kit8Engine::executeCode(uint &budget) {
executeMode(instruction);
break;
case Token::EXECUTE:
- executeCall(instruction);
+ executeCall(instruction, script);
break;
case Token::END:
- _scriptStack.pop_back();
+ script.stack.pop_back();
break;
case Token::ENDGAME:
_gameStateControl = kFreescapeGameStateRestart;
- _scriptStack.clear();
- return false;
+ script.stack.clear();
+ return kFCLPaused;
case Token::TIMER:
_timerInterval = instruction._source;
break;
@@ -232,7 +253,7 @@ bool Kit8Engine::executeCode(uint &budget) {
break;
case Token::DELAY:
_delayUntil = g_system->getMillis() + 20 * (instruction._source ? instruction._source : 256);
- return false;
+ return kFCLPaused;
case Token::TEXTCOL:
_textColor = instruction._source;
break;
@@ -243,60 +264,60 @@ bool Kit8Engine::executeCode(uint &budget) {
writeSystemVariables();
_scriptSurface.fillRect(_viewArea, 255);
updateInstruments();
- return false;
+ return kFCLPaused;
default:
error("Unsupported 8-bit 3D Construction Kit instruction %u", op);
}
}
- return _scriptStack.empty();
-}
-
-void Kit8Engine::setPredicate(bool value) {
- if (_booleanOp == Token::AND)
- value = _previousZero && value;
- else if (_booleanOp == Token::OR)
- value = _previousZero || value;
- _zero = value;
- _booleanOp = Token::UNKNOWN;
+ return script.stack.empty() ? kFCLFinished : kFCLPaused;
}
-void Kit8Engine::executeArithmetic(const FCLInstruction &instruction) {
- byte index = instruction._destination & 127;
- byte source = instruction._sourceType == Token::VARIABLE ? _variables[instruction._source & 127] : instruction._source;
- int destination = _variables[index];
- int result = destination;
+void Kit8Engine::executeArithmetic(const FCLInstruction &instruction, ScriptState &script) {
+ byte index = instruction._source & 127;
+ byte operand = instruction._destinationType == Token::VARIABLE ? _kitVariables[instruction._destination & 127] : instruction._destination;
+ int value = _kitVariables[index];
+ int result = value;
bool store = true;
switch (instruction.getType()) {
- case Token::SETVAR: result = source; break;
- case Token::ADDVAR: result += source; break;
- case Token::ADCV: result += source + (_carry ? 1 : 0); break;
- case Token::SUBVAR: result -= source; break;
- case Token::SBCV: result -= source + (_carry ? 1 : 0); break;
- case Token::ANDV: result &= source; break;
- case Token::ORV: result |= source; break;
- case Token::XORV: result ^= source; break;
+ case Token::SETVAR: result = operand; break;
+ case Token::ADDVAR: result += operand; break;
+ case Token::ADCV: result += operand + (script.carry ? 1 : 0); break;
+ case Token::SUBVAR: result -= operand; break;
+ case Token::SBCV: result -= operand + (script.carry ? 1 : 0); break;
+ case Token::ANDV: result &= operand; break;
+ case Token::ORV: result |= operand; break;
+ case Token::XORV: result ^= operand; break;
case Token::TESTV:
- result &= source;
+ result &= operand;
store = false;
break;
case Token::CMPV:
- result = source - destination;
+ result = operand - value;
store = false;
break;
default:
break;
}
if (store) {
- _variables[index] = result;
+ _kitVariables[index] = result;
if (index >= 112 && index <= 120)
_changedVariables |= 1 << (index - 112);
}
- _carry = result < 0 || result > 255;
+ script.carry = result < 0 || result > 255;
if (instruction.getType() == Token::SETVAR) {
// SETV retains the CPC operand decoder's zero flag.
- setPredicate(instruction._sourceType != Token::VARIABLE);
+ script.predicate.set(instruction._destinationType != Token::VARIABLE);
} else
- setPredicate(byte(result) == 0);
+ script.predicate.set(byte(result) == 0);
+}
+
+void Kit8Engine::getObjectReference(const FCLInstruction &instruction, uint16 &area, uint16 &id) const {
+ area = _currentArea->getAreaID();
+ id = instruction._source;
+ if (instruction._destinationType != Token::UNKNOWN) {
+ area = instruction._source;
+ id = instruction._destination;
+ }
}
Object *Kit8Engine::scriptObject(uint16 area, uint16 id) {
@@ -308,21 +329,22 @@ Object *Kit8Engine::scriptObject(uint16 area, uint16 id) {
return object;
}
-void Kit8Engine::executeComparison(const FCLInstruction &instruction) {
+void Kit8Engine::executeComparison(const FCLInstruction &instruction, ScriptState &script) {
if (instruction.getType() == Token::VISQ) {
- uint16 area = instruction._destinationType == Token::UNKNOWN ? _currentArea->getAreaID() : instruction._destination;
- Object *object = scriptObject(area, instruction._source);
- _carry = false;
- setPredicate(!object || (!object->isInvisible() && !object->isDestroyed()));
+ uint16 area, id;
+ getObjectReference(instruction, area, id);
+ Object *object = scriptObject(area, id);
+ script.carry = false;
+ script.predicate.set(!object || (!object->isInvisible() && !object->isDestroyed()));
} else if (instruction.getType() == Token::IFGT)
- setPredicate(!_zero && _carry);
+ script.predicate.set(!script.predicate.value && script.carry);
else if (instruction.getType() == Token::IFLT)
- setPredicate(!_zero && !_carry);
+ script.predicate.set(!script.predicate.value && !script.carry);
else
- setPredicate(_zero);
+ script.predicate.set(script.predicate.value);
}
-void Kit8Engine::executeConditional(const FCLInstruction &instruction) {
+void Kit8Engine::executeConditional(const FCLInstruction &instruction, ScriptState &script) {
int area = instruction._additionalType == Token::UNKNOWN ? _currentArea->getAreaID() : instruction._additional;
int id = instruction._destination;
int value = 0;
@@ -331,21 +353,21 @@ void Kit8Engine::executeConditional(const FCLInstruction &instruction) {
case kConditionalCollided: value = _hitObject; break;
case kConditionalActivated: value = _activatedObject; break;
case kConditionalTimeout:
- _carry = false;
- setPredicate(_timerTriggered);
+ script.carry = false;
+ script.predicate.set(_timerTriggered);
return;
case kConditionalFallen:
- setPredicate(_fallen);
+ script.predicate.set(_fallen);
_fallen = false;
return;
case kConditionalCrushed:
- setPredicate(_crushed);
+ script.predicate.set(_crushed);
_crushed = false;
return;
case kConditionalSensed: {
Object *object = scriptObject(area, id);
- _carry = false;
- setPredicate(!object || (object->getType() == kSensorType && static_cast<Sensor *>(object)->isShooting()));
+ script.carry = false;
+ script.predicate.set(!object || (object->getType() == kSensorType && static_cast<Sensor *>(object)->isShooting()));
return;
}
default:
@@ -355,13 +377,14 @@ void Kit8Engine::executeConditional(const FCLInstruction &instruction) {
value = _currentArea->getAreaID();
id = area;
}
- _carry = value < id;
- setPredicate(value == id);
+ script.carry = value < id;
+ script.predicate.set(value == id);
}
void Kit8Engine::executeObjectStatus(const FCLInstruction &instruction) {
- uint16 area = instruction._destinationType == Token::UNKNOWN ? _currentArea->getAreaID() : instruction._destination;
- Object *object = scriptObject(area, instruction._source);
+ uint16 area, id;
+ getObjectReference(instruction, area, id);
+ Object *object = scriptObject(area, id);
if (!object || object->isDestroyed())
return;
switch (instruction.getType()) {
@@ -378,9 +401,9 @@ void Kit8Engine::executeObjectStatus(const FCLInstruction &instruction) {
}
void Kit8Engine::executeGoto(const FCLInstruction &instruction) {
- uint16 area = instruction._destinationType == Token::UNKNOWN ? _currentArea->getAreaID() : instruction._destination;
+ uint16 area = instruction._sourceType == Token::UNKNOWN ? _currentArea->getAreaID() : instruction._source;
writeSystemVariables();
- gotoArea(area, instruction._source);
+ gotoArea(area, instruction._destination);
}
void Kit8Engine::executeMode(const FCLInstruction &instruction) {
@@ -389,10 +412,10 @@ void Kit8Engine::executeMode(const FCLInstruction &instruction) {
readSystemVariables();
}
-void Kit8Engine::executeCall(const FCLInstruction &instruction) {
+void Kit8Engine::executeCall(const FCLInstruction &instruction, ScriptState &script) {
for (const auto &procedure : _procedures) {
if (procedure.id == instruction._source) {
- startScript(procedure.code);
+ startScript(script, procedure.condition);
return;
}
}
diff --git a/engines/freescape/language/8bitKitDetokeniser.h b/engines/freescape/language/execution_3dck8.h
similarity index 72%
rename from engines/freescape/language/8bitKitDetokeniser.h
rename to engines/freescape/language/execution_3dck8.h
index dab26bb8abc..c80d5222ef5 100644
--- a/engines/freescape/language/8bitKitDetokeniser.h
+++ b/engines/freescape/language/execution_3dck8.h
@@ -19,14 +19,20 @@
*
*/
-#ifndef FREESCAPE_8BIT_KIT_DETOKENISER_H
-#define FREESCAPE_8BIT_KIT_DETOKENISER_H
+#ifndef FREESCAPE_LANGUAGE_EXECUTION_3DCK8_H
+#define FREESCAPE_LANGUAGE_EXECUTION_3DCK8_H
-#include "freescape/language/instruction.h"
+#include "freescape/language/execution.h"
namespace Freescape {
-Common::String detokenise8bitKitCondition(const Common::Array<byte> &code, FCLInstructionVector &instructions);
+// The byte interpreter shares its zero/carry flags across procedure calls.
+struct FCLKit8ExecutionState {
+ Common::Array<FCLExecutionFrame> stack;
+ FCLPredicateState predicate;
+ bool executing = true;
+ bool carry = false;
+};
} // namespace Freescape
diff --git a/engines/freescape/language/execution_freescape.cpp b/engines/freescape/language/execution_freescape.cpp
new file mode 100644
index 00000000000..7ef56674c3c
--- /dev/null
+++ b/engines/freescape/language/execution_freescape.cpp
@@ -0,0 +1,870 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Based on Phantasma code by Thomas Harte (2013),
+// available at https://github.com/TomHarte/Phantasma/ (MIT)
+
+#include "freescape/freescape.h"
+#include "freescape/language/variables.h"
+#include "freescape/sweepAABB.h"
+
+namespace Freescape {
+
+uint8 k8bitVariableShield = 63;
+
+static bool isFreescapeConditional(const FCLInstruction &instruction) {
+ Token::Type type = instruction.getType();
+ return type == Token::BITNOTEQ || type == Token::VARNOTEQ ||
+ type == Token::IFGTEQ || type == Token::IFLTEQ ||
+ type == Token::VAREQ || type == Token::INVISQ;
+}
+
+void FreescapeEngine::executeEntranceConditions(Entrance *entrance) {
+ if (!entrance->_conditionSource.empty()) {
+ _firstSound = true;
+ _syncSound = false;
+
+ debugC(1, kFreescapeDebugCode, "Executing entrance condition with collision flag: %s", entrance->_conditionSource.c_str());
+ executeCode(entrance->_condition, false, true, false, false);
+ }
+}
+
+bool FreescapeEngine::executeObjectConditions(GeometricObject *obj, bool shot, bool collided, bool activated) {
+ bool executed = false;
+ assert(obj != nullptr);
+ if (!obj->_conditionSource.empty()) {
+ _firstSound = true;
+ _syncSound = false;
+ _objExecutingCodeSize = collided ? obj->getSize() : Math::Vector3d();
+ if (collided) {
+ if (!isCastle())
+ clearGameBit(31); // We collided with something that has code
+ debugC(1, kFreescapeDebugCode, "Executing with collision flag: %s", obj->_conditionSource.c_str());
+ } else if (shot)
+ debugC(1, kFreescapeDebugCode, "Executing with shot flag: %s", obj->_conditionSource.c_str());
+ else if (activated) {
+ if (isCastle()) // TODO: add a 3DCK check here
+ clearTemporalMessages();
+ debugC(1, kFreescapeDebugCode, "Executing with activated flag: %s", obj->_conditionSource.c_str());
+ } else
+ error("Neither shot or collided flag is set!");
+ executed = executeCode(obj->_condition, shot, collided, false, activated); // TODO: check this last parameter
+ }
+ if (activated && !executed)
+ if (!_noEffectMessage.empty())
+ insertTemporaryMessage(_noEffectMessage, _countdown - 2);
+
+ return executed;
+}
+
+void FreescapeEngine::executeLocalGlobalConditions(bool shot, bool collided, bool timer) {
+ debugC(1, kFreescapeDebugCode, "Executing room conditions");
+ Common::Array<FCLInstructionVector> conditions = _currentArea->_conditions;
+ Common::Array<Common::String> conditionSources = _currentArea->_conditionSources;
+
+ for (uint i = 0; i < conditions.size(); i++) {
+ debugC(1, kFreescapeDebugCode, "%s", conditionSources[i].c_str());
+ executeCode(conditions[i], shot, collided, timer, false);
+ }
+
+ _executingGlobalCode = true;
+ debugC(1, kFreescapeDebugCode, "Executing global conditions (%d)", _conditions.size());
+ for (uint i = 0; i < _conditions.size(); i++) {
+ debugC(1, kFreescapeDebugCode, "%s", _conditionSources[i].c_str());
+ executeCode(_conditions[i], shot, collided, timer, false);
+ }
+ _executingGlobalCode = false;
+}
+
+bool FreescapeEngine::executeCode(FCLInstructionVector &code, bool shot, bool collided, bool timer, bool activated) {
+ int ip = 0;
+ bool skip = false;
+ int skipDepth = 0;
+ int conditionalDepth = 0;
+ bool executed = false;
+ int loopIterations = 0;
+ int loopHead = -1;
+ int codeSize = code.size();
+
+ if (codeSize == 0) {
+ assert(isCastle()); // Only seems to happen in Castle Master (magister room)
+ debugC(1, kFreescapeDebugCode, "Code is empty!");
+ return false;
+ }
+
+ while (ip <= codeSize - 1) {
+ FCLInstruction &instruction = code[ip];
+ debugC(1, kFreescapeDebugCode, "Executing ip: %d with type %d in code with size: %d. Skip flag is: %d", ip, instruction.getType(), codeSize, skip);
+
+ if (isFreescapeConditional(instruction)) {
+ conditionalDepth++;
+ debugC(1, kFreescapeDebugCode, "Conditional depth increased to: %d", conditionalDepth);
+ } else if (instruction.getType() == Token::ENDIF) {
+ conditionalDepth--;
+ debugC(1, kFreescapeDebugCode, "Conditional depth decreased to: %d", conditionalDepth);
+ }
+
+ if (skip) {
+ if (instruction.getType() == Token::ELSE) {
+ debugC(1, kFreescapeDebugCode, "Else found, skip depth: %d, conditional depth: %d", skipDepth, conditionalDepth);
+ if (skipDepth == conditionalDepth - 1) {
+ skip = false;
+ }
+ } else if (instruction.getType() == Token::ENDIF) {
+ debugC(1, kFreescapeDebugCode, "Endif found, skip depth: %d, conditional depth: %d", skipDepth, conditionalDepth);
+ if (skipDepth == conditionalDepth) {
+ skip = false;
+ }
+ }
+ debugC(1, kFreescapeDebugCode, "Instruction skipped!");
+ ip++;
+ continue;
+ }
+
+ if (instruction.getType() != Token::CONDITIONAL && !isFreescapeConditional(instruction))
+ executed = true;
+
+ switch (instruction.getType()) {
+ default:
+ error("Instruction %x at ip: %d not implemented!", instruction.getType(), ip);
+ break;
+ case Token::NOP:
+ debugC(1, kFreescapeDebugCode, "Executing NOP at ip: %d", ip);
+ break;
+
+ case Token::LOOP:
+ loopHead = ip;
+ loopIterations = instruction._source;
+ debugC(1, kFreescapeDebugCode, "Starting loop with %d iterations at ip: %d", loopIterations, ip);
+ break;
+
+ case Token::AGAIN:
+ if (loopIterations > 1) {
+ loopIterations--;
+ ip = loopHead;
+ debugC(1, kFreescapeDebugCode, "Looping again, %d iterations left, jumping to ip: %d", loopIterations, ip);
+ } else if (loopIterations == 1) {
+ loopIterations--;
+ debugC(1, kFreescapeDebugCode, "Loop finished");
+ } else {
+ error("AGAIN found without a matching LOOP!");
+ }
+ break;
+
+ case Token::CONDITIONAL:
+ if (checkConditional(instruction, shot, collided, timer, activated))
+ executed = executeCode(*instruction._thenInstructions, shot, collided, timer, activated);
+ // else branch is always empty
+ assert(instruction._elseInstructions == nullptr);
+ break;
+
+ case Token::VARNOTEQ:
+ if (executeEndIfNotEqual(instruction)) {
+ if (isCastle()) {
+ skip = true;
+ skipDepth = conditionalDepth - 1;
+ } else
+ ip = codeSize;
+ }
+ break;
+ case Token::IFGTEQ:
+ skip = !checkIfGreaterOrEqual(instruction);
+ if (skip)
+ skipDepth = conditionalDepth - 1;
+ break;
+
+ case Token::IFLTEQ:
+ skip = !checkIfLessOrEqual(instruction);
+ if (skip)
+ skipDepth = conditionalDepth - 1;
+ break;
+
+
+ case Token::ELSE:
+ skip = !skip;
+ if (skip)
+ skipDepth = conditionalDepth - 1;
+ break;
+
+ case Token::ENDIF:
+ skip = false;
+ break;
+
+ case Token::SWAPJET:
+ executeSwapJet(instruction);
+ break;
+ case Token::ADDVAR:
+ executeIncrementVariable(instruction);
+ break;
+ case Token::SUBVAR:
+ executeDecrementVariable(instruction);
+ break;
+ case Token::SETVAR:
+ executeSetVariable(instruction);
+ break;
+ case Token::GOTO:
+ executeGoto(instruction);
+ break;
+ case Token::TOGVIS:
+ executeToggleVisibility(instruction);
+ break;
+ case Token::INVIS:
+ executeMakeInvisible(instruction);
+ break;
+ case Token::VIS:
+ executeMakeVisible(instruction);
+ break;
+ case Token::DESTROY:
+ executeDestroy(instruction);
+ break;
+ case Token::REDRAW:
+ executeRedraw(instruction);
+ break;
+ case Token::EXECUTE:
+ executeCall(instruction);
+ ip = codeSize;
+ break;
+ case Token::DELAY:
+ executeDelay(instruction);
+ break;
+ case Token::SOUND:
+ executeSound(instruction);
+ break;
+ case Token::SETBIT:
+ executeSetBit(instruction);
+ break;
+ case Token::CLEARBIT:
+ executeClearBit(instruction);
+ break;
+ case Token::TOGGLEBIT:
+ executeToggleBit(instruction);
+ break;
+ case Token::PRINT:
+ executePrint(instruction);
+ break;
+ case Token::SPFX:
+ executeSPFX(instruction);
+ break;
+ case Token::SCREEN:
+ // TODO
+ break;
+ case Token::SETFLAGS:
+ // TODO
+ break;
+ case Token::STARTANIM:
+ executeStartAnim(instruction);
+ break;
+ case Token::BITNOTEQ:
+ if (executeEndIfBitNotEqual(instruction)) {
+ if (isCastle()) {
+ skip = true;
+ skipDepth = conditionalDepth - 1;
+ } else
+ ip = codeSize;
+ }
+ break;
+ case Token::INVISQ:
+ if (executeEndIfVisibilityIsEqual(instruction)) {
+ if (isCastle()) {
+ skip = true;
+ skipDepth = conditionalDepth - 1;
+ } else
+ ip = codeSize;
+ }
+ break;
+ }
+ ip++;
+ }
+ return executed;
+}
+
+void FreescapeEngine::executeRedraw(FCLInstruction &instruction) {
+ debugC(1, kFreescapeDebugCode, "Redrawing screen");
+ uint32 delay = (100 / 15) + 1;
+ if (isEclipse2() && _currentArea->getAreaID() == _startArea && _gameStateControl == kFreescapeGameStateStart)
+ delay = delay * 10;
+
+ if (isCastle() && (isSpectrum() || isCPC() || isC64()) && getGameBit(31))
+ delay = delay * 15; // Slow down redraws when the final cutscene is playing
+
+ if (isDriller() && (isSpectrum() || isCPC() || isC64()) && _gameStateVars[32] == 18)
+ delay = delay * 15; // Slow down redraws when the final cutscene is playing
+
+ if (isEclipse() && _currentArea->getAreaID() == 37 && getGameBit(6))
+ delay = delay * 10; // Slow down redraws in the final area of Eclipse
+
+ waitInLoop(delay);
+}
+
+void FreescapeEngine::executeCall(FCLInstruction &instruction) {
+ uint16 objId = instruction._source;
+ debugC(1, kFreescapeDebugCode, "Executing instructions from object %d", objId);
+ Object *obj = _currentArea->objectWithID(objId);
+ if (!obj) {
+ obj = _areaMap[255]->objectWithID(objId);
+ if (!obj) {
+ obj = _areaMap[255]->entranceWithID(objId);
+ if (!obj) {
+ debugC(1, kFreescapeDebugCode, "WARNING: executing instructions from a non-existent object %d", objId);
+ return;
+ }
+ assert(obj);
+ FCLInstructionVector &condition = ((Entrance *)obj)->_condition;
+ executeCode(condition, true, true, true, true);
+ return;
+ }
+ }
+ executeObjectConditions((GeometricObject *)obj, true, true, true);
+}
+
+void FreescapeEngine::executeSound(FCLInstruction &instruction) {
+ uint16 index = instruction._source;
+ bool sync = instruction._additional;
+ // An undefined sound index is a no-op in the original engines (e.g.
+ // start_speaker_sound returns early when the DOS table index is 0xFF) and
+ // must NOT disturb the sound that is already playing. Otherwise the
+ // stopAllSounds() below cuts the previous sound and then plays nothing
+ // (e.g. SOUND 15; SOUND 16 where sound 16 is undefined in the data).
+ if (_sound && !_sound->isSoundAvailable(index)) {
+ debugC(1, kFreescapeDebugCode, "Sound %d not available, keeping current sound", index);
+ return;
+ }
+ stopAllSounds(Sound::kTypeMovement);
+ _firstSound = false;
+ debugC(1, kFreescapeDebugCode, "Playing sound %d", index);
+ playSound(index, sync);
+}
+
+void FreescapeEngine::executeDelay(FCLInstruction &instruction) {
+ uint16 delay = instruction._source;
+ debugC(1, kFreescapeDebugCode, "Delaying %d * 1/50 seconds", delay);
+ waitInLoop(((20 * delay) / 15) + 1);
+}
+
+void FreescapeEngine::executePrint(FCLInstruction &instruction) {
+ uint16 index = instruction._source - 1;
+ debugC(1, kFreescapeDebugCode, "Printing message %d: \"%s\"", index, _messagesList[index].c_str());
+ _currentAreaMessages.clear();
+ _currentAreaMessages.push_back(_messagesList[index]);
+}
+
+uint32 spfxBasePaletteColor(FreescapeEngine *engine, uint8 index) {
+ index &= 0x0f;
+ uint8 r = engine->_gfx->_palette[3 * index + 0];
+ uint8 g = engine->_gfx->_palette[3 * index + 1];
+ uint8 b = engine->_gfx->_palette[3 * index + 2];
+ return engine->_gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
+}
+
+uint32 spfxDirectPaletteColor(FreescapeEngine *engine, uint16 value) {
+ uint8 r = (value >> 8) & 0x0f;
+ uint8 g = (value >> 4) & 0x0f;
+ uint8 b = value & 0x0f;
+
+ if (engine->isAtariST()) {
+ r = ((r & 0x07) << 1) | ((r & 0x07) >> 2);
+ g = ((g & 0x07) << 1) | ((g & 0x07) >> 2);
+ b = ((b & 0x07) << 1) | ((b & 0x07) >> 2);
+ }
+
+ r = (r << 4) | r;
+ g = (g << 4) | g;
+ b = (b << 4) | b;
+ return engine->_gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
+}
+
+uint32 spfxActivePaletteColor(FreescapeEngine *engine, uint8 index) {
+ index &= 0x0f;
+ if (engine->_currentArea->_colorRemaps.contains(index))
+ return (uint32)engine->_currentArea->_colorRemaps[index];
+ return spfxBasePaletteColor(engine, index);
+}
+
+void spfxSetActivePaletteColor(FreescapeEngine *engine, uint8 index, uint32 color) {
+ index &= 0x0f;
+ if (color == spfxBasePaletteColor(engine, index))
+ engine->_currentArea->unremapColor(index);
+ else
+ engine->_currentArea->remapColor(index, color);
+}
+
+void spfxFillRange(FreescapeEngine *engine, uint8 start, uint8 end, uint32 color) {
+ if (end < start)
+ return;
+
+ for (int i = start; i <= end; i++)
+ spfxSetActivePaletteColor(engine, i, color);
+}
+
+void spfxRestoreRange(FreescapeEngine *engine, uint8 start, uint8 end) {
+ if (end < start)
+ return;
+
+ for (int i = start; i <= end; i++)
+ engine->_currentArea->unremapColor(i);
+}
+
+void spfxRotateLeft(FreescapeEngine *engine, uint8 start, uint8 end) {
+ if (end <= start)
+ return;
+
+ uint32 color = spfxActivePaletteColor(engine, start);
+ for (int i = start; i < end; i++)
+ spfxSetActivePaletteColor(engine, i, spfxActivePaletteColor(engine, i + 1));
+ spfxSetActivePaletteColor(engine, end, color);
+}
+
+void spfxRotateRight(FreescapeEngine *engine, uint8 start, uint8 end) {
+ if (end <= start)
+ return;
+
+ uint32 color = spfxActivePaletteColor(engine, end);
+ for (int i = end; i > start; i--)
+ spfxSetActivePaletteColor(engine, i, spfxActivePaletteColor(engine, i - 1));
+ spfxSetActivePaletteColor(engine, start, color);
+}
+
+void FreescapeEngine::executeSPFX(FCLInstruction &instruction) {
+ uint16 src = instruction._source;
+ uint16 dst = instruction._destination;
+ if (isAmiga() || isAtariST()) {
+ uint16 raw = ((src & 0xff) << 8) | (dst & 0xff);
+ if (raw & 0x8000) {
+ uint16 color = raw & 0x7770;
+ if (isAmiga())
+ color >>= 3;
+ else
+ color >>= 4;
+
+ spfxSetActivePaletteColor(this, raw & 0x0f, spfxDirectPaletteColor(this, color));
+ } else if ((raw & 0xf000) == 0x1000) {
+ spfxFillRange(this, (raw >> 4) & 0x0f, raw & 0x0f, spfxBasePaletteColor(this, (raw >> 8) & 0x0f));
+ } else {
+ switch (raw & 0x0f00) {
+ case 0x0000:
+ spfxSetActivePaletteColor(this, raw & 0x0f, spfxBasePaletteColor(this, (raw >> 4) & 0x0f));
+ break;
+ case 0x0100:
+ spfxFillRange(this, 0, 14, spfxBasePaletteColor(this, raw & 0x0f));
+ break;
+ case 0x0200:
+ _currentArea->_colorRemaps.clear();
+ break;
+ case 0x0300:
+ spfxRestoreRange(this, (raw >> 4) & 0x0f, raw & 0x0f);
+ break;
+ case 0x0400:
+ spfxRotateLeft(this, (raw >> 4) & 0x0f, raw & 0x0f);
+ break;
+ case 0x0500:
+ spfxRotateRight(this, (raw >> 4) & 0x0f, raw & 0x0f);
+ break;
+ default:
+ break;
+ }
+ }
+ } else {
+ debugC(1, kFreescapeDebugCode, "Switching palette from position %d to %d", src, dst);
+ if (src == 0 && dst == 1) {
+
+ src = _currentArea->_usualBackgroundColor;
+ dst = _currentArea->_underFireBackgroundColor;
+
+ if (_renderMode == Common::kRenderCGA)
+ dst = 1;
+ else if (isC64()) {
+ src %= 16;
+ dst %= 16;
+ }
+
+ _currentArea->remapColor(src, dst);
+ } else if (src == 0 && dst == 0)
+ _currentArea->unremapColor(_currentArea->_usualBackgroundColor);
+ else if (src == 15 && dst == 15) // Found in Total Eclipse (DOS)
+ _currentArea->unremapColor(_currentArea->_usualBackgroundColor);
+ else
+ _currentArea->remapColor(src, dst);
+ }
+ _gfx->setColorRemaps(&_currentArea->_colorRemaps);
+ executeRedraw(instruction);
+}
+
+
+bool FreescapeEngine::executeEndIfVisibilityIsEqual(FCLInstruction &instruction) {
+ uint16 source = instruction._source;
+ uint16 additional = instruction._additional;
+ uint16 value = instruction._destination;
+
+ Object *obj = nullptr;
+ if (additional == 0) {
+ obj = _currentArea->objectWithID(source);
+ if (!obj && isCastle())
+ return (true == (value != 0));
+ assert(obj);
+ debugC(1, kFreescapeDebugCode, "End condition if visibility of obj with id %d is %d!", source, value);
+ } else {
+ debugC(1, kFreescapeDebugCode, "End condition if visibility of obj with id %d in area %d is %d!", additional, source, value);
+ if (_areaMap.contains(source)) {
+ obj = _areaMap[source]->objectWithID(additional);
+ assert(obj);
+ } else {
+ assert(isDOS() && isDemo()); // Should only happen in the DOS demo
+ return (value == false);
+ }
+ }
+
+ return (obj->isInvisible() == (value != 0));
+}
+
+bool FreescapeEngine::checkConditional(const FCLInstruction &instruction, bool shot, bool collided, bool timer, bool activated) {
+ uint16 conditional = instruction._source;
+ bool result = false;
+
+ if (conditional & kConditionalShot)
+ result |= shot;
+ if (conditional & kConditionalTimeout)
+ result |= timer;
+ if (conditional & kConditionalCollided)
+ result |= collided;
+ if (conditional & kConditionalActivated)
+ result |= activated;
+
+ debugC(1, kFreescapeDebugCode, "Check if conditional %x is true: %d!", conditional, result);
+ return result;
+}
+
+bool FreescapeEngine::checkIfGreaterOrEqual(FCLInstruction &instruction) {
+ assert(instruction._destination <= 128);
+
+ uint16 variable = instruction._source;
+ int8 value = instruction._destination;
+ debugC(1, kFreescapeDebugCode, "Check if variable %d with value %d is greater or equal to %d!", variable, (int8)_gameStateVars[variable], value);
+ return ((int8)_gameStateVars[variable] >= value);
+}
+
+bool FreescapeEngine::checkIfLessOrEqual(FCLInstruction &instruction) {
+ assert(instruction._destination <= 128);
+
+ uint16 variable = instruction._source;
+ int8 value = instruction._destination;
+ debugC(1, kFreescapeDebugCode, "Check if variable %d with value %d is less or equal to %d!", variable, (int8)_gameStateVars[variable], value);
+ return ((int8)_gameStateVars[variable] <= value);
+}
+
+
+bool FreescapeEngine::executeEndIfNotEqual(FCLInstruction &instruction) {
+ uint16 variable = instruction._source;
+ uint16 value = instruction._destination;
+ debugC(1, kFreescapeDebugCode, "End condition if variable %d with value %d is not equal to %d!", variable, (int8)_gameStateVars[variable], value);
+ return (_gameStateVars[variable] != value);
+}
+
+void FreescapeEngine::executeIncrementVariable(FCLInstruction &instruction) {
+ int32 variable = instruction._source;
+ int32 increment = instruction._destination;
+ _gameStateVars[variable] = _gameStateVars[variable] + increment;
+ if (variable == k8bitVariableScore) {
+ debugC(1, kFreescapeDebugCode, "Score incremented by %d up to %d", increment, _gameStateVars[variable]);
+ } else if (variable == k8bitVariableEnergy) {
+ if (_gameStateVars[variable] > _maxEnergy)
+ _gameStateVars[variable] = _maxEnergy;
+ else if (_gameStateVars[variable] < 0)
+ _gameStateVars[variable] = 0;
+ debugC(1, kFreescapeDebugCode, "Energy incremented by %d up to %d", increment, _gameStateVars[variable]);
+ } else if (variable == k8bitVariableShield) {
+ if (_gameStateVars[variable] > _maxShield)
+ _gameStateVars[variable] = _maxShield;
+ else if (_gameStateVars[variable] < 0)
+ _gameStateVars[variable] = 0;
+
+ if (increment < 0 && !isCastle())
+ flashScreen(_renderMode == Common::kRenderCGA ? 1 :_currentArea->_underFireBackgroundColor);
+
+ debugC(1, kFreescapeDebugCode, "Shield incremented by %d up to %d", increment, _gameStateVars[variable]);
+ } else {
+ debugC(1, kFreescapeDebugCode, "Variable %d by %d incremented up to %d!", variable, increment, _gameStateVars[variable]);
+ }
+}
+
+void FreescapeEngine::executeDecrementVariable(FCLInstruction &instruction) {
+ uint16 variable = instruction._source;
+ uint16 decrement = instruction._destination;
+ _gameStateVars[variable] = _gameStateVars[variable] - decrement;
+ if (variable == k8bitVariableEnergy) {
+ debugC(1, kFreescapeDebugCode, "Energy decrement by %d up to %d", decrement, _gameStateVars[variable]);
+ } else
+ debugC(1, kFreescapeDebugCode, "Variable %d by %d incremented up to %d!", variable, decrement, _gameStateVars[variable]);
+}
+
+void FreescapeEngine::executeSetVariable(FCLInstruction &instruction) {
+ uint16 variable = instruction._source;
+ uint16 value = instruction._destination;
+ _gameStateVars[variable] = value;
+ if (variable == k8bitVariableEnergy)
+ debugC(1, kFreescapeDebugCode, "Energy set to %d", value);
+ else
+ debugC(1, kFreescapeDebugCode, "Variable %d by set to %d!", variable, value);
+}
+
+void FreescapeEngine::executeDestroy(FCLInstruction &instruction) {
+ uint16 objectID = 0;
+ uint16 areaID = _currentArea->getAreaID();
+
+ if (instruction._destination > 0) {
+ objectID = instruction._destination;
+ areaID = instruction._source;
+ } else {
+ objectID = instruction._source;
+ }
+
+ debugC(1, kFreescapeDebugCode, "Destroying obj %d in area %d!", objectID, areaID);
+ assert(_areaMap.contains(areaID));
+ Object *obj = _areaMap[areaID]->objectWithID(objectID);
+ assert(obj); // We know that an object should be there
+ if (obj->isDestroyed())
+ debugC(1, kFreescapeDebugCode, "WARNING: Destroying obj %d in area %d already destroyed!", objectID, areaID);
+
+ obj->destroy();
+ obj->makeInvisible();
+}
+
+void FreescapeEngine::executeMakeInvisible(FCLInstruction &instruction) {
+ uint16 objectID = 0;
+ uint16 areaID = _currentArea->getAreaID();
+
+ if (instruction._destination > 0) {
+ objectID = instruction._destination;
+ areaID = instruction._source;
+ } else {
+ objectID = instruction._source;
+ }
+
+ debugC(1, kFreescapeDebugCode, "Making obj %d invisible in area %d!", objectID, areaID);
+ if (_areaMap.contains(areaID)) {
+ Object *obj = _areaMap[areaID]->objectWithID(objectID);
+
+ if (!obj) {
+ // Object is not in the area, but it should be invisible so we can return immediately
+ return;
+ /*obj = _areaMap[255]->objectWithID(objectID);
+ if (!obj) {
+ error("obj %d does not exists in area %d nor in the global one!", objectID, areaID);
+ return;
+ }
+ _currentArea->addObjectFromArea(objectID, _areaMap[255]);
+ obj = _areaMap[areaID]->objectWithID(objectID);*/
+ }
+
+ assert(obj); // We assume the object was there
+ obj->makeInvisible();
+ } else {
+ assert(isDriller() && isDOS() && isDemo());
+ }
+
+}
+
+void FreescapeEngine::executeMakeVisible(FCLInstruction &instruction) {
+ uint16 objectID = 0;
+ uint16 areaID = _currentArea->getAreaID();
+
+ if (instruction._destination > 0) {
+ objectID = instruction._destination;
+ areaID = instruction._source;
+ } else {
+ objectID = instruction._source;
+ }
+
+ debugC(1, kFreescapeDebugCode, "Making obj %d visible in area %d!", objectID, areaID);
+ if (_areaMap.contains(areaID)) {
+ Object *obj = _areaMap[areaID]->objectWithID(objectID);
+ if (!obj) {
+ obj = _areaMap[255]->objectWithID(objectID);
+ if (!obj) {
+ if (isCastleMaster2()) {
+ // CM2 Z80 code (Lb286_find_object_by_id) returns silently
+ // when object is not found â the caller skips the rule.
+ debugC(1, kFreescapeDebugCode, "obj %d not found in area %d nor in global area, skipping", objectID, areaID);
+ return;
+ }
+ if (!isCastle() || !isDemo())
+ error("obj %d does not exists in area %d nor in the global one!", objectID, areaID);
+ return;
+ }
+
+ if (obj->getType() != kGroupType)
+ _currentArea->addObjectFromArea(objectID, _areaMap[255]);
+ else if (obj->_partOfGroup)
+ _currentArea->addGroupFromArea(objectID, _areaMap[255]);
+ obj = _areaMap[areaID]->objectWithID(objectID);
+ assert(obj); // We know that an object should be there
+ }
+
+ obj->makeVisible();
+ if (!isDriller()) {
+ Math::AABB boundingBox = createPlayerAABB(_position, _playerHeight);
+ if (obj->_boundingBox.collides(boundingBox)) {
+ _playerWasCrushed = true;
+ _avoidRenderingFrames = 60 * 3;
+ if (isEclipse())
+ playSoundFx(2, true);
+ _shootingFrames = 0;
+ }
+ }
+ } else {
+ assert(isDOS() && isDemo()); // Should only happen in the DOS demo
+ }
+}
+
+void FreescapeEngine::executeToggleVisibility(FCLInstruction &instruction) {
+ uint16 objectID = 0;
+ uint16 areaID = _currentArea->getAreaID();
+
+ if (instruction._destination > 0) {
+ objectID = instruction._destination;
+ areaID = instruction._source;
+ } else {
+ objectID = instruction._source;
+ }
+
+ debugC(1, kFreescapeDebugCode, "Toggling obj %d visibility in area %d!", objectID, areaID);
+ Object *obj = _areaMap[areaID]->objectWithID(objectID);
+ if (obj)
+ obj->toggleVisibility();
+ else {
+ obj = _areaMap[255]->objectWithID(objectID);
+ if (!obj) {
+ // This happens in Driller, the ketar hangar
+ warning("ERROR!: obj %d does not exists in area %d nor in the global one!", objectID, areaID);
+ return;
+ }
+ // If an object is not in the area, it is considered to be invisible
+ _currentArea->addObjectFromArea(objectID, _areaMap[255]);
+ obj = _areaMap[areaID]->objectWithID(objectID);
+ assert(obj); // We know that an object should be there
+ obj->makeVisible();
+ }
+ if (!obj->isInvisible()) {
+ if (!isDriller()) {
+ Math::AABB boundingBox = createPlayerAABB(_position, _playerHeight);
+ if (obj->_boundingBox.collides(boundingBox)) {
+ _playerWasCrushed = true;
+ _avoidRenderingFrames = 60 * 3;
+ _shootingFrames = 0;
+ }
+ }
+ }
+}
+
+void FreescapeEngine::executeGoto(FCLInstruction &instruction) {
+ uint16 areaID = instruction._source;
+ uint16 entranceID = instruction._destination;
+ gotoArea(areaID, entranceID);
+ _gotoExecuted = true;
+}
+
+void FreescapeEngine::executeSetBit(FCLInstruction &instruction) {
+ uint16 index = instruction._source; // Starts at 1
+ assert(index > 0 && index <= 32);
+ setGameBit(index);
+ debugC(1, kFreescapeDebugCode, "Setting bit %d", index);
+}
+
+void FreescapeEngine::executeClearBit(FCLInstruction &instruction) {
+ uint16 index = instruction._source; // Starts at 1
+ assert(index > 0 && index <= 32);
+ clearGameBit(index);
+ debugC(1, kFreescapeDebugCode, "Clearing bit %d", index);
+}
+
+void FreescapeEngine::executeToggleBit(FCLInstruction &instruction) {
+ uint16 index = instruction._source; // Starts at 1
+ assert(index > 0 && index <= 32);
+ toggleGameBit(index);
+ debugC(1, kFreescapeDebugCode, "Toggling bit %d", index);
+}
+
+bool FreescapeEngine::executeEndIfBitNotEqual(FCLInstruction &instruction) {
+ uint16 index = instruction._source;
+ uint16 value = instruction._destination;
+ assert(index <= 32);
+ debugC(1, kFreescapeDebugCode, "End condition if bit %d is not equal to %d!", index, value);
+ return (getGameBit(index) != value);
+}
+
+void FreescapeEngine::executeSwapJet(FCLInstruction &instruction) {
+ //playSound(15, false);
+ _flyMode = !_flyMode;
+ uint16 areaID = _currentArea->getAreaID();
+
+ if (_flyMode) {
+ debugC(1, kFreescapeDebugCode, "Swaping to ship mode");
+ if (areaID == 27 && !(isAmiga() || isAtariST())) {
+ traverseEntrance(26);
+ _lastPosition = _position;
+ }
+ _playerHeight = 2;
+ _playerHeightNumber = -1;
+
+ // Save tank energy and shield
+ _gameStateVars[k8bitVariableEnergyDrillerTank] = _gameStateVars[k8bitVariableEnergy];
+ _gameStateVars[k8bitVariableShieldDrillerTank] = _gameStateVars[k8bitVariableShield];
+
+ // Restore ship energy and shield
+ _gameStateVars[k8bitVariableEnergy] = _gameStateVars[k8bitVariableEnergyDrillerJet];
+ _gameStateVars[k8bitVariableShield] = _gameStateVars[k8bitVariableShieldDrillerJet];
+ } else {
+ debugC(1, kFreescapeDebugCode, "Swaping to tank mode");
+ _playerHeightNumber = 0;
+ if (areaID == 27 && !(isAmiga() || isAtariST())) {
+ traverseEntrance(27);
+ _lastPosition = _position;
+ }
+
+ // Save shield energy and shield
+ _gameStateVars[k8bitVariableEnergyDrillerJet] = _gameStateVars[k8bitVariableEnergy];
+ _gameStateVars[k8bitVariableShieldDrillerJet] = _gameStateVars[k8bitVariableShield];
+
+ // Restore ship energy and shield
+ _gameStateVars[k8bitVariableEnergy] = _gameStateVars[k8bitVariableEnergyDrillerTank];
+ _gameStateVars[k8bitVariableShield] = _gameStateVars[k8bitVariableShieldDrillerTank];
+ }
+ // TODO: implement the rest of the changes (e.g. border)
+}
+
+void FreescapeEngine::executeStartAnim(FCLInstruction &instruction) {
+ uint16 objID = instruction._source;
+ debugC(1, kFreescapeDebugCode, "Staring animation of object %d", objID);
+ Object *obj = _currentArea->objectWithID(objID);
+ assert(obj);
+ Group *group = nullptr;
+ if (obj->getType() == kGroupType) {
+ group = (Group *)obj;
+ } else {
+ assert(obj->_partOfGroup);
+ group = (Group *)obj->_partOfGroup;
+ }
+ debugC(1, kFreescapeDebugCode, "From group %d", group->getObjectID());
+ if (!group->isDestroyed())
+ group->start();
+}
+
+
+} // End of namespace Freescape
diff --git a/engines/freescape/language/instruction.cpp b/engines/freescape/language/instruction.cpp
index eae6d37fcad..c5c7fb228a4 100644
--- a/engines/freescape/language/instruction.cpp
+++ b/engines/freescape/language/instruction.cpp
@@ -22,13 +22,11 @@
// Based on Phantasma code by Thomas Harte (2013),
// available at https://github.com/TomHarte/Phantasma/ (MIT)
-#include "freescape/freescape.h"
-#include "freescape/language/8bitDetokeniser.h"
-#include "freescape/sweepAABB.h"
+#include "freescape/language/instruction.h"
namespace Freescape {
-FCLInstructionVector *duplicateCondition(FCLInstructionVector *condition) {
+FCLInstructionVector *duplicateCondition(const FCLInstructionVector *condition) {
if (!condition)
return nullptr;
@@ -39,7 +37,7 @@ FCLInstructionVector *duplicateCondition(FCLInstructionVector *condition) {
return copy;
}
-FCLInstruction FCLInstruction::duplicate() {
+FCLInstruction FCLInstruction::duplicate() const {
FCLInstruction copy(_type);
copy.setSource(_source, _sourceType);
copy.setDestination(_destination, _destinationType);
@@ -62,15 +60,7 @@ FCLInstruction::FCLInstruction(Token::Type type_) {
_elseInstructions = nullptr;
}
-FCLInstruction::FCLInstruction() {
- _source = 0;
- _destination = 0;
- _additional = 0;
- _sourceType = _destinationType = _additionalType = Token::UNKNOWN;
- _type = Token::UNKNOWN;
- _thenInstructions = nullptr;
- _elseInstructions = nullptr;
-}
+FCLInstruction::FCLInstruction() : FCLInstruction(Token::UNKNOWN) {}
void FCLInstruction::setSource(int32 source_, Token::Type type) {
_source = source_;
@@ -96,834 +86,4 @@ Token::Type FCLInstruction::getType() const {
return _type;
}
-void FreescapeEngine::executeEntranceConditions(Entrance *entrance) {
- if (!entrance->_conditionSource.empty()) {
- _firstSound = true;
- _syncSound = false;
-
- debugC(1, kFreescapeDebugCode, "Executing entrance condition with collision flag: %s", entrance->_conditionSource.c_str());
- executeCode(entrance->_condition, false, true, false, false);
- }
-}
-
-bool FreescapeEngine::executeObjectConditions(GeometricObject *obj, bool shot, bool collided, bool activated) {
- bool executed = false;
- assert(obj != nullptr);
- if (!obj->_conditionSource.empty()) {
- _firstSound = true;
- _syncSound = false;
- _objExecutingCodeSize = collided ? obj->getSize() : Math::Vector3d();
- if (collided) {
- if (!isCastle())
- clearGameBit(31); // We collided with something that has code
- debugC(1, kFreescapeDebugCode, "Executing with collision flag: %s", obj->_conditionSource.c_str());
- } else if (shot)
- debugC(1, kFreescapeDebugCode, "Executing with shot flag: %s", obj->_conditionSource.c_str());
- else if (activated) {
- if (isCastle()) // TODO: add a 3DCK check here
- clearTemporalMessages();
- debugC(1, kFreescapeDebugCode, "Executing with activated flag: %s", obj->_conditionSource.c_str());
- } else
- error("Neither shot or collided flag is set!");
- executed = executeCode(obj->_condition, shot, collided, false, activated); // TODO: check this last parameter
- }
- if (activated && !executed)
- if (!_noEffectMessage.empty())
- insertTemporaryMessage(_noEffectMessage, _countdown - 2);
-
- return executed;
-}
-
-void FreescapeEngine::executeLocalGlobalConditions(bool shot, bool collided, bool timer) {
- debugC(1, kFreescapeDebugCode, "Executing room conditions");
- Common::Array<FCLInstructionVector> conditions = _currentArea->_conditions;
- Common::Array<Common::String> conditionSources = _currentArea->_conditionSources;
-
- for (uint i = 0; i < conditions.size(); i++) {
- debugC(1, kFreescapeDebugCode, "%s", conditionSources[i].c_str());
- executeCode(conditions[i], shot, collided, timer, false);
- }
-
- _executingGlobalCode = true;
- debugC(1, kFreescapeDebugCode, "Executing global conditions (%d)", _conditions.size());
- for (uint i = 0; i < _conditions.size(); i++) {
- debugC(1, kFreescapeDebugCode, "%s", _conditionSources[i].c_str());
- executeCode(_conditions[i], shot, collided, timer, false);
- }
- _executingGlobalCode = false;
-}
-
-bool FreescapeEngine::executeCode(FCLInstructionVector &code, bool shot, bool collided, bool timer, bool activated) {
- int ip = 0;
- bool skip = false;
- int skipDepth = 0;
- int conditionalDepth = 0;
- bool executed = false;
- int loopIterations = 0;
- int loopHead = -1;
- int codeSize = code.size();
-
- if (codeSize == 0) {
- assert(isCastle()); // Only seems to happen in Castle Master (magister room)
- debugC(1, kFreescapeDebugCode, "Code is empty!");
- return false;
- }
-
- while (ip <= codeSize - 1) {
- FCLInstruction &instruction = code[ip];
- debugC(1, kFreescapeDebugCode, "Executing ip: %d with type %d in code with size: %d. Skip flag is: %d", ip, instruction.getType(), codeSize, skip);
-
- if (instruction.isConditional()) {
- conditionalDepth++;
- debugC(1, kFreescapeDebugCode, "Conditional depth increased to: %d", conditionalDepth);
- } else if (instruction.getType() == Token::ENDIF) {
- conditionalDepth--;
- debugC(1, kFreescapeDebugCode, "Conditional depth decreased to: %d", conditionalDepth);
- }
-
- if (skip) {
- if (instruction.getType() == Token::ELSE) {
- debugC(1, kFreescapeDebugCode, "Else found, skip depth: %d, conditional depth: %d", skipDepth, conditionalDepth);
- if (skipDepth == conditionalDepth - 1) {
- skip = false;
- }
- } else if (instruction.getType() == Token::ENDIF) {
- debugC(1, kFreescapeDebugCode, "Endif found, skip depth: %d, conditional depth: %d", skipDepth, conditionalDepth);
- if (skipDepth == conditionalDepth) {
- skip = false;
- }
- }
- debugC(1, kFreescapeDebugCode, "Instruction skipped!");
- ip++;
- continue;
- }
-
- if (instruction.getType() != Token::CONDITIONAL && !instruction.isConditional())
- executed = true;
-
- switch (instruction.getType()) {
- default:
- error("Instruction %x at ip: %d not implemented!", instruction.getType(), ip);
- break;
- case Token::NOP:
- debugC(1, kFreescapeDebugCode, "Executing NOP at ip: %d", ip);
- break;
-
- case Token::LOOP:
- loopHead = ip;
- loopIterations = instruction._source;
- debugC(1, kFreescapeDebugCode, "Starting loop with %d iterations at ip: %d", loopIterations, ip);
- break;
-
- case Token::AGAIN:
- if (loopIterations > 1) {
- loopIterations--;
- ip = loopHead;
- debugC(1, kFreescapeDebugCode, "Looping again, %d iterations left, jumping to ip: %d", loopIterations, ip);
- } else if (loopIterations == 1) {
- loopIterations--;
- debugC(1, kFreescapeDebugCode, "Loop finished");
- } else {
- error("AGAIN found without a matching LOOP!");
- }
- break;
-
- case Token::CONDITIONAL:
- if (checkConditional(instruction, shot, collided, timer, activated))
- executed = executeCode(*instruction._thenInstructions, shot, collided, timer, activated);
- // else branch is always empty
- assert(instruction._elseInstructions == nullptr);
- break;
-
- case Token::VARNOTEQ:
- if (executeEndIfNotEqual(instruction)) {
- if (isCastle()) {
- skip = true;
- skipDepth = conditionalDepth - 1;
- } else
- ip = codeSize;
- }
- break;
- case Token::IFGTEQ:
- skip = !checkIfGreaterOrEqual(instruction);
- if (skip)
- skipDepth = conditionalDepth - 1;
- break;
-
- case Token::IFLTEQ:
- skip = !checkIfLessOrEqual(instruction);
- if (skip)
- skipDepth = conditionalDepth - 1;
- break;
-
-
- case Token::ELSE:
- skip = !skip;
- if (skip)
- skipDepth = conditionalDepth - 1;
- break;
-
- case Token::ENDIF:
- skip = false;
- break;
-
- case Token::SWAPJET:
- executeSwapJet(instruction);
- break;
- case Token::ADDVAR:
- executeIncrementVariable(instruction);
- break;
- case Token::SUBVAR:
- executeDecrementVariable(instruction);
- break;
- case Token::SETVAR:
- executeSetVariable(instruction);
- break;
- case Token::GOTO:
- executeGoto(instruction);
- break;
- case Token::TOGVIS:
- executeToggleVisibility(instruction);
- break;
- case Token::INVIS:
- executeMakeInvisible(instruction);
- break;
- case Token::VIS:
- executeMakeVisible(instruction);
- break;
- case Token::DESTROY:
- executeDestroy(instruction);
- break;
- case Token::REDRAW:
- executeRedraw(instruction);
- break;
- case Token::EXECUTE:
- executeExecute(instruction);
- ip = codeSize;
- break;
- case Token::DELAY:
- executeDelay(instruction);
- break;
- case Token::SOUND:
- executeSound(instruction);
- break;
- case Token::SETBIT:
- executeSetBit(instruction);
- break;
- case Token::CLEARBIT:
- executeClearBit(instruction);
- break;
- case Token::TOGGLEBIT:
- executeToggleBit(instruction);
- break;
- case Token::PRINT:
- executePrint(instruction);
- break;
- case Token::SPFX:
- executeSPFX(instruction);
- break;
- case Token::SCREEN:
- // TODO
- break;
- case Token::SETFLAGS:
- // TODO
- break;
- case Token::STARTANIM:
- executeStartAnim(instruction);
- break;
- case Token::BITNOTEQ:
- if (executeEndIfBitNotEqual(instruction)) {
- if (isCastle()) {
- skip = true;
- skipDepth = conditionalDepth - 1;
- } else
- ip = codeSize;
- }
- break;
- case Token::INVISQ:
- if (executeEndIfVisibilityIsEqual(instruction)) {
- if (isCastle()) {
- skip = true;
- skipDepth = conditionalDepth - 1;
- } else
- ip = codeSize;
- }
- break;
- }
- ip++;
- }
- return executed;
-}
-
-void FreescapeEngine::executeRedraw(FCLInstruction &instruction) {
- debugC(1, kFreescapeDebugCode, "Redrawing screen");
- uint32 delay = (100 / 15) + 1;
- if (isEclipse2() && _currentArea->getAreaID() == _startArea && _gameStateControl == kFreescapeGameStateStart)
- delay = delay * 10;
-
- if (isCastle() && (isSpectrum() || isCPC() || isC64()) && getGameBit(31))
- delay = delay * 15; // Slow down redraws when the final cutscene is playing
-
- if (isDriller() && (isSpectrum() || isCPC() || isC64()) && _gameStateVars[32] == 18)
- delay = delay * 15; // Slow down redraws when the final cutscene is playing
-
- if (isEclipse() && _currentArea->getAreaID() == 37 && getGameBit(6))
- delay = delay * 10; // Slow down redraws in the final area of Eclipse
-
- waitInLoop(delay);
-}
-
-void FreescapeEngine::executeExecute(FCLInstruction &instruction) {
- uint16 objId = instruction._source;
- debugC(1, kFreescapeDebugCode, "Executing instructions from object %d", objId);
- Object *obj = _currentArea->objectWithID(objId);
- if (!obj) {
- obj = _areaMap[255]->objectWithID(objId);
- if (!obj) {
- obj = _areaMap[255]->entranceWithID(objId);
- if (!obj) {
- debugC(1, kFreescapeDebugCode, "WARNING: executing instructions from a non-existent object %d", objId);
- return;
- }
- assert(obj);
- FCLInstructionVector &condition = ((Entrance *)obj)->_condition;
- executeCode(condition, true, true, true, true);
- return;
- }
- }
- executeObjectConditions((GeometricObject *)obj, true, true, true);
-}
-
-void FreescapeEngine::executeSound(FCLInstruction &instruction) {
- uint16 index = instruction._source;
- bool sync = instruction._additional;
- // An undefined sound index is a no-op in the original engines (e.g.
- // start_speaker_sound returns early when the DOS table index is 0xFF) and
- // must NOT disturb the sound that is already playing. Otherwise the
- // stopAllSounds() below cuts the previous sound and then plays nothing
- // (e.g. SOUND 15; SOUND 16 where sound 16 is undefined in the data).
- if (_sound && !_sound->isSoundAvailable(index)) {
- debugC(1, kFreescapeDebugCode, "Sound %d not available, keeping current sound", index);
- return;
- }
- stopAllSounds(Sound::kTypeMovement);
- _firstSound = false;
- debugC(1, kFreescapeDebugCode, "Playing sound %d", index);
- playSound(index, sync);
-}
-
-void FreescapeEngine::executeDelay(FCLInstruction &instruction) {
- uint16 delay = instruction._source;
- debugC(1, kFreescapeDebugCode, "Delaying %d * 1/50 seconds", delay);
- waitInLoop(((20 * delay) / 15) + 1);
-}
-
-void FreescapeEngine::executePrint(FCLInstruction &instruction) {
- uint16 index = instruction._source - 1;
- debugC(1, kFreescapeDebugCode, "Printing message %d: \"%s\"", index, _messagesList[index].c_str());
- _currentAreaMessages.clear();
- _currentAreaMessages.push_back(_messagesList[index]);
-}
-
-uint32 spfxBasePaletteColor(FreescapeEngine *engine, uint8 index) {
- index &= 0x0f;
- uint8 r = engine->_gfx->_palette[3 * index + 0];
- uint8 g = engine->_gfx->_palette[3 * index + 1];
- uint8 b = engine->_gfx->_palette[3 * index + 2];
- return engine->_gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
-}
-
-uint32 spfxDirectPaletteColor(FreescapeEngine *engine, uint16 value) {
- uint8 r = (value >> 8) & 0x0f;
- uint8 g = (value >> 4) & 0x0f;
- uint8 b = value & 0x0f;
-
- if (engine->isAtariST()) {
- r = ((r & 0x07) << 1) | ((r & 0x07) >> 2);
- g = ((g & 0x07) << 1) | ((g & 0x07) >> 2);
- b = ((b & 0x07) << 1) | ((b & 0x07) >> 2);
- }
-
- r = (r << 4) | r;
- g = (g << 4) | g;
- b = (b << 4) | b;
- return engine->_gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
-}
-
-uint32 spfxActivePaletteColor(FreescapeEngine *engine, uint8 index) {
- index &= 0x0f;
- if (engine->_currentArea->_colorRemaps.contains(index))
- return (uint32)engine->_currentArea->_colorRemaps[index];
- return spfxBasePaletteColor(engine, index);
-}
-
-void spfxSetActivePaletteColor(FreescapeEngine *engine, uint8 index, uint32 color) {
- index &= 0x0f;
- if (color == spfxBasePaletteColor(engine, index))
- engine->_currentArea->unremapColor(index);
- else
- engine->_currentArea->remapColor(index, color);
-}
-
-void spfxFillRange(FreescapeEngine *engine, uint8 start, uint8 end, uint32 color) {
- if (end < start)
- return;
-
- for (int i = start; i <= end; i++)
- spfxSetActivePaletteColor(engine, i, color);
-}
-
-void spfxRestoreRange(FreescapeEngine *engine, uint8 start, uint8 end) {
- if (end < start)
- return;
-
- for (int i = start; i <= end; i++)
- engine->_currentArea->unremapColor(i);
-}
-
-void spfxRotateLeft(FreescapeEngine *engine, uint8 start, uint8 end) {
- if (end <= start)
- return;
-
- uint32 color = spfxActivePaletteColor(engine, start);
- for (int i = start; i < end; i++)
- spfxSetActivePaletteColor(engine, i, spfxActivePaletteColor(engine, i + 1));
- spfxSetActivePaletteColor(engine, end, color);
-}
-
-void spfxRotateRight(FreescapeEngine *engine, uint8 start, uint8 end) {
- if (end <= start)
- return;
-
- uint32 color = spfxActivePaletteColor(engine, end);
- for (int i = end; i > start; i--)
- spfxSetActivePaletteColor(engine, i, spfxActivePaletteColor(engine, i - 1));
- spfxSetActivePaletteColor(engine, start, color);
-}
-
-void FreescapeEngine::executeSPFX(FCLInstruction &instruction) {
- uint16 src = instruction._source;
- uint16 dst = instruction._destination;
- if (isAmiga() || isAtariST()) {
- uint16 raw = ((src & 0xff) << 8) | (dst & 0xff);
- if (raw & 0x8000) {
- uint16 color = raw & 0x7770;
- if (isAmiga())
- color >>= 3;
- else
- color >>= 4;
-
- spfxSetActivePaletteColor(this, raw & 0x0f, spfxDirectPaletteColor(this, color));
- } else if ((raw & 0xf000) == 0x1000) {
- spfxFillRange(this, (raw >> 4) & 0x0f, raw & 0x0f, spfxBasePaletteColor(this, (raw >> 8) & 0x0f));
- } else {
- switch (raw & 0x0f00) {
- case 0x0000:
- spfxSetActivePaletteColor(this, raw & 0x0f, spfxBasePaletteColor(this, (raw >> 4) & 0x0f));
- break;
- case 0x0100:
- spfxFillRange(this, 0, 14, spfxBasePaletteColor(this, raw & 0x0f));
- break;
- case 0x0200:
- _currentArea->_colorRemaps.clear();
- break;
- case 0x0300:
- spfxRestoreRange(this, (raw >> 4) & 0x0f, raw & 0x0f);
- break;
- case 0x0400:
- spfxRotateLeft(this, (raw >> 4) & 0x0f, raw & 0x0f);
- break;
- case 0x0500:
- spfxRotateRight(this, (raw >> 4) & 0x0f, raw & 0x0f);
- break;
- default:
- break;
- }
- }
- } else {
- debugC(1, kFreescapeDebugCode, "Switching palette from position %d to %d", src, dst);
- if (src == 0 && dst == 1) {
-
- src = _currentArea->_usualBackgroundColor;
- dst = _currentArea->_underFireBackgroundColor;
-
- if (_renderMode == Common::kRenderCGA)
- dst = 1;
- else if (isC64()) {
- src %= 16;
- dst %= 16;
- }
-
- _currentArea->remapColor(src, dst);
- } else if (src == 0 && dst == 0)
- _currentArea->unremapColor(_currentArea->_usualBackgroundColor);
- else if (src == 15 && dst == 15) // Found in Total Eclipse (DOS)
- _currentArea->unremapColor(_currentArea->_usualBackgroundColor);
- else
- _currentArea->remapColor(src, dst);
- }
- _gfx->setColorRemaps(&_currentArea->_colorRemaps);
- executeRedraw(instruction);
-}
-
-
-bool FreescapeEngine::executeEndIfVisibilityIsEqual(FCLInstruction &instruction) {
- uint16 source = instruction._source;
- uint16 additional = instruction._additional;
- uint16 value = instruction._destination;
-
- Object *obj = nullptr;
- if (additional == 0) {
- obj = _currentArea->objectWithID(source);
- if (!obj && isCastle())
- return (true == (value != 0));
- assert(obj);
- debugC(1, kFreescapeDebugCode, "End condition if visibility of obj with id %d is %d!", source, value);
- } else {
- debugC(1, kFreescapeDebugCode, "End condition if visibility of obj with id %d in area %d is %d!", additional, source, value);
- if (_areaMap.contains(source)) {
- obj = _areaMap[source]->objectWithID(additional);
- assert(obj);
- } else {
- assert(isDOS() && isDemo()); // Should only happen in the DOS demo
- return (value == false);
- }
- }
-
- return (obj->isInvisible() == (value != 0));
-}
-
-bool FreescapeEngine::checkConditional(const FCLInstruction &instruction, bool shot, bool collided, bool timer, bool activated) {
- uint16 conditional = instruction._source;
- bool result = false;
-
- if (conditional & kConditionalShot)
- result |= shot;
- if (conditional & kConditionalTimeout)
- result |= timer;
- if (conditional & kConditionalCollided)
- result |= collided;
- if (conditional & kConditionalActivated)
- result |= activated;
-
- debugC(1, kFreescapeDebugCode, "Check if conditional %x is true: %d!", conditional, result);
- return result;
-}
-
-bool FreescapeEngine::checkIfGreaterOrEqual(FCLInstruction &instruction) {
- assert(instruction._destination <= 128);
-
- uint16 variable = instruction._source;
- int8 value = instruction._destination;
- debugC(1, kFreescapeDebugCode, "Check if variable %d with value %d is greater or equal to %d!", variable, (int8)_gameStateVars[variable], value);
- return ((int8)_gameStateVars[variable] >= value);
-}
-
-bool FreescapeEngine::checkIfLessOrEqual(FCLInstruction &instruction) {
- assert(instruction._destination <= 128);
-
- uint16 variable = instruction._source;
- int8 value = instruction._destination;
- debugC(1, kFreescapeDebugCode, "Check if variable %d with value %d is less or equal to %d!", variable, (int8)_gameStateVars[variable], value);
- return ((int8)_gameStateVars[variable] <= value);
-}
-
-
-bool FreescapeEngine::executeEndIfNotEqual(FCLInstruction &instruction) {
- uint16 variable = instruction._source;
- uint16 value = instruction._destination;
- debugC(1, kFreescapeDebugCode, "End condition if variable %d with value %d is not equal to %d!", variable, (int8)_gameStateVars[variable], value);
- return (_gameStateVars[variable] != value);
-}
-
-void FreescapeEngine::executeIncrementVariable(FCLInstruction &instruction) {
- int32 variable = instruction._source;
- int32 increment = instruction._destination;
- _gameStateVars[variable] = _gameStateVars[variable] + increment;
- if (variable == k8bitVariableScore) {
- debugC(1, kFreescapeDebugCode, "Score incremented by %d up to %d", increment, _gameStateVars[variable]);
- } else if (variable == k8bitVariableEnergy) {
- if (_gameStateVars[variable] > _maxEnergy)
- _gameStateVars[variable] = _maxEnergy;
- else if (_gameStateVars[variable] < 0)
- _gameStateVars[variable] = 0;
- debugC(1, kFreescapeDebugCode, "Energy incremented by %d up to %d", increment, _gameStateVars[variable]);
- } else if (variable == k8bitVariableShield) {
- if (_gameStateVars[variable] > _maxShield)
- _gameStateVars[variable] = _maxShield;
- else if (_gameStateVars[variable] < 0)
- _gameStateVars[variable] = 0;
-
- if (increment < 0 && !isCastle())
- flashScreen(_renderMode == Common::kRenderCGA ? 1 :_currentArea->_underFireBackgroundColor);
-
- debugC(1, kFreescapeDebugCode, "Shield incremented by %d up to %d", increment, _gameStateVars[variable]);
- } else {
- debugC(1, kFreescapeDebugCode, "Variable %d by %d incremented up to %d!", variable, increment, _gameStateVars[variable]);
- }
-}
-
-void FreescapeEngine::executeDecrementVariable(FCLInstruction &instruction) {
- uint16 variable = instruction._source;
- uint16 decrement = instruction._destination;
- _gameStateVars[variable] = _gameStateVars[variable] - decrement;
- if (variable == k8bitVariableEnergy) {
- debugC(1, kFreescapeDebugCode, "Energy decrement by %d up to %d", decrement, _gameStateVars[variable]);
- } else
- debugC(1, kFreescapeDebugCode, "Variable %d by %d incremented up to %d!", variable, decrement, _gameStateVars[variable]);
-}
-
-void FreescapeEngine::executeSetVariable(FCLInstruction &instruction) {
- uint16 variable = instruction._source;
- uint16 value = instruction._destination;
- _gameStateVars[variable] = value;
- if (variable == k8bitVariableEnergy)
- debugC(1, kFreescapeDebugCode, "Energy set to %d", value);
- else
- debugC(1, kFreescapeDebugCode, "Variable %d by set to %d!", variable, value);
-}
-
-void FreescapeEngine::executeDestroy(FCLInstruction &instruction) {
- uint16 objectID = 0;
- uint16 areaID = _currentArea->getAreaID();
-
- if (instruction._destination > 0) {
- objectID = instruction._destination;
- areaID = instruction._source;
- } else {
- objectID = instruction._source;
- }
-
- debugC(1, kFreescapeDebugCode, "Destroying obj %d in area %d!", objectID, areaID);
- assert(_areaMap.contains(areaID));
- Object *obj = _areaMap[areaID]->objectWithID(objectID);
- assert(obj); // We know that an object should be there
- if (obj->isDestroyed())
- debugC(1, kFreescapeDebugCode, "WARNING: Destroying obj %d in area %d already destroyed!", objectID, areaID);
-
- obj->destroy();
- obj->makeInvisible();
-}
-
-void FreescapeEngine::executeMakeInvisible(FCLInstruction &instruction) {
- uint16 objectID = 0;
- uint16 areaID = _currentArea->getAreaID();
-
- if (instruction._destination > 0) {
- objectID = instruction._destination;
- areaID = instruction._source;
- } else {
- objectID = instruction._source;
- }
-
- debugC(1, kFreescapeDebugCode, "Making obj %d invisible in area %d!", objectID, areaID);
- if (_areaMap.contains(areaID)) {
- Object *obj = _areaMap[areaID]->objectWithID(objectID);
-
- if (!obj) {
- // Object is not in the area, but it should be invisible so we can return immediately
- return;
- /*obj = _areaMap[255]->objectWithID(objectID);
- if (!obj) {
- error("obj %d does not exists in area %d nor in the global one!", objectID, areaID);
- return;
- }
- _currentArea->addObjectFromArea(objectID, _areaMap[255]);
- obj = _areaMap[areaID]->objectWithID(objectID);*/
- }
-
- assert(obj); // We assume the object was there
- obj->makeInvisible();
- } else {
- assert(isDriller() && isDOS() && isDemo());
- }
-
-}
-
-void FreescapeEngine::executeMakeVisible(FCLInstruction &instruction) {
- uint16 objectID = 0;
- uint16 areaID = _currentArea->getAreaID();
-
- if (instruction._destination > 0) {
- objectID = instruction._destination;
- areaID = instruction._source;
- } else {
- objectID = instruction._source;
- }
-
- debugC(1, kFreescapeDebugCode, "Making obj %d visible in area %d!", objectID, areaID);
- if (_areaMap.contains(areaID)) {
- Object *obj = _areaMap[areaID]->objectWithID(objectID);
- if (!obj) {
- obj = _areaMap[255]->objectWithID(objectID);
- if (!obj) {
- if (isCastleMaster2()) {
- // CM2 Z80 code (Lb286_find_object_by_id) returns silently
- // when object is not found â the caller skips the rule.
- debugC(1, kFreescapeDebugCode, "obj %d not found in area %d nor in global area, skipping", objectID, areaID);
- return;
- }
- if (!isCastle() || !isDemo())
- error("obj %d does not exists in area %d nor in the global one!", objectID, areaID);
- return;
- }
-
- if (obj->getType() != kGroupType)
- _currentArea->addObjectFromArea(objectID, _areaMap[255]);
- else if (obj->_partOfGroup)
- _currentArea->addGroupFromArea(objectID, _areaMap[255]);
- obj = _areaMap[areaID]->objectWithID(objectID);
- assert(obj); // We know that an object should be there
- }
-
- obj->makeVisible();
- if (!isDriller()) {
- Math::AABB boundingBox = createPlayerAABB(_position, _playerHeight);
- if (obj->_boundingBox.collides(boundingBox)) {
- _playerWasCrushed = true;
- _avoidRenderingFrames = 60 * 3;
- if (isEclipse())
- playSoundFx(2, true);
- _shootingFrames = 0;
- }
- }
- } else {
- assert(isDOS() && isDemo()); // Should only happen in the DOS demo
- }
-}
-
-void FreescapeEngine::executeToggleVisibility(FCLInstruction &instruction) {
- uint16 objectID = 0;
- uint16 areaID = _currentArea->getAreaID();
-
- if (instruction._destination > 0) {
- objectID = instruction._destination;
- areaID = instruction._source;
- } else {
- objectID = instruction._source;
- }
-
- debugC(1, kFreescapeDebugCode, "Toggling obj %d visibility in area %d!", objectID, areaID);
- Object *obj = _areaMap[areaID]->objectWithID(objectID);
- if (obj)
- obj->toggleVisibility();
- else {
- obj = _areaMap[255]->objectWithID(objectID);
- if (!obj) {
- // This happens in Driller, the ketar hangar
- warning("ERROR!: obj %d does not exists in area %d nor in the global one!", objectID, areaID);
- return;
- }
- // If an object is not in the area, it is considered to be invisible
- _currentArea->addObjectFromArea(objectID, _areaMap[255]);
- obj = _areaMap[areaID]->objectWithID(objectID);
- assert(obj); // We know that an object should be there
- obj->makeVisible();
- }
- if (!obj->isInvisible()) {
- if (!isDriller()) {
- Math::AABB boundingBox = createPlayerAABB(_position, _playerHeight);
- if (obj->_boundingBox.collides(boundingBox)) {
- _playerWasCrushed = true;
- _avoidRenderingFrames = 60 * 3;
- _shootingFrames = 0;
- }
- }
- }
-}
-
-void FreescapeEngine::executeGoto(FCLInstruction &instruction) {
- uint16 areaID = instruction._source;
- uint16 entranceID = instruction._destination;
- gotoArea(areaID, entranceID);
- _gotoExecuted = true;
-}
-
-void FreescapeEngine::executeSetBit(FCLInstruction &instruction) {
- uint16 index = instruction._source; // Starts at 1
- assert(index > 0 && index <= 32);
- setGameBit(index);
- debugC(1, kFreescapeDebugCode, "Setting bit %d", index);
-}
-
-void FreescapeEngine::executeClearBit(FCLInstruction &instruction) {
- uint16 index = instruction._source; // Starts at 1
- assert(index > 0 && index <= 32);
- clearGameBit(index);
- debugC(1, kFreescapeDebugCode, "Clearing bit %d", index);
-}
-
-void FreescapeEngine::executeToggleBit(FCLInstruction &instruction) {
- uint16 index = instruction._source; // Starts at 1
- assert(index > 0 && index <= 32);
- toggleGameBit(index);
- debugC(1, kFreescapeDebugCode, "Toggling bit %d", index);
-}
-
-bool FreescapeEngine::executeEndIfBitNotEqual(FCLInstruction &instruction) {
- uint16 index = instruction._source;
- uint16 value = instruction._destination;
- assert(index <= 32);
- debugC(1, kFreescapeDebugCode, "End condition if bit %d is not equal to %d!", index, value);
- return (getGameBit(index) != value);
-}
-
-void FreescapeEngine::executeSwapJet(FCLInstruction &instruction) {
- //playSound(15, false);
- _flyMode = !_flyMode;
- uint16 areaID = _currentArea->getAreaID();
-
- if (_flyMode) {
- debugC(1, kFreescapeDebugCode, "Swaping to ship mode");
- if (areaID == 27 && !(isAmiga() || isAtariST())) {
- traverseEntrance(26);
- _lastPosition = _position;
- }
- _playerHeight = 2;
- _playerHeightNumber = -1;
-
- // Save tank energy and shield
- _gameStateVars[k8bitVariableEnergyDrillerTank] = _gameStateVars[k8bitVariableEnergy];
- _gameStateVars[k8bitVariableShieldDrillerTank] = _gameStateVars[k8bitVariableShield];
-
- // Restore ship energy and shield
- _gameStateVars[k8bitVariableEnergy] = _gameStateVars[k8bitVariableEnergyDrillerJet];
- _gameStateVars[k8bitVariableShield] = _gameStateVars[k8bitVariableShieldDrillerJet];
- } else {
- debugC(1, kFreescapeDebugCode, "Swaping to tank mode");
- _playerHeightNumber = 0;
- if (areaID == 27 && !(isAmiga() || isAtariST())) {
- traverseEntrance(27);
- _lastPosition = _position;
- }
-
- // Save shield energy and shield
- _gameStateVars[k8bitVariableEnergyDrillerJet] = _gameStateVars[k8bitVariableEnergy];
- _gameStateVars[k8bitVariableShieldDrillerJet] = _gameStateVars[k8bitVariableShield];
-
- // Restore ship energy and shield
- _gameStateVars[k8bitVariableEnergy] = _gameStateVars[k8bitVariableEnergyDrillerTank];
- _gameStateVars[k8bitVariableShield] = _gameStateVars[k8bitVariableShieldDrillerTank];
- }
- // TODO: implement the rest of the changes (e.g. border)
-}
-
-void FreescapeEngine::executeStartAnim(FCLInstruction &instruction) {
- uint16 objID = instruction._source;
- debugC(1, kFreescapeDebugCode, "Staring animation of object %d", objID);
- Object *obj = _currentArea->objectWithID(objID);
- assert(obj);
- Group *group = nullptr;
- if (obj->getType() == kGroupType) {
- group = (Group *)obj;
- } else {
- assert(obj->_partOfGroup);
- group = (Group *)obj->_partOfGroup;
- }
- debugC(1, kFreescapeDebugCode, "From group %d", group->getObjectID());
- if (!group->isDestroyed())
- group->start();
-}
-
-
} // End of namespace Freescape
diff --git a/engines/freescape/language/instruction.h b/engines/freescape/language/instruction.h
index 0be0997da41..4c25d2fb392 100644
--- a/engines/freescape/language/instruction.h
+++ b/engines/freescape/language/instruction.h
@@ -44,6 +44,8 @@ enum {
class FCLInstruction;
typedef Common::Array<FCLInstruction> FCLInstructionVector;
+FCLInstructionVector *duplicateCondition(const FCLInstructionVector *condition);
+
class FCLInstruction {
public:
FCLInstruction();
@@ -54,21 +56,16 @@ public:
Token::Type getType() const;
- bool isConditional() const {
- Token::Type type = getType();
- return type == Token::Type::BITNOTEQ || type == Token::Type::VARNOTEQ || \
- type == Token::Type::IFGTEQ || type == Token::Type::IFLTEQ || \
- type == Token::Type::VAREQ || _type == Token::Type::INVISQ;
- }
-
void setBranches(FCLInstructionVector *thenBranch, FCLInstructionVector *elseBranch);
- FCLInstruction duplicate();
+ FCLInstruction duplicate() const;
+ // Source/destination: arithmetic uses (variable, value); GOTO uses (area, entrance).
+ // Object commands use (object) or (area, object).
int32 _source;
int32 _additional;
int32 _destination;
- // UNKNOWN denotes an omitted operand.
+ // Kit decoders mark omitted operands as UNKNOWN.
Token::Type _sourceType;
Token::Type _additionalType;
Token::Type _destinationType;
@@ -78,7 +75,7 @@ public:
FCLInstructionVector *_elseInstructions;
private:
- enum Token::Type _type;
+ Token::Type _type;
};
} // End of namespace Freescape
diff --git a/engines/freescape/language/8bitDetokeniser.h b/engines/freescape/language/variables.h
similarity index 81%
rename from engines/freescape/language/8bitDetokeniser.h
rename to engines/freescape/language/variables.h
index c06d5b73c5b..7c4bdba2c67 100644
--- a/engines/freescape/language/8bitDetokeniser.h
+++ b/engines/freescape/language/variables.h
@@ -19,13 +19,14 @@
*
*/
-#ifndef FREESCAPE_8BITDETOKENIZER_H
-#define FREESCAPE_8BITDETOKENIZER_H
+#ifndef FREESCAPE_LANGUAGE_VARIABLES_H
+#define FREESCAPE_LANGUAGE_VARIABLES_H
-#include "freescape/language/instruction.h"
+#include "common/scummsys.h"
namespace Freescape {
+// Variable and bit assignments used by the classic Freescape games.
enum {
k8bitGameBitTravelRock = 30
};
@@ -44,8 +45,6 @@ enum {
extern uint8 k8bitVariableShield;
-Common::String detokenise8bitCondition(Common::Array<uint16> &tokenisedCondition, FCLInstructionVector &instructions, bool enableActivated);
-
} // End of namespace Freescape
-#endif // FREESCAPE_8BITDETOKENIZER_H
+#endif // FREESCAPE_LANGUAGE_VARIABLES_H
diff --git a/engines/freescape/loaders/8bitBinaryLoader.cpp b/engines/freescape/loaders/8bitBinaryLoader.cpp
index f1f99c47df6..11207328071 100644
--- a/engines/freescape/loaders/8bitBinaryLoader.cpp
+++ b/engines/freescape/loaders/8bitBinaryLoader.cpp
@@ -26,7 +26,8 @@
#include "common/file.h"
#include "freescape/freescape.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/detokeniser.h"
+#include "freescape/language/variables.h"
#include "freescape/objects/connections.h"
#include "freescape/objects/global.h"
#include "freescape/objects/group.h"
@@ -170,7 +171,7 @@ Group *FreescapeEngine::load8bitGroupV1(Common::SeekableReadStream *file, byte r
debugC(1, kFreescapeDebugParser, "Length of condition: %d at %lx", lengthOfCondition, long(file->pos()));
// get the condition
Common::Array<uint16> conditionArray = readArray(file, lengthOfCondition);
- operation->conditionSource = detokenise8bitCondition(conditionArray, operation->condition, isAmiga() || isAtariST());
+ operation->conditionSource = detokeniseFreescapeCondition(conditionArray, operation->condition, isAmiga() || isAtariST());
debugC(1, kFreescapeDebugParser, "%s", operation->conditionSource.c_str());
byteSizeOfObject = byteSizeOfObject - lengthOfCondition;
} else {
@@ -276,7 +277,7 @@ Group *FreescapeEngine::load8bitGroupV2(Common::SeekableReadStream *file, byte r
debugC(1, kFreescapeDebugParser, "Length of condition: %d at %lx", lengthOfCondition, long(file->pos()));
// get the condition
Common::Array<uint16> conditionArray = readArray(file, lengthOfCondition);
- operation->conditionSource = detokenise8bitCondition(conditionArray, operation->condition, isAmiga() || isAtariST());
+ operation->conditionSource = detokeniseFreescapeCondition(conditionArray, operation->condition, isAmiga() || isAtariST());
debugC(1, kFreescapeDebugParser, "%s", operation->conditionSource.c_str());
byteSizeOfObject = byteSizeOfObject - lengthOfCondition;
} else {
@@ -455,7 +456,7 @@ Object *FreescapeEngine::load8bitObject(Common::SeekableReadStream *file) {
Common::String conditionSource;
if (byteSizeOfObject) {
Common::Array<uint16> conditionArray = readArray(file, byteSizeOfObject);
- conditionSource = detokenise8bitCondition(conditionArray, instructions, isAmiga() || isAtariST());
+ conditionSource = detokeniseFreescapeCondition(conditionArray, instructions, isAmiga() || isAtariST());
// instructions = getInstructions(conditionSource);
debugC(1, kFreescapeDebugParser, "%s", conditionSource.c_str());
}
@@ -486,7 +487,7 @@ Object *FreescapeEngine::load8bitObject(Common::SeekableReadStream *file) {
debugC(1, kFreescapeDebugParser, "b: %x", readField(file, 8));
} else {
Common::Array<uint16> conditionArray = readArray(file, byteSizeOfObject);
- conditionSource = detokenise8bitCondition(conditionArray, instructions, isAmiga() || isAtariST());
+ conditionSource = detokeniseFreescapeCondition(conditionArray, instructions, isAmiga() || isAtariST());
debugC(1, kFreescapeDebugParser, "Entrance condition:");
debugC(1, kFreescapeDebugParser, "%s", conditionSource.c_str());
}
@@ -573,7 +574,7 @@ Object *FreescapeEngine::load8bitObject(Common::SeekableReadStream *file) {
// grab the object condition, if there is one
if (byteSizeOfObject) {
Common::Array<uint16> conditionArray = readArray(file, byteSizeOfObject);
- conditionSource = detokenise8bitCondition(conditionArray, instructions, isAmiga() || isAtariST());
+ conditionSource = detokeniseFreescapeCondition(conditionArray, instructions, isAmiga() || isAtariST());
debugC(1, kFreescapeDebugParser, "%s", conditionSource.c_str());
}
debugC(1, kFreescapeDebugParser, "End of object at %lx", long(file->pos()));
@@ -860,7 +861,7 @@ Area *FreescapeEngine::load8bitArea(Common::SeekableReadStream *file, uint16 nco
// get the condition
if (lengthOfCondition > 0) {
Common::Array<uint16> conditionArray = readArray(file, lengthOfCondition);
- Common::String conditionSource = detokenise8bitCondition(conditionArray, instructions, isAmiga() || isAtariST());
+ Common::String conditionSource = detokeniseFreescapeCondition(conditionArray, instructions, isAmiga() || isAtariST());
area->_conditions.push_back(instructions);
area->_conditionSources.push_back(conditionSource);
debugC(1, kFreescapeDebugParser, "%s", conditionSource.c_str());
@@ -976,7 +977,7 @@ void FreescapeEngine::load8bitBinary(Common::SeekableReadStream *file, int offse
// get the condition
if (lengthOfCondition > 0) {
Common::Array<uint16> conditionArray = readArray(file, lengthOfCondition);
- Common::String conditionSource = detokenise8bitCondition(conditionArray, instructions, isAmiga() || isAtariST());
+ Common::String conditionSource = detokeniseFreescapeCondition(conditionArray, instructions, isAmiga() || isAtariST());
_conditions.push_back(instructions);
_conditionSources.push_back(conditionSource);
debugC(1, kFreescapeDebugParser, "%s", conditionSource.c_str());
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index 528f17feda9..9cdd6b53917 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -61,12 +61,14 @@ MODULE_OBJS := \
loaders/8bitImage.o \
loaders/8bitBinaryLoader.o \
loaders/c64.o \
- language/8bitDetokeniser.o \
- language/8bitKitDetokeniser.o \
- language/16bitDetokeniser.o \
+ language/detokeniser.o \
+ language/detokeniser_freescape.o \
+ language/detokeniser_3dck8.o \
+ language/detokeniser_3dck16.o \
language/instruction.o \
- language/instruction8bitKit.o \
- language/instruction16bit.o \
+ language/execution_freescape.o \
+ language/execution_3dck8.o \
+ language/execution_3dck16.o \
metaengine.o \
movement.o \
objects/geometricobject.o \
diff --git a/engines/freescape/objects/entrance.h b/engines/freescape/objects/entrance.h
index 7303d6c34ba..7608969f86c 100644
--- a/engines/freescape/objects/entrance.h
+++ b/engines/freescape/objects/entrance.h
@@ -25,12 +25,11 @@
#ifndef FREESCAPE_ENTRANCE_H
#define FREESCAPE_ENTRANCE_H
+#include "freescape/language/instruction.h"
#include "freescape/objects/object.h"
namespace Freescape {
-extern FCLInstructionVector *duplicateCondition(FCLInstructionVector *condition);
-
class Entrance : public Object {
public:
Entrance(
diff --git a/engines/freescape/objects/geometricobject.cpp b/engines/freescape/objects/geometricobject.cpp
index 16042386de4..7a0e16a7fc0 100644
--- a/engines/freescape/objects/geometricobject.cpp
+++ b/engines/freescape/objects/geometricobject.cpp
@@ -28,8 +28,6 @@
namespace Freescape {
-extern FCLInstructionVector *duplicateCondition(FCLInstructionVector *condition);
-
int GeometricObject::numberOfColoursForObjectOfType(ObjectType type) {
switch (type) {
default:
diff --git a/engines/freescape/objects/group.cpp b/engines/freescape/objects/group.cpp
index 47cfb9db1a2..7383564135c 100644
--- a/engines/freescape/objects/group.cpp
+++ b/engines/freescape/objects/group.cpp
@@ -21,7 +21,7 @@
#include "freescape/freescape.h"
#include "freescape/objects/group.h"
#include "freescape/objects/geometricobject.h"
-#include "freescape/language/8bitDetokeniser.h"
+#include "freescape/language/variables.h"
namespace Freescape {
Commit: 241b138f5e385b5d8f2f25e3ab58564f7e70d5e9
https://github.com/scummvm/scummvm/commit/241b138f5e385b5d8f2f25e3ab58564f7e70d5e9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-07T07:23:53+02:00
Commit Message:
FREESCAPE: refactored freescape script detokenizer to match other similar functions
Changed paths:
engines/freescape/language/detokeniser_freescape.cpp
diff --git a/engines/freescape/language/detokeniser_freescape.cpp b/engines/freescape/language/detokeniser_freescape.cpp
index b41d9f096fb..f894fd1a063 100644
--- a/engines/freescape/language/detokeniser_freescape.cpp
+++ b/engines/freescape/language/detokeniser_freescape.cpp
@@ -30,459 +30,198 @@
namespace Freescape {
-Common::String detokeniseFreescapeCondition(const Common::Array<uint16> &tokenisedCondition, FCLInstructionVector &instructions, bool isAmigaAtari) {
- Common::String detokenisedStream;
- Common::Array<uint8>::size_type bytePointer = 0;
- Common::Array<uint8>::size_type sizeOfTokenisedContent = tokenisedCondition.size();
-
- if (sizeOfTokenisedContent == 0)
- error("No tokenised content");
-
- // on the 8bit platforms, all instructions have a conditional flag;
- // we'll want to convert them into runs of "if shot? then", "if collided? then" or "if timer? then",
- // and we'll want to start that from the top
- FCLInstructionVector *conditionalInstructions = new FCLInstructionVector();
- FCLInstruction currentInstruction = FCLInstruction(Token::UNKNOWN);
-
- // this lookup table tells us how many argument bytes to read per opcode
- uint8 argumentsRequiredByOpcode[49] =
- {0, 3, 1, 1, 1, 1, 2, 2,
- 2, 1, 1, 2, 1, 1, 2, 1,
- 1, 2, 2, 1, 2, 0, 0, 0,
- 1, 1, 0, 1, 1, 1, 1, 1,
- 2, 2, 1, 1, 1, 1, 0, 0,
- 0, 1, 0, 0, 0, 0, 2, 2,
- 1};
-
- if (sizeOfTokenisedContent > 0)
- detokenisedStream += Common::String::format("CONDITION FLAG: %x\n", tokenisedCondition[0]);
- uint16 newConditional = 0;
- uint16 oldConditional = 0;
+static const FCLOpcode kFreescapeOpcodes[] = {
+ {0x00, Token::NOP, "NOP", 0, 0, 0},
+ {0x01, Token::ADDVAR, "ADDVAR", 3, 3, 0},
+ {0x02, Token::ADDVAR, "ADDVAR", 1, 1, 0},
+ {0x03, Token::TOGVIS, "TOGVIS", 1, 1, 0},
+ {0x04, Token::VIS, "VIS", 1, 1, 0},
+ {0x05, Token::INVIS, "INVIS", 1, 1, 0},
+ {0x06, Token::TOGVIS, "TOGVIS", 2, 2, 0},
+ {0x07, Token::VIS, "VIS", 2, 2, 0},
+ {0x08, Token::INVIS, "INVIS", 2, 2, 0},
+ {0x09, Token::ADDVAR, "ADDVAR", 1, 1, 0},
+ {0x0a, Token::SUBVAR, "SUBVAR", 1, 1, 0},
+ {0x0b, Token::VARNOTEQ, "IF VAR!=?", 2, 2, 0},
+ {0x0c, Token::SETBIT, "SETBIT", 1, 1, 0},
+ {0x0d, Token::CLEARBIT, "CLRBIT", 1, 1, 0},
+ {0x0e, Token::BITNOTEQ, "IF BIT!=?", 2, 2, 0},
+ {0x0f, Token::SOUND, "SOUND", 1, 1, 0},
+ {0x10, Token::DESTROY, "DESTROY", 1, 1, 0},
+ {0x11, Token::DESTROY, "DESTROY", 2, 2, 0},
+ {0x12, Token::GOTO, "GOTO", 2, 2, 0},
+ {0x13, Token::ADDVAR, "ADDVAR", 1, 1, 0},
+ {0x14, Token::SETVAR, "SETVAR", 2, 2, 0},
+ {0x15, Token::SWAPJET, "SWAPJET", 0, 1, 0},
+ {0x16, Token::UNKNOWN, "UNKNOWN", 0, 0, 0},
+ {0x17, Token::UNKNOWN, "UNKNOWN", 0, 0, 0},
+ {0x18, Token::UNKNOWN, "UNKNOWN", 1, 1, 0},
+ {0x19, Token::SPFX, "SPFX", 1, 1, 0},
+ {0x1a, Token::REDRAW, "REDRAW", 0, 0, 0},
+ {0x1b, Token::DELAY, "DELAY", 1, 1, 0},
+ {0x1c, Token::SOUND, "SYNCSND", 1, 1, 0},
+ {0x1d, Token::TOGGLEBIT, "TOGGLEBIT", 1, 1, 0},
+ {0x1e, Token::INVISQ, "IF INVIS?", 1, 1, 0},
+ {0x1f, Token::INVISQ, "IF VIS?", 1, 1, 0},
+ {0x20, Token::INVISQ, "IF RINVIS?", 2, 2, 0},
+ {0x21, Token::INVISQ, "IF RVIS?", 2, 2, 0},
+ {0x22, Token::PRINT, "PRINT", 1, 1, 0},
+ {0x23, Token::SCREEN, "SCREEN", 1, 1, 0},
+ {0x24, Token::SETFLAGS, "SETFLAGS", 1, 1, 0},
+ {0x25, Token::STARTANIM, "STARTANIM", 1, 1, 0},
+ {0x26, Token::UNKNOWN, "UNKNOWN", 0, 0, 0},
+ {0x27, Token::UNKNOWN, "UNKNOWN", 0, 0, 0},
+ {0x28, Token::UNKNOWN, "UNKNOWN", 0, 0, 0},
+ {0x29, Token::LOOP, "LOOP", 1, 1, 0},
+ {0x2a, Token::AGAIN, "AGAIN", 0, 0, 0},
+ {0x2b, Token::UNKNOWN, "UNKNOWN", 0, 0, 0},
+ {0x2c, Token::ELSE, "ELSE", 0, 0, 0},
+ {0x2d, Token::ENDIF, "ENDIF", 0, 0, 0},
+ {0x2e, Token::IFGTEQ, "IFGTE", 2, 2, 0},
+ {0x2f, Token::IFLTEQ, "IFLTE", 2, 2, 0},
+ {0x30, Token::EXECUTE, "EXECUTE", 1, 1, 0}
+};
+
+static const struct {
+ byte flag;
+ const char *name;
+} kFreescapeEvents[] = {
+ {kConditionalCollided, "COLLIDED?"},
+ {kConditionalTimeout, "TIMER?"},
+ {kConditionalShot, "SHOT?"},
+ {kConditionalActivated, "ACTIVATED?"}
+};
+
+static FCLInstruction decodeFreescapeInstruction(const FCLOpcode &entry, const uint16 *operands, bool isAmigaAtari) {
+ FCLInstruction instruction(entry.type);
+ if (entry.minArgs > 0)
+ instruction.setSource(operands[0]);
+ if (entry.minArgs > 1)
+ instruction.setDestination(operands[1]);
+
+ switch (entry.type) {
+ case Token::ADDVAR:
+ if (entry.opcode == 0x01) {
+ instruction.setSource(k8bitVariableScore);
+ instruction.setDestination(operands[0] | (operands[1] << 8) | (operands[2] << 16));
+ } else if (entry.opcode == 0x02 || entry.opcode == 0x13) {
+ instruction.setSource(entry.opcode == 0x02 ? k8bitVariableEnergy : k8bitVariableShield);
+ instruction.setDestination(int8(operands[0]));
+ } else {
+ instruction.setDestination(1);
+ }
+ break;
+ case Token::SUBVAR:
+ instruction.setDestination(1);
+ break;
+ case Token::TOGVIS:
+ case Token::VIS:
+ case Token::INVIS:
+ if (entry.minArgs == 1)
+ instruction.setDestination(0);
+ break;
+ case Token::INVISQ:
+ if (entry.minArgs == 2)
+ instruction.setAdditional(operands[1]);
+ instruction.setDestination(entry.opcode == 0x1e || entry.opcode == 0x20);
+ break;
+ case Token::SOUND:
+ instruction.setAdditional(entry.opcode == 0x1c);
+ break;
+ case Token::SPFX:
+ instruction.setSource(operands[0] >> (isAmigaAtari ? 8 : 4));
+ instruction.setDestination(operands[0] & (isAmigaAtari ? 0xff : 0xf));
+ break;
+ default:
+ break;
+ }
+ return instruction;
+}
- while (bytePointer < sizeOfTokenisedContent) {
- // get the conditional type of the next operation
- uint8 conditionalByte = tokenisedCondition[bytePointer] & 0xc0;
- //detokenisedStream += Common::String::format("CONDITION FLAG: %x\n", conditionalByte);
- newConditional = 0;
+static Common::String formatFreescapeInstruction(const FCLOpcode &entry, const FCLInstruction &instruction) {
+ int32 operands[2] = {instruction._source, instruction._destination};
+ Token::Type types[2] = {Token::CONSTANT, Token::CONSTANT};
+ byte count = entry.minArgs;
+ switch (entry.type) {
+ case Token::ADDVAR:
+ case Token::SUBVAR:
+ operands[0] = instruction._destination;
+ operands[1] = instruction._source;
+ count = 2;
+ types[1] = Token::VARIABLE;
+ break;
+ case Token::SETVAR:
+ case Token::VARNOTEQ:
+ case Token::IFGTEQ:
+ case Token::IFLTEQ:
+ types[0] = Token::VARIABLE;
+ break;
+ case Token::INVISQ:
+ operands[1] = instruction._additional;
+ break;
+ case Token::SPFX:
+ count = 2;
+ break;
+ default:
+ break;
+ }
- if (conditionalByte == 0x40)
- newConditional = kConditionalTimeout;
- else if (conditionalByte == 0x80)
- newConditional = kConditionalShot;
- else if (conditionalByte == 0xc0)
- newConditional = kConditionalActivated;
- else
- newConditional = kConditionalCollided;
+ Common::String source = entry.name;
+ if (count)
+ source += " (";
+ for (uint i = 0; i < count; i++) {
+ if (i)
+ source += ", ";
+ source += Common::String::format(types[i] == Token::VARIABLE ? "v%d" : "%d", operands[i]);
+ }
+ if (count)
+ source += ")";
+ if (entry.type == Token::VARNOTEQ || entry.type == Token::BITNOTEQ || entry.type == Token::INVISQ)
+ source += " THEN END ENDIF";
+ return source + '\n';
+}
- // if the conditional type has changed then end the old conditional,
- // if we were in one, and begin a new one
- if (bytePointer == 0 || newConditional != oldConditional) {
- oldConditional = newConditional;
- FCLInstruction branch;
- branch = FCLInstruction(Token::CONDITIONAL);
+Common::String detokeniseFreescapeCondition(const Common::Array<uint16> &tokenisedCondition, FCLInstructionVector &instructions, bool isAmigaAtari) {
+ if (tokenisedCondition.empty())
+ error("No tokenised content");
- if (bytePointer > 0) {
+ Common::String detokenisedStream = Common::String::format("CONDITION FLAG: %x\n", tokenisedCondition[0]);
+ FCLInstructionVector *conditionalInstructions = nullptr;
+ byte conditional = 0;
+ for (uint bytePointer = 0; bytePointer < tokenisedCondition.size();) {
+ uint16 raw = tokenisedCondition[bytePointer++];
+ const auto &event = kFreescapeEvents[(raw >> 6) & 3];
+ // Consecutive instructions with the same event flag share a branch.
+ if (event.flag != conditional) {
+ if (conditionalInstructions) {
detokenisedStream += "ENDIF\n";
- assert(conditionalInstructions->size() > 0);
- // Allocate the next vector of instructions
- conditionalInstructions = new FCLInstructionVector();
+ assert(!conditionalInstructions->empty());
}
-
+ conditional = event.flag;
+ conditionalInstructions = new FCLInstructionVector();
+ FCLInstruction branch(Token::CONDITIONAL);
+ branch.setSource(conditional);
branch.setBranches(conditionalInstructions, nullptr);
- branch.setSource(oldConditional); // conditional flag
instructions.push_back(branch);
-
- detokenisedStream += "IF ";
-
- if (oldConditional & kConditionalShot)
- detokenisedStream += "SHOT? ";
- else if (oldConditional & kConditionalTimeout)
- detokenisedStream += "TIMER? ";
- else if (oldConditional & kConditionalCollided)
- detokenisedStream += "COLLIDED? ";
- else if (oldConditional & kConditionalActivated)
- detokenisedStream += "ACTIVATED? ";
- else
- error("Invalid conditional: %x", oldConditional);
-
- detokenisedStream += "THEN\n";
- }
-
- // get the actual operation
- uint16 opcode = tokenisedCondition[bytePointer] & 0x3f;
- bytePointer++;
-
- // figure out how many argument bytes we're going to need,
- // check we have enough bytes left to read
- if (opcode > 48) {
- debugC(1, kFreescapeDebugParser, "%s", detokenisedStream.c_str());
- error("ERROR: failed to read opcode: %x", opcode);
- break;
+ detokenisedStream += Common::String::format("IF %s THEN\n", event.name);
}
- uint8 numberOfArguments = argumentsRequiredByOpcode[opcode];
- if (bytePointer + numberOfArguments > sizeOfTokenisedContent)
+ byte opcode = raw & 0x3f;
+ const FCLOpcode *entry = findFCLOpcode(kFreescapeOpcodes, opcode);
+ if (entry && entry->minArgs > tokenisedCondition.size() - bytePointer)
break;
-
- // generate the string
- switch (opcode) {
- default:
- detokenisedStream += "<UNKNOWN 8 bit: ";
- detokenisedStream += Common::String::format("%x", (int)opcode);
- detokenisedStream += " > ";
+ if (!entry || entry->type == Token::UNKNOWN) {
debugC(1, kFreescapeDebugParser, "%s", detokenisedStream.c_str());
- error("ERROR: failed to read opcode: %x", opcode);
- break;
-
- case 0:
- detokenisedStream += "NOP ";
- currentInstruction = FCLInstruction(Token::NOP);
- conditionalInstructions->push_back(currentInstruction);
- currentInstruction = FCLInstruction(Token::UNKNOWN);
- break; // NOP
- case 1: // add three-byte value to score
- {
- int32 additionValue =
- tokenisedCondition[bytePointer] |
- (tokenisedCondition[bytePointer + 1] << 8) |
- (tokenisedCondition[bytePointer + 2] << 16);
- detokenisedStream += "ADDVAR";
- detokenisedStream += Common::String::format("(%d, v%d)", additionValue, k8bitVariableScore);
- currentInstruction = FCLInstruction(Token::ADDVAR);
- currentInstruction.setSource(k8bitVariableScore);
- currentInstruction.setDestination(additionValue);
- conditionalInstructions->push_back(currentInstruction);
- currentInstruction = FCLInstruction(Token::UNKNOWN);
- bytePointer += 3;
- numberOfArguments = 0;
- } break;
- case 2: // add one-byte value to energy
- detokenisedStream += "ADDVAR ";
- detokenisedStream += Common::String::format("(%d, v%d)", (int8)tokenisedCondition[bytePointer], k8bitVariableEnergy);
- currentInstruction = FCLInstruction(Token::ADDVAR);
- currentInstruction.setSource(k8bitVariableEnergy);
- currentInstruction.setDestination((int8)tokenisedCondition[bytePointer]);
- conditionalInstructions->push_back(currentInstruction);
- currentInstruction = FCLInstruction(Token::UNKNOWN);
- bytePointer++;
- numberOfArguments = 0;
- break;
- case 19: // add one-byte value to shield
- detokenisedStream += "ADDVAR ";
- detokenisedStream += Common::String::format("(%d, v%d)", (int8)tokenisedCondition[bytePointer], k8bitVariableShield);
- currentInstruction = FCLInstruction(Token::ADDVAR);
- currentInstruction.setSource(k8bitVariableShield);
- currentInstruction.setDestination((int8)tokenisedCondition[bytePointer]);
- conditionalInstructions->push_back(currentInstruction);
- currentInstruction = FCLInstruction(Token::UNKNOWN);
- bytePointer++;
- numberOfArguments = 0;
- break;
-
- case 6:
- case 3:
- detokenisedStream += "TOGVIS (";
- currentInstruction = FCLInstruction(Token::TOGVIS);
- currentInstruction.setSource(0);
- currentInstruction.setDestination(0);
- break; // these all come in unary and binary versions,
- case 7:
- case 4:
- detokenisedStream += "VIS (";
- currentInstruction = FCLInstruction(Token::VIS);
- currentInstruction.setSource(0);
- currentInstruction.setDestination(0);
- break; // hence each getting two case statement entries
- case 8:
- case 5:
- detokenisedStream += "INVIS (";
- currentInstruction = FCLInstruction(Token::INVIS);
- currentInstruction.setSource(0);
- currentInstruction.setDestination(0);
- break;
-
- case 9:
- detokenisedStream += "ADDVAR (1, v";
- detokenisedStream += Common::String::format("%d)", tokenisedCondition[bytePointer]);
- currentInstruction = FCLInstruction(Token::ADDVAR);
- currentInstruction.setSource(tokenisedCondition[bytePointer]);
- currentInstruction.setDestination(1);
- conditionalInstructions->push_back(currentInstruction);
- currentInstruction = FCLInstruction(Token::UNKNOWN);
- bytePointer++;
- numberOfArguments = 0;
- break;
- case 10:
- detokenisedStream += "SUBVAR (1, v";
- detokenisedStream += Common::String::format("%d)", tokenisedCondition[bytePointer]);
- currentInstruction = FCLInstruction(Token::SUBVAR);
- currentInstruction.setSource(tokenisedCondition[bytePointer]);
- currentInstruction.setDestination(1);
- conditionalInstructions->push_back(currentInstruction);
- currentInstruction = FCLInstruction(Token::UNKNOWN);
- bytePointer++;
- numberOfArguments = 0;
- break;
-
- case 11: // end condition if a variable doesn't have a particular value
- detokenisedStream += "IF VAR!=? ";
- detokenisedStream += Common::String::format("(v%d, %d)", (int)tokenisedCondition[bytePointer], (int)tokenisedCondition[bytePointer + 1]);
- detokenisedStream += " THEN END ENDIF";
- currentInstruction = FCLInstruction(Token::VARNOTEQ);
- currentInstruction.setSource(tokenisedCondition[bytePointer]);
- currentInstruction.setDestination(tokenisedCondition[bytePointer + 1]);
- conditionalInstructions->push_back(currentInstruction);
- currentInstruction = FCLInstruction(Token::UNKNOWN);
- bytePointer += 2;
- numberOfArguments = 0;
- break;
- case 14: // end condition if a bit doesn't have a particular value
- detokenisedStream += "IF BIT!=? ";
- detokenisedStream += Common::String::format("(%d, %d)", (int)tokenisedCondition[bytePointer], (int)tokenisedCondition[bytePointer + 1]);
- detokenisedStream += " THEN END ENDIF";
- currentInstruction = FCLInstruction(Token::BITNOTEQ);
- currentInstruction.setSource(tokenisedCondition[bytePointer]);
- currentInstruction.setDestination(tokenisedCondition[bytePointer + 1]);
- conditionalInstructions->push_back(currentInstruction);
- currentInstruction = FCLInstruction(Token::UNKNOWN);
- bytePointer += 2;
- numberOfArguments = 0;
- break;
- case 30: // end condition if an object is invisible
- detokenisedStream += "IF INVIS? ";
- detokenisedStream += Common::String::format("(%d)", (int)tokenisedCondition[bytePointer]);
- detokenisedStream += " THEN END ENDIF";
- currentInstruction = FCLInstruction(Token::INVISQ);
- currentInstruction.setSource(tokenisedCondition[bytePointer]);
- currentInstruction.setDestination(true); // invisible
- conditionalInstructions->push_back(currentInstruction);
- currentInstruction = FCLInstruction(Token::UNKNOWN);
- bytePointer++;
- numberOfArguments = 0;
- break;
- case 31: // end condition if an object is visible
- detokenisedStream += "IF VIS? ";
- detokenisedStream += Common::String::format("(%d)", (int)tokenisedCondition[bytePointer]);
- detokenisedStream += " THEN END ENDIF";
- currentInstruction = FCLInstruction(Token::INVISQ);
- currentInstruction.setSource(tokenisedCondition[bytePointer]);
- currentInstruction.setDestination(false); // visible
- conditionalInstructions->push_back(currentInstruction);
- currentInstruction = FCLInstruction(Token::UNKNOWN);
- bytePointer++;
- numberOfArguments = 0;
- break;
-
- case 32: // end condition if an object is visible in another area
- detokenisedStream += "IF RINVIS? ";
- detokenisedStream += Common::String::format("(%d, %d)", (int)tokenisedCondition[bytePointer], (int)tokenisedCondition[bytePointer + 1]);
- detokenisedStream += " THEN END ENDIF";
- currentInstruction = FCLInstruction(Token::INVISQ);
- currentInstruction.setSource(tokenisedCondition[bytePointer]);
- currentInstruction.setAdditional(tokenisedCondition[bytePointer + 1]);
- currentInstruction.setDestination(true); // invisible
- conditionalInstructions->push_back(currentInstruction);
- currentInstruction = FCLInstruction(Token::UNKNOWN);
- bytePointer += 2;
- numberOfArguments = 0;
- break;
-
- case 33: // end condition if an object is invisible in another area
- detokenisedStream += "IF RVIS? ";
- detokenisedStream += Common::String::format("(%d, %d)", (int)tokenisedCondition[bytePointer], (int)tokenisedCondition[bytePointer + 1]);
- detokenisedStream += " THEN END ENDIF";
- currentInstruction = FCLInstruction(Token::INVISQ);
- currentInstruction.setSource(tokenisedCondition[bytePointer]);
- currentInstruction.setAdditional(tokenisedCondition[bytePointer + 1]);
- currentInstruction.setDestination(false); // visible
- conditionalInstructions->push_back(currentInstruction);
- currentInstruction = FCLInstruction(Token::UNKNOWN);
- bytePointer += 2;
- numberOfArguments = 0;
- break;
-
- case 34: // show a message on screen
- detokenisedStream += "PRINT (";
- currentInstruction = FCLInstruction(Token::PRINT);
- break;
-
- case 35:
- detokenisedStream += "SCREEN (";
- currentInstruction = FCLInstruction(Token::SCREEN);
- break;
-
- case 36: // Only used in Dark Side to keep track of cristals and letters collected
- detokenisedStream += "SETFLAGS (";
- currentInstruction = FCLInstruction(Token::SETFLAGS);
- break;
-
- case 37:
- detokenisedStream += "STARTANIM (";
- currentInstruction = FCLInstruction(Token::STARTANIM);
- break;
-
- case 41: // Not sure about this one
- detokenisedStream += "LOOP (";
- currentInstruction = FCLInstruction(Token::LOOP);
- break;
-
- case 42: // Not sure about this one
- detokenisedStream += "AGAIN";
- currentInstruction = FCLInstruction(Token::AGAIN);
- conditionalInstructions->push_back(currentInstruction);
- currentInstruction = FCLInstruction(Token::UNKNOWN);
- numberOfArguments = 0;
- break;
-
- case 12:
- detokenisedStream += "SETBIT (";
- currentInstruction = FCLInstruction(Token::SETBIT);
- break;
- case 13:
- detokenisedStream += "CLRBIT (";
- currentInstruction = FCLInstruction(Token::CLEARBIT);
- break;
-
- case 15:
- detokenisedStream += "SOUND (";
- currentInstruction = FCLInstruction(Token::SOUND);
- currentInstruction.setAdditional(false);
- break;
- case 17:
- case 16:
- detokenisedStream += "DESTROY (";
- currentInstruction = FCLInstruction(Token::DESTROY);
- break;
- case 18:
- detokenisedStream += "GOTO (";
- currentInstruction = FCLInstruction(Token::GOTO);
- break;
-
- case 21:
- detokenisedStream += "SWAPJET";
- currentInstruction = FCLInstruction(Token::SWAPJET);
- conditionalInstructions->push_back(currentInstruction);
- currentInstruction = FCLInstruction(Token::UNKNOWN);
- // The 16-bit Amiga/Atari token stream stores SWAPJET without a
- // padding argument. The 8-bit data has one unused byte here.
- if (!isAmigaAtari)
- bytePointer++;
- numberOfArguments = 0;
- break;
-
- /*
- case 22:
- case 23:
- case 24:
- UNUSED
- */
-
- case 26:
- detokenisedStream += "REDRAW";
- currentInstruction = FCLInstruction(Token::REDRAW);
- conditionalInstructions->push_back(currentInstruction);
- currentInstruction = FCLInstruction(Token::UNKNOWN);
- break;
- case 27:
- detokenisedStream += "DELAY (";
- currentInstruction = FCLInstruction(Token::DELAY);
- break;
- case 28:
- detokenisedStream += "SYNCSND (";
- currentInstruction = FCLInstruction(Token::SOUND);
- currentInstruction.setAdditional(true);
- break;
- case 29:
- detokenisedStream += "TOGGLEBIT (";
- currentInstruction = FCLInstruction(Token::TOGGLEBIT);
- break;
-
- case 25:
- // this should toggle border colour or the room palette
- detokenisedStream += "SPFX (";
- currentInstruction = FCLInstruction(Token::SPFX);
- if (isAmigaAtari) {
- currentInstruction.setSource(tokenisedCondition[bytePointer] >> 8);
- currentInstruction.setDestination(tokenisedCondition[bytePointer] & 0xff);
- } else {
- currentInstruction.setSource(tokenisedCondition[bytePointer] >> 4);
- currentInstruction.setDestination(tokenisedCondition[bytePointer] & 0xf);
- }
- detokenisedStream += Common::String::format("%d, %d)", currentInstruction._source, currentInstruction._destination);
- conditionalInstructions->push_back(currentInstruction);
- currentInstruction = FCLInstruction(Token::UNKNOWN);
- bytePointer++;
- numberOfArguments = 0;
- break;
-
- case 20:
- detokenisedStream += "SETVAR (v";
- currentInstruction = FCLInstruction(Token::SETVAR);
- break;
-
- case 44:
- detokenisedStream += "ELSE ";
- currentInstruction = FCLInstruction(Token::ELSE);
- conditionalInstructions->push_back(currentInstruction);
- currentInstruction = FCLInstruction(Token::UNKNOWN);
- numberOfArguments = 0;
- break;
-
- case 45:
- detokenisedStream += "ENDIF ";
- currentInstruction = FCLInstruction(Token::ENDIF);
- conditionalInstructions->push_back(currentInstruction);
- currentInstruction = FCLInstruction(Token::UNKNOWN);
- numberOfArguments = 0;
- break;
-
- case 46:
- detokenisedStream += "IFGTE (v";
- currentInstruction = FCLInstruction(Token::IFGTEQ);
- break;
-
- case 47:
- detokenisedStream += "IFLTE (v";
- currentInstruction = FCLInstruction(Token::IFLTEQ);
- break;
-
- case 48:
- detokenisedStream += "EXECUTE (";
- currentInstruction = FCLInstruction(Token::EXECUTE);
- break;
+ error("Unknown Freescape opcode %02x at %u", opcode, bytePointer - 1);
}
- // if there are any regular arguments to add, do so
- if (numberOfArguments) {
- for (uint8 argumentNumber = 0; argumentNumber < numberOfArguments; argumentNumber++) {
- if (argumentNumber == 0)
- currentInstruction.setSource(tokenisedCondition[bytePointer]);
- else if (argumentNumber == 1)
- currentInstruction.setDestination(tokenisedCondition[bytePointer]);
- else
- error("Unexpected number of arguments!");
-
- detokenisedStream += Common::String::format("%d", (int)tokenisedCondition[bytePointer]);
- bytePointer++;
-
- if (argumentNumber < numberOfArguments - 1)
- detokenisedStream += ", ";
- }
-
- detokenisedStream += ")";
- assert(currentInstruction.getType() != Token::UNKNOWN);
- conditionalInstructions->push_back(currentInstruction);
- currentInstruction = FCLInstruction(Token::UNKNOWN);
- }
-
- // throw in a newline
- detokenisedStream += "\n";
+ FCLInstruction instruction = decodeFreescapeInstruction(*entry, tokenisedCondition.data() + bytePointer, isAmigaAtari);
+ conditionalInstructions->push_back(instruction);
+ detokenisedStream += formatFreescapeInstruction(*entry, instruction);
+ // SWAPJET has unused padding outside the Amiga/Atari stream.
+ bytePointer += isAmigaAtari ? entry->minArgs : entry->maxArgs;
}
-
- // This fails in Castle Master
- //assert(conditionalInstructions->size() > 0);
-
return detokenisedStream;
}
-} // End of namespace Freescape
+} // namespace Freescape
More information about the Scummvm-git-logs
mailing list