[Scummvm-git-logs] scummvm master -> d88ede37a6cb6e350a86fe4670153b76143fd246

neuromancer noreply at scummvm.org
Tue Sep 8 11:35:04 UTC 2026


This automated email contains information about 13 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .

Summary:
6732084fdf FREESCAPE: avoid invisible faces in 3dck
107a1a5211 FREESCAPE: correctly process inputs events in 3dck
774e43c325 FREESCAPE: added some 3dck games/demos for detection
c5c0eba8e2 FREESCAPE: adjusted delay timing in 3dck
3ac67c9b30 FREESCAPE: initial implementation of sound for 3dck games
fa58919982 FREESCAPE: fixes to run Desert Maze (3dck game)
d36c070008 FREESCAPE: corrected palette in CPC 3dck games
21f0ea581d FREESCAPE: input/movement fixes for 3dck games
3dca7164ad FREESCAPE: implementation of 8bit sounds for 3dck games
3cb304bda7 FREESCAPE: initial support for zx spectrum 3dck games
dc4b8e3436 FREESCAPE: complete implementation of Colour opcode for zx spectrum 3dck games
28c76906af FREESCAPE: initial code to support c64 3dck games
d88ede37a6 COMMON: moved unp64 to common/compression


Commit: 6732084fdffafa4fc3a62e382d088d100c5ca91f
    https://github.com/scummvm/scummvm/commit/6732084fdffafa4fc3a62e382d088d100c5ca91f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-08T13:33:08+02:00

Commit Message:
FREESCAPE: avoid invisible faces in 3dck

Changed paths:
    engines/freescape/games/3dck/3dck.cpp


diff --git a/engines/freescape/games/3dck/3dck.cpp b/engines/freescape/games/3dck/3dck.cpp
index 24c178415e2..e736a3e0caa 100644
--- a/engines/freescape/games/3dck/3dck.cpp
+++ b/engines/freescape/games/3dck/3dck.cpp
@@ -332,6 +332,12 @@ Object *KitEngine::loadObject(Common::SeekableReadStream &file, ObjectData &data
 			colors->push_back(first);
 			colors->push_back(second);
 		}
+		if (GeometricObject::isPyramid(type)) {
+			// Kit stores opposite sides together; the renderer walks around the base.
+			const byte sides[] = {(*colors)[2], (*colors)[0], (*colors)[3], (*colors)[1]};
+			for (uint i = 0; i < ARRAYSIZE(sides); i++)
+				(*colors)[i] = sides[i];
+		}
 		int ordinateCount = GeometricObject::numberOfOrdinatesForType(type);
 		if (ordinateCount) {
 			requireBytes(payload, 2 * ordinateCount);


Commit: 107a1a521197a26ef2c0621b20c91e1340f6b7a4
    https://github.com/scummvm/scummvm/commit/107a1a521197a26ef2c0621b20c91e1340f6b7a4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-08T13:33:08+02:00

Commit Message:
FREESCAPE: correctly process inputs events in 3dck

Changed paths:
    engines/freescape/games/3dck/3dck.cpp
    engines/freescape/games/3dck/3dck.h
    engines/freescape/games/3dck/ui.cpp
    engines/freescape/language/execution_3dck16.cpp


diff --git a/engines/freescape/games/3dck/3dck.cpp b/engines/freescape/games/3dck/3dck.cpp
index e736a3e0caa..96e601579ee 100644
--- a/engines/freescape/games/3dck/3dck.cpp
+++ b/engines/freescape/games/3dck/3dck.cpp
@@ -416,6 +416,7 @@ void KitEngine::gotoArea(uint16 areaID, int entranceID) {
 	_gfx->_scale = _currentArea->getScale();
 	_gotoExecuted = true;
 	_delayedShootObject = nullptr;
+	_pendingInteractions = 0;
 	_timerTicks = 0;
 	_scriptSurface.fillRect(_viewArea, 0);
 	resetInput();
@@ -429,6 +430,47 @@ void KitEngine::checkIfStillInArea() {
 	_position.z() = CLIP(_position.z(), 0.0f, limit);
 }
 
+void KitEngine::updatePlayerMovement(float deltaTime) {
+	if (_scriptFrameActive || _initialScriptPending)
+		return;
+	updateInteractions();
+	if (!_eventManager->isActionActive(kActionMoveUp))
+		_moveForward = false;
+	if (!_eventManager->isActionActive(kActionMoveDown))
+		_moveBackward = false;
+	if (!_eventManager->isActionActive(kActionMoveLeft))
+		_strafeLeft = false;
+	if (!_eventManager->isActionActive(kActionMoveRight))
+		_strafeRight = false;
+
+	Math::Vector3d front = _flyMode ? _cameraFront : directionToVector(0, _yaw, false);
+	Math::Vector3d movement;
+	if (_moveForward)
+		movement += front;
+	if (_moveBackward)
+		movement -= front;
+	if (_strafeLeft)
+		movement += _cameraRight;
+	if (_strafeRight)
+		movement -= _cameraRight;
+	if (movement.length() == 0)
+		return;
+	movement.normalize();
+
+	// The runner advances one full step per completed script frame.
+	float height = _position.y();
+	resolveCollisions(_position + movement * _playerSteps[_playerStepIndex]);
+	checkIfStillInArea();
+	_lastPosition = _position;
+	_gotoExecuted = false;
+	clearGameBit(31);
+	if (_hasFallen) {
+		_kitVariables[10] += MAX<int>(0, height - _position.y() - _maxFallingDistance);
+		_hasFallen = false;
+		_avoidRenderingFrames = 0;
+	}
+}
+
 bool KitEngine::checkIfGameEnded() {
 	return false;
 }
diff --git a/engines/freescape/games/3dck/3dck.h b/engines/freescape/games/3dck/3dck.h
index e09933f47be..ee5b034028a 100644
--- a/engines/freescape/games/3dck/3dck.h
+++ b/engines/freescape/games/3dck/3dck.h
@@ -141,6 +141,7 @@ private:
 	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 updateInteractions();
 	void interact(bool shot);
 	void printMessage(uint16 indicator, const Common::String &message);
 	void updateIndicators();
@@ -161,6 +162,7 @@ private:
 	bool _timerTriggered = false, _initialScriptPending = false;
 	bool _scriptFrameActive = false, _scriptDelayed = false;
 	bool _soundWarning = false;
+	byte _pendingInteractions = 0, _shootCooldown = 0, _activateCooldown = 0;
 	uint _scriptQueueIndex = 0;
 	Common::Array<ScriptEntry> _scriptQueue;
 	Common::Array<ScriptState *> _suspendedScripts;
diff --git a/engines/freescape/games/3dck/ui.cpp b/engines/freescape/games/3dck/ui.cpp
index ce116adce4f..c8ac0a951ed 100644
--- a/engines/freescape/games/3dck/ui.cpp
+++ b/engines/freescape/games/3dck/ui.cpp
@@ -147,8 +147,8 @@ bool KitEngine::handleInput(const Common::Event &event) {
 				_kitVariables[15] = key;
 		}
 		if (event.customType == kActionShoot || event.customType == kActionActivate) {
-			if (!_scriptFrameActive)
-				interact(event.customType == kActionShoot);
+			if (!event.kbdRepeat)
+				_pendingInteractions |= event.customType == kActionShoot ? 1 : 2;
 			return true;
 		}
 		// Track held movement keys during DELAY; movement itself waits.
@@ -169,16 +169,51 @@ bool KitEngine::handleInput(const Common::Event &event) {
 		_kitVariables[16] |= event.type == Common::EVENT_LBUTTONDOWN ? 1 : 2;
 		_kitVariables[17] = mouse.x;
 		_kitVariables[18] = mouse.y;
-		if (!_scriptFrameActive)
-			interact(event.type == Common::EVENT_LBUTTONDOWN);
+		if (_viewArea.contains(_crossairPosition))
+			_pendingInteractions |= event.type == Common::EVENT_LBUTTONDOWN ? 1 : 2;
+		return true;
+	} else if (event.type == Common::EVENT_MOUSEMOVE) {
+		if (_hasFallen || _playerWasCrushed)
+			return true;
+		if (_shootMode)
+			_crossairPosition = getNormalizedPosition(event.mouse);
+		else {
+			// Relative mouse input keeps queued button events intact.
+			int y = _invertY ? -event.relMouse.y : event.relMouse.y;
+			rotate(event.relMouse.x * _mouseSensitivity, y * _mouseSensitivity, 0);
+		}
 		return true;
 	}
 	return false;
 }
 
+void KitEngine::updateInteractions() {
+	int buttons = g_system->getEventManager()->getButtonState();
+	bool shot = (buttons & Common::EventManager::LBUTTON) || _eventManager->isActionActive(kActionShoot);
+	bool activated = (buttons & Common::EventManager::RBUTTON) || _eventManager->isActionActive(kActionActivate);
+	if (_shootCooldown)
+		_shootCooldown--;
+	if (_activateCooldown)
+		_activateCooldown--;
+
+	// The DOS runner uses bit 3 to require a button release between shots.
+	if (!_shootCooldown && ((_pendingInteractions & 1) || (shot && !(_kitVariables[20] & 8)))) {
+		_pendingInteractions &= ~1;
+		interact(true);
+	}
+	if (!_activateCooldown && ((_pendingInteractions & 2) || activated)) {
+		_pendingInteractions &= ~2;
+		interact(false);
+	}
+}
+
 void KitEngine::interact(bool shot) {
 	if (!_viewArea.contains(_crossairPosition) || (shot && !(_kitVariables[20] & 1)))
 		return;
+	if (shot)
+		_shootCooldown = 2;
+	else
+		_activateCooldown = 2;
 	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);
diff --git a/engines/freescape/language/execution_3dck16.cpp b/engines/freescape/language/execution_3dck16.cpp
index 271aa4ad2c6..7233405abf0 100644
--- a/engines/freescape/language/execution_3dck16.cpp
+++ b/engines/freescape/language/execution_3dck16.cpp
@@ -35,6 +35,7 @@ void KitEngine::resetScripts() {
 	_lastScriptTick = _ticks;
 	_scriptFrameActive = _scriptDelayed = false;
 	_initialScriptPending = _initialCondition != 0;
+	_pendingInteractions = _shootCooldown = _activateCooldown = 0;
 	_scriptQueue.clear();
 	_suspendedScripts.clear();
 	_scriptSurface.fillRect(_fullscreenViewArea, 0);
@@ -801,16 +802,4 @@ bool KitEngine::moveAnimation(ScriptState &script, Math::Vector3d movement, bool
 	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


Commit: 774e43c325604af321c76e7a332827b9cb11f2a1
    https://github.com/scummvm/scummvm/commit/774e43c325604af321c76e7a332827b9cb11f2a1
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-08T13:33:08+02:00

Commit Message:
FREESCAPE: added some 3dck games/demos for detection

Changed paths:
    engines/freescape/detection.cpp
    engines/freescape/games/3dck/3dck.cpp
    engines/freescape/language/detokeniser_3dck16.cpp


diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index cbd4f199658..ad80610c043 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -1228,6 +1228,24 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSUPPORTED,
 		GUIO1(GUIO_NOMIDI)
 	},
+	{
+		"3dkit",
+		"Christmas Tree Demo",
+		AD_ENTRY1s("CHRISTMA.RUN", "106b8f0dd0384d3138a8f0f62caef392", 69910),
+		Common::EN_ANY,
+		Common::kPlatformDOS,
+		ADGF_UNSTABLE | ADGF_DEMO,
+		GUIO2(GUIO_NOMIDI, GUIO_RENDERVGA)
+	},
+	{
+		"3dkit",
+		"Desert Maze",
+		AD_ENTRY1s("DESMAZE.RUN", "5cfab15e53d77029bdb02c87acae3186", 99212),
+		Common::EN_ANY,
+		Common::kPlatformDOS,
+		ADGF_UNSTABLE,
+		GUIO2(GUIO_NOMIDI, GUIO_RENDERVGA)
+	},
 	{
 		"3dkit",
 		"Desert Sands v1.3",
@@ -1246,6 +1264,15 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSUPPORTED,
 		GUIO1(GUIO_NOMIDI)
 	},
+	{
+		"3dkit",
+		"Easy? or Not?",
+		AD_ENTRY1s("EASY.RUN", "28e4c38ff4d06443433a02f857069fa4", 93108),
+		Common::EN_ANY,
+		Common::kPlatformDOS,
+		ADGF_UNSTABLE,
+		GUIO2(GUIO_NOMIDI, GUIO_RENDERVGA)
+	},
 	{
 		"3dkit",
 		"Eerie Estates",
@@ -1255,6 +1282,15 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSUPPORTED,
 		GUIO1(GUIO_NOMIDI)
 	},
+	{
+		"3dkit",
+		"Funnyrace",
+		AD_ENTRY1s("FUNRACE.RUN", "a8643c6eb251802a9e27612d417592be", 99098),
+		Common::EN_ANY,
+		Common::kPlatformDOS,
+		ADGF_UNSTABLE,
+		GUIO2(GUIO_NOMIDI, GUIO_RENDERVGA)
+	},
 	{
 		"3dkit",
 		"Gaynor Ave. House",
@@ -1300,6 +1336,15 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSUPPORTED,
 		GUIO1(GUIO_NOMIDI)
 	},
+	{
+		"3dkit",
+		"Mountain Adventure",
+		AD_ENTRY1s("MOUNTAIN.RUN", "ec3bb57fe23b1a6785e870af1baa74d7", 129106),
+		Common::EN_ANY,
+		Common::kPlatformDOS,
+		ADGF_UNSTABLE,
+		GUIO2(GUIO_NOMIDI, GUIO_RENDERVGA)
+	},
 	{
 		"3dkit",
 		"Rubber Room v1.0",
@@ -1318,6 +1363,15 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSUPPORTED,
 		GUIO1(GUIO_NOMIDI)
 	},
+	{
+		"3dkit",
+		"Tunnel Adventure",
+		AD_ENTRY1s("TUNNEL.RUN", "414738d92decdfee028fa0d04679c23d", 98918),
+		Common::EN_ANY,
+		Common::kPlatformDOS,
+		ADGF_UNSTABLE,
+		GUIO2(GUIO_NOMIDI, GUIO_RENDERVGA)
+	},
 	{
 		"3dkit",
 		"Virtual Reality Studio Demo",
diff --git a/engines/freescape/games/3dck/3dck.cpp b/engines/freescape/games/3dck/3dck.cpp
index 96e601579ee..c7f8ccc1708 100644
--- a/engines/freescape/games/3dck/3dck.cpp
+++ b/engines/freescape/games/3dck/3dck.cpp
@@ -30,6 +30,7 @@ namespace Freescape {
 
 enum {
 	kKitAnimatorType = 16,
+	kKitDisabledType = 0x7f,
 	kKitInitiallyInvisible = 0x04,
 	kKitMovable = 0x80
 };
@@ -178,7 +179,8 @@ void KitEngine::loadWorld(Common::SeekableReadStream &file) {
 	_angleRotationIndex = 0;
 
 	file.skip(2);
-	uint32 indicatorOffset = 2 * file.readUint16BE();
+	// DOS word offsets wrap at 64 KiB.
+	uint32 indicatorOffset = uint16(2 * file.readUint16BE());
 	uint16 indicatorCount = file.readUint16BE();
 	_initialCondition = file.readUint16BE();
 	if (indicatorOffset > uint32(file.size()) || (!indicatorOffset && indicatorCount))
@@ -195,10 +197,14 @@ void KitEngine::loadWorld(Common::SeekableReadStream &file) {
 	requireBytes(file, uint32(areaCount) * 4);
 	Common::Array<uint32> areaOffsets;
 	for (uint i = 0; i < areaCount; i++) {
-		uint32 offset = file.readUint32BE();
-		if (offset >= areasEnd / 2)
+		uint32 offset = uint16(2 * file.readUint32BE());
+		if (indicatorCount && offset >= indicatorOffset && offset < indicatorOffset + 2 * _indicatorData.size()) {
+			warning("Ignoring stale 3D Construction Kit area offset %u into indicator data", offset);
+			continue;
+		}
+		if (offset >= areasEnd)
 			error("Invalid 3D Construction Kit area offset");
-		areaOffsets.push_back(2 * offset);
+		areaOffsets.push_back(offset);
 	}
 	Common::sort(areaOffsets.begin(), areaOffsets.end());
 	if (areaOffsets.empty() || globalConditions < uint32(file.pos()) ||
@@ -266,6 +272,8 @@ Area *KitEngine::loadArea(Common::SeekableReadStream &file) {
 	for (uint i = 0; i < objectCount; i++) {
 		ObjectData record;
 		Object *obj = loadObject(objectData, record);
+		if (record.type == kKitDisabledType)
+			continue;
 		if (data.objects.contains(record.id))
 			error("Duplicate 3D Construction Kit object %u in area %u", record.id, id);
 		data.objects[record.id] = record;
@@ -280,12 +288,9 @@ Area *KitEngine::loadArea(Common::SeekableReadStream &file) {
 			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");
+	// Unused records can remain between the counted objects and conditions.
 	file.seek(conditions);
 	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);
@@ -315,6 +320,10 @@ Object *KitEngine::loadObject(Common::SeekableReadStream &file, ObjectData &data
 		error("Invalid 3D Construction Kit object size");
 	requireBytes(file, 2 * (words - 10));
 	uint32 end = file.pos() + 2 * (words - 10);
+	if (data.type == kKitDisabledType) {
+		file.seek(end);
+		return nullptr;
+	}
 	Common::SeekableSubReadStream payload(&file, file.pos(), end);
 	if (data.type > kKitAnimatorType)
 		error("Unsupported 3D Construction Kit object %u (type %u)", data.id, data.type);
diff --git a/engines/freescape/language/detokeniser_3dck16.cpp b/engines/freescape/language/detokeniser_3dck16.cpp
index 964f90b8fdf..dfae0771a4b 100644
--- a/engines/freescape/language/detokeniser_3dck16.cpp
+++ b/engines/freescape/language/detokeniser_3dck16.cpp
@@ -138,8 +138,14 @@ Common::String detokeniseKit16Condition(const Common::Array<byte> &tokenisedCond
 			loops++;
 			break;
 		case Token::AGAIN:
-			if (--loops < 0)
+			if (loops) {
+				loops--;
+			} else if (!READ_BE_UINT16(&tokenisedCondition[bytePointer + 2])) {
+				// An orphaned AGAIN with no jump offset has no effect in RUNVGA.
+				instruction = FCLInstruction(Token::NOP);
+			} else {
 				error("16-bit FCL AGAIN without LOOP");
+			}
 			break;
 		default:
 			break;


Commit: c5c0eba8e278fad0b6a7fce500a7321647397140
    https://github.com/scummvm/scummvm/commit/c5c0eba8e278fad0b6a7fce500a7321647397140
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-08T13:33:08+02:00

Commit Message:
FREESCAPE: adjusted delay timing in 3dck

Changed paths:
    engines/freescape/games/3dck/3dck.cpp
    engines/freescape/language/execution.h
    engines/freescape/language/execution_3dck16.cpp
    engines/freescape/language/execution_3dck16.h
    engines/freescape/language/execution_freescape.cpp


diff --git a/engines/freescape/games/3dck/3dck.cpp b/engines/freescape/games/3dck/3dck.cpp
index c7f8ccc1708..06c1d36d74a 100644
--- a/engines/freescape/games/3dck/3dck.cpp
+++ b/engines/freescape/games/3dck/3dck.cpp
@@ -127,6 +127,8 @@ void KitEngine::loadAssets() {
 		_palette[i] = (component << 2) | (component >> 4);
 	}
 	_border->setPalette(_palette, 0, 256);
+	// Preserve the full VGA palette during shared border processing.
+	_border->convertToInPlace(_gfx->_texturePixelFormat);
 	_gfx->_palette = _palette;
 	_gfx->_keyColor = 0;
 	_scriptSurface.create(_screenW, _screenH, _gfx->_texturePixelFormat);
diff --git a/engines/freescape/language/execution.h b/engines/freescape/language/execution.h
index fc44a51c1ae..1e853e16c22 100644
--- a/engines/freescape/language/execution.h
+++ b/engines/freescape/language/execution.h
@@ -26,6 +26,9 @@
 
 namespace Freescape {
 
+// Default redraw cadence in 50 Hz ticks.
+static const uint32 kFCLRedrawTicks = 8;
+
 enum FCLExecutionResult {
 	kFCLFinished,
 	kFCLYielded, // Resume this script after the other scripts in the frame.
diff --git a/engines/freescape/language/execution_3dck16.cpp b/engines/freescape/language/execution_3dck16.cpp
index 7233405abf0..01725a257da 100644
--- a/engines/freescape/language/execution_3dck16.cpp
+++ b/engines/freescape/language/execution_3dck16.cpp
@@ -75,6 +75,7 @@ void KitEngine::resetScripts() {
 void KitEngine::startScript(ScriptState &script) {
 	script.code = script.source;
 	script.ip = script.restart = 0;
+	script.resumeTick = 0;
 	script.loops.clear();
 	script.predicate = FCLPredicateState(true);
 	script.events = script.object ? script.object->flags & 0x38 : 0;
@@ -205,6 +206,10 @@ void KitEngine::updateScripts() {
 				_scriptQueueIndex++;
 				continue;
 			}
+			if (animator && script.running && int32(_scriptTicks - script.resumeTick) < 0) {
+				_scriptQueueIndex++;
+				continue;
+			}
 			if (!script.running)
 				startScript(script);
 			entry.resume = true;
@@ -218,7 +223,9 @@ void KitEngine::updateScripts() {
 			script.running = false;
 			if (animator)
 				object->flags |= 2;
-		} else if (!animator) {
+		} else if (animator) {
+			script.resumeTick = _scriptTicks + kFCLRedrawTicks;
+		} else {
 			_suspendedScripts.push_back(&script);
 		}
 		_scriptQueueIndex++;
diff --git a/engines/freescape/language/execution_3dck16.h b/engines/freescape/language/execution_3dck16.h
index 0e9c92a9367..5e62e6c0b4e 100644
--- a/engines/freescape/language/execution_3dck16.h
+++ b/engines/freescape/language/execution_3dck16.h
@@ -36,6 +36,7 @@ struct FCLKit16Loop {
 struct FCLKit16ExecutionState : FCLExecutionFrame {
 	const FCLInstructionVector *source = nullptr;
 	uint32 restart = 0;
+	uint32 resumeTick = 0;
 	Common::HashMap<uint32, FCLKit16Loop> loops;
 	bool running = false;
 	FCLPredicateState predicate = FCLPredicateState(true);
diff --git a/engines/freescape/language/execution_freescape.cpp b/engines/freescape/language/execution_freescape.cpp
index 7ef56674c3c..ccccf44576c 100644
--- a/engines/freescape/language/execution_freescape.cpp
+++ b/engines/freescape/language/execution_freescape.cpp
@@ -23,6 +23,7 @@
 // available at https://github.com/TomHarte/Phantasma/ (MIT)
 
 #include "freescape/freescape.h"
+#include "freescape/language/execution.h"
 #include "freescape/language/variables.h"
 #include "freescape/sweepAABB.h"
 
@@ -298,7 +299,7 @@ bool FreescapeEngine::executeCode(FCLInstructionVector &code, bool shot, bool co
 
 void FreescapeEngine::executeRedraw(FCLInstruction &instruction) {
 	debugC(1, kFreescapeDebugCode, "Redrawing screen");
-	uint32 delay = (100 / 15) + 1;
+	uint32 delay = kFCLRedrawTicks - 1; // waitInLoop includes its final tick.
 	if (isEclipse2() && _currentArea->getAreaID() == _startArea && _gameStateControl == kFreescapeGameStateStart)
 		delay = delay * 10;
 


Commit: 3ac67c9b3013dc0f8451b23d5e467b6a93469a38
    https://github.com/scummvm/scummvm/commit/3ac67c9b3013dc0f8451b23d5e467b6a93469a38
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-08T13:33:08+02:00

Commit Message:
FREESCAPE: initial implementation of sound for 3dck games

Changed paths:
  A engines/freescape/sound/3dck.cpp
  A engines/freescape/sound/3dck.h
  A engines/freescape/sound/3dck_adlib.cpp
    engines/freescape/games/3dck/3dck.cpp
    engines/freescape/games/3dck/3dck.h
    engines/freescape/games/3dck/ui.cpp
    engines/freescape/language/execution_3dck16.cpp
    engines/freescape/module.mk


diff --git a/engines/freescape/games/3dck/3dck.cpp b/engines/freescape/games/3dck/3dck.cpp
index 06c1d36d74a..d8811d6f550 100644
--- a/engines/freescape/games/3dck/3dck.cpp
+++ b/engines/freescape/games/3dck/3dck.cpp
@@ -133,6 +133,9 @@ void KitEngine::loadAssets() {
 	_gfx->_keyColor = 0;
 	_scriptSurface.create(_screenW, _screenH, _gfx->_texturePixelFormat);
 	_scriptSurface.fillRect(_fullscreenViewArea, 0);
+	uint16 soundSize = readBlockSize(file);
+	Common::SeekableSubReadStream sounds(&file, file.pos(), file.pos() + soundSize);
+	loadSounds(sounds);
 }
 
 void KitEngine::loadWorld(Common::SeekableReadStream &file) {
@@ -472,9 +475,18 @@ void KitEngine::updatePlayerMovement(float deltaTime) {
 	float height = _position.y();
 	resolveCollisions(_position + movement * _playerSteps[_playerStepIndex]);
 	checkIfStillInArea();
+	bool blocked = (_position - _lastPosition).length() < 1;
 	_lastPosition = _position;
 	_gotoExecuted = false;
 	clearGameBit(31);
+	if (_hasFallen)
+		_pendingSound = 7;
+	else if (!_flyMode && _position.y() > height)
+		_pendingSound = 5;
+	else if (!_flyMode && _position.y() < height)
+		_pendingSound = 6;
+	else if (blocked)
+		_pendingSound = 2;
 	if (_hasFallen) {
 		_kitVariables[10] += MAX<int>(0, height - _position.y() - _maxFallingDistance);
 		_hasFallen = false;
diff --git a/engines/freescape/games/3dck/3dck.h b/engines/freescape/games/3dck/3dck.h
index ee5b034028a..49695fee625 100644
--- a/engines/freescape/games/3dck/3dck.h
+++ b/engines/freescape/games/3dck/3dck.h
@@ -97,6 +97,8 @@ private:
 	};
 
 	void loadWorld(Common::SeekableReadStream &file);
+	void loadSounds(Common::SeekableReadStream &file);
+	void playPendingSound();
 	Area *loadArea(Common::SeekableReadStream &file);
 	Object *loadObject(Common::SeekableReadStream &file, ObjectData &data);
 	Common::Array<ConditionData> loadConditions(Common::SeekableReadStream &file);
@@ -161,7 +163,7 @@ private:
 	int _lastScriptTick = 0;
 	bool _timerTriggered = false, _initialScriptPending = false;
 	bool _scriptFrameActive = false, _scriptDelayed = false;
-	bool _soundWarning = false;
+	int _pendingSound = -1;
 	byte _pendingInteractions = 0, _shootCooldown = 0, _activateCooldown = 0;
 	uint _scriptQueueIndex = 0;
 	Common::Array<ScriptEntry> _scriptQueue;
diff --git a/engines/freescape/games/3dck/ui.cpp b/engines/freescape/games/3dck/ui.cpp
index c8ac0a951ed..5f757430745 100644
--- a/engines/freescape/games/3dck/ui.cpp
+++ b/engines/freescape/games/3dck/ui.cpp
@@ -107,6 +107,8 @@ void KitEngine::drawUI() {
 	drawFullscreenSurface(_scriptSurface.surfacePtr());
 	_gfx->setViewport(_fullscreenViewArea);
 	_gfx->renderCrossair(_crossairPosition);
+	if (!_scriptFrameActive)
+		playPendingSound();
 }
 
 bool KitEngine::handleInput(const Common::Event &event) {
@@ -224,6 +226,8 @@ void KitEngine::interact(bool shot) {
 		_kitVariables[21]++;
 		if (_kitVariables[20] & 2)
 			_shootingFrames = 3;
+		if (_kitVariables[20] & 4)
+			_pendingSound = 4;
 	}
 	if (!object || !object->isGeometric())
 		return;
diff --git a/engines/freescape/language/execution_3dck16.cpp b/engines/freescape/language/execution_3dck16.cpp
index 01725a257da..5599a6521ee 100644
--- a/engines/freescape/language/execution_3dck16.cpp
+++ b/engines/freescape/language/execution_3dck16.cpp
@@ -27,6 +27,8 @@
 namespace Freescape {
 
 void KitEngine::resetScripts() {
+	stopAllSounds();
+	_pendingSound = -1;
 	// V255 survives ENDGAME, as in the original runner.
 	memset(_kitVariables, 0, 255 * sizeof(_kitVariables[0]));
 	_changedVariables = 0;
@@ -398,6 +400,7 @@ FCLExecutionResult KitEngine::executeCode(ScriptState &script, uint &budget) {
 			break;
 		case Token::REDRAW:
 			_scriptSurface.fillRect(_viewArea, 0);
+			playPendingSound();
 			return animator ? kFCLYielded : kFCLPaused;
 		case Token::LOOP:
 			executeLoop(instruction, script);
@@ -731,10 +734,11 @@ void KitEngine::executeMove(const FCLInstruction &instruction, ScriptState &scri
 }
 
 void KitEngine::executeSound(const FCLInstruction &instruction) {
-	if (!_soundWarning) {
-		warning("3D Construction Kit sound playback is not implemented");
-		_soundWarning = true;
-	}
+	uint16 index = getVariableOrConstant(instruction._source, instruction._sourceType);
+	if (instruction.getType() == Token::SYNCSND)
+		_pendingSound = index == 0xffff ? -1 : index;
+	else if (_sound)
+		_sound->playSound(index, Sound::kTypeNormal);
 }
 
 bool KitEngine::executeObjectConditions(GeometricObject *obj, bool shot, bool collided, bool activated) {
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index 9cdd6b53917..4edc107e0f9 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -78,6 +78,8 @@ MODULE_OBJS := \
 	sound/amiga.o \
 	sound/atari.o \
 	sound/common.o \
+	sound/3dck.o \
+	sound/3dck_adlib.o \
 	sound/cpc.o \
 	sound/dos.o \
 	sound/fx.o \
diff --git a/engines/freescape/sound/3dck.cpp b/engines/freescape/sound/3dck.cpp
new file mode 100644
index 00000000000..9c13f3dad75
--- /dev/null
+++ b/engines/freescape/sound/3dck.cpp
@@ -0,0 +1,190 @@
+/* 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/mutex.h"
+#include "freescape/games/3dck/3dck.h"
+#include "freescape/sound/3dck.h"
+
+namespace Freescape {
+
+class KitSpeakerSound : public Sound, public Audio::AudioStream {
+public:
+	KitSpeakerSound(Audio::Mixer *mixer, const Common::Array<byte> &data);
+	~KitSpeakerSound() override { _mixer->stopHandle(_handle); }
+
+	void playSound(int index, Type type) override;
+	void stopSound(Type type) override;
+	bool isPlayingSound(Type type) const override;
+	bool isSoundAvailable(int index) const override;
+	int readBuffer(int16 *buffer, int samples) override;
+	int getRate() const override { return _mixer->getOutputRate(); }
+	bool isStereo() const override { return false; }
+	bool endOfData() const override { return false; }
+
+private:
+	void loadPart();
+	void tick();
+
+	struct Part {
+		byte steps, duration;
+		uint16 delta;
+	};
+	struct Effect {
+		uint16 divisor;
+		byte repeats;
+		Common::Array<Part> parts;
+	};
+	Common::Array<Effect> _effects;
+	Audio::Mixer *_mixer;
+	Audio::SoundHandle _handle;
+	mutable Common::Mutex _mutex;
+	const Effect *_effect = nullptr;
+	uint _part = 0;
+	byte _steps = 0, _duration = 0, _repeats = 0;
+	uint16 _divisor = 0;
+	uint64 _phase = 0, _timer = 0;
+	Type _type = kTypeNormal;
+};
+
+KitSpeakerSound::KitSpeakerSound(Audio::Mixer *mixer, const Common::Array<byte> &data) : _mixer(mixer) {
+	uint count = READ_LE_UINT16(&data[0x323]);
+	if (0x325 + count * 4 > 0x375)
+		error("Invalid 3D Construction Kit speaker table");
+	_effects.resize(count);
+	for (uint i = 0; i < count; i++) {
+		const byte *entry = &data[0x325 + i * 4];
+		if (entry[0] == 0xff)
+			continue;
+		uint offset = 0x375 + entry[0] * 5;
+		if (offset >= data.size() || !data[offset] || offset + 1 + data[offset] * 4 > data.size())
+			error("Invalid 3D Construction Kit speaker pattern");
+		Effect &effect = _effects[i];
+		effect.divisor = READ_LE_UINT16(entry + 1);
+		effect.repeats = entry[3];
+		effect.parts.resize(data[offset++]);
+		for (Part &part : effect.parts) {
+			part.steps = data[offset];
+			part.delta = READ_LE_UINT16(&data[offset + 1]);
+			part.duration = data[offset + 3];
+			offset += 4;
+		}
+	}
+	_mixer->playStream(Audio::Mixer::kSFXSoundType, &_handle, this, -1,
+		Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO);
+}
+
+bool KitSpeakerSound::isSoundAvailable(int index) const {
+	return index > 0 && uint(index) <= _effects.size() && !_effects[index - 1].parts.empty();
+}
+
+void KitSpeakerSound::playSound(int index, Type type) {
+	if (!isSoundAvailable(index))
+		return;
+	Common::StackLock lock(_mutex);
+	_effect = &_effects[index - 1];
+	_type = type;
+	_part = 0;
+	_repeats = _effect->repeats;
+	_divisor = _effect->divisor;
+	_phase = 0;
+	loadPart();
+}
+
+void KitSpeakerSound::loadPart() {
+	_steps = _effect->parts[_part].steps;
+	_duration = _effect->parts[_part].duration;
+}
+
+void KitSpeakerSound::tick() {
+	if (!_effect || --_duration)
+		return;
+	_duration = _effect->parts[_part].duration;
+	_divisor += _effect->parts[_part].delta;
+	_phase = 0;
+	if (--_steps)
+		return;
+	if (++_part == _effect->parts.size()) {
+		if (!--_repeats) {
+			_effect = nullptr;
+			return;
+		}
+		_part = 0;
+	}
+	loadPart();
+}
+
+int KitSpeakerSound::readBuffer(int16 *buffer, int samples) {
+	Common::StackLock lock(_mutex);
+	// The DOS IRQ uses PIT reload 3637; channel 2 holds an unsigned divisor.
+	uint64 interval = uint64(getRate()) * 3637;
+	for (int i = 0; i < samples; i++) {
+		if (_effect) {
+			uint64 period = uint64(_divisor ? _divisor : 65536) * getRate();
+			_phase %= period;
+			buffer[i] = _phase < period / 2 ? 2540 : -2540;
+			_phase += kKitPITClock;
+		} else {
+			buffer[i] = 0;
+		}
+		_timer += kKitPITClock;
+		while (_timer >= interval) {
+			_timer -= interval;
+			tick();
+		}
+	}
+	return samples;
+}
+
+void KitSpeakerSound::stopSound(Type type) {
+	Common::StackLock lock(_mutex);
+	if (type == kTypeNormal || _type == type)
+		_effect = nullptr;
+}
+
+bool KitSpeakerSound::isPlayingSound(Type type) const {
+	Common::StackLock lock(_mutex);
+	return _effect && (type == kTypeNormal || _type == type);
+}
+
+void KitEngine::loadSounds(Common::SeekableReadStream &file) {
+	Common::Array<byte> data;
+	data.resize(file.size());
+	if (data.size() < 0x108 || file.read(data.data(), data.size()) != data.size() ||
+			memcmp(data.data(), "3D Construction Kit (c) Incentive Software.", 42))
+		error("Invalid 3D Construction Kit sound driver");
+	uint16 init = READ_LE_UINT16(&data[0x100]);
+	uint16 play = READ_LE_UINT16(&data[0x104]);
+	if (init == 0x124 && play == 0x1c5 && data.size() >= 0x375)
+		_sound = new KitSpeakerSound(_mixer, data);
+	else if (init == 0x79a && play == 0x864 && data.size() >= 0x4480)
+		_sound = createKitAdLibSound(_mixer, data);
+	else if (init != 0x115 || play != 0x1a7)
+		warning("Unsupported 3D Construction Kit sound driver");
+}
+
+void KitEngine::playPendingSound() {
+	if (_pendingSound >= 0 && _sound)
+		_sound->playSound(_pendingSound, Sound::kTypeNormal);
+	_pendingSound = -1;
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/sound/3dck.h b/engines/freescape/sound/3dck.h
new file mode 100644
index 00000000000..a2965b0db1f
--- /dev/null
+++ b/engines/freescape/sound/3dck.h
@@ -0,0 +1,41 @@
+/* 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_SOUND_3DCK_H
+#define FREESCAPE_SOUND_3DCK_H
+
+#include "common/array.h"
+
+namespace Audio {
+class Mixer;
+}
+
+namespace Freescape {
+
+class Sound;
+
+static const uint32 kKitPITClock = 1193182;
+
+Sound *createKitAdLibSound(Audio::Mixer *mixer, const Common::Array<byte> &data);
+
+} // namespace Freescape
+
+#endif
diff --git a/engines/freescape/sound/3dck_adlib.cpp b/engines/freescape/sound/3dck_adlib.cpp
new file mode 100644
index 00000000000..f4ffc715148
--- /dev/null
+++ b/engines/freescape/sound/3dck_adlib.cpp
@@ -0,0 +1,725 @@
+/* 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 "audio/fmopl.h"
+#include "audio/mixer.h"
+#include "common/endian.h"
+#include "common/mutex.h"
+#include "common/queue.h"
+#include "common/textconsole.h"
+#include "freescape/sound.h"
+#include "freescape/sound/3dck.h"
+
+namespace Freescape {
+
+// The RUN driver uses early Westwood AdLib bytecode with absolute offsets.
+class KitAdLibSound : public Sound {
+public:
+	KitAdLibSound(Audio::Mixer *mixer, const Common::Array<byte> &data);
+	~KitAdLibSound() override;
+	void playSound(int index, Type type) override;
+	void stopSound(Type type) override;
+	bool isPlayingSound(Type type) const override;
+	bool isSoundAvailable(int index) const override;
+
+private:
+	enum Command {
+		kRepeat = 0, kCheckRepeat, kStartProgram, kNoteSpacing, kJump, kCall, kReturn,
+		kBaseOctave, kStop, kRest, kWriteRegister, kNoteDuration, kBaseNote,
+		kSecondaryEffect, kStopOther,
+		kInstrument = 16, kSlide, kRemoveSlide, kBaseFrequency,
+		kVibrato = 21, kPriority = 26, kBeat = 28, kWaitBeat, kLevel1,
+		kDuration = 32, kNoteOn, kFractionalSpacing = 36, kTempo = 38,
+		kRemoveSecondary, kChannelTempo = 41, kLevel3 = 43, kLevel2, kChangeLevel2,
+		kAMDepth, kVibratoDepth, kChangeLevel1, kClearChannel = 51,
+		kRandomNote = 53, kRemoveVibrato, kPitchBend = 57, kResetTempo, kNop,
+		kRandomDuration, kChangeTempo, kKeyModulation = 63, kRemoveKeyModulation,
+		kSetupRhythm, kPlayRhythm, kRemoveRhythm, kRhythmLevel2, kChangeRhythmLevel1,
+		kRhythmLevel1, kTempoReset = 72, kNoteLevels
+	};
+	enum Effect { kNoEffect, kSlideEffect, kVibratoEffect };
+	struct Channel {
+		uint32 ip = 0;
+		uint16 stack[4] = {};
+		byte stackSize = 0, duration = 0, repeat = 0;
+		int8 priority = 0, baseOctave = 0, baseNote = 0, pitchBend = 0;
+		byte baseFrequency = 0, rawNote = 0;
+		byte tempo = 255, timer = 255, tempoReset = 0;
+		byte spacing = 1, fractionalSpacing = 0, gateDuration = 0, durationRandomness = 0;
+		byte regA = 0, regB = 0;
+		byte level1 = 0, level2 = 0, level3 = 0;
+		byte opLevel[2] = {}, noteLevelScale[2] = {}, noteLevel = 0;
+		bool additive = false;
+		Effect effect = kNoEffect;
+		byte slideTempo = 0, slideTimer = 255;
+		int16 slideStep = 0, vibratoStep = 0;
+		byte vibratoTempo = 0, vibratoTimer = 0, vibratoRange = 0;
+		byte vibratoSteps = 0, vibratoCount = 0, vibratoDelay = 0, vibratoWait = 0;
+		uint16 secondaryData = 0;
+		byte secondaryTempo = 0, secondaryTimer = 0, secondaryRegister = 0;
+		int8 secondarySize = 0, secondaryPosition = 0;
+	};
+
+	bool contains(uint32 offset, uint size) const { return offset <= _data.size() && size <= _data.size() - offset; }
+	uint16 word(uint offset) const { return contains(offset, 2) ? READ_LE_UINT16(&_data[offset]) : 0; }
+	uint16 program(uint16 bank, byte index) const;
+	uint16 mapSound(uint16 index) const { return index <= 20 ? word(0x2f6 + index * 2) : index; }
+	static bool advance(byte &timer, byte tempo) {
+		uint sum = timer + tempo;
+		timer = sum;
+		return sum > 255;
+	}
+	void onTimer();
+	void tick();
+	void startProgram(byte index);
+	void stopChannel(uint index, bool clear);
+	bool executeChannel(uint index);
+	bool executeCommand(uint index, byte command, const byte *values);
+	void updateEffects(uint index);
+	void setupNote(uint index, byte note);
+	void noteOn(uint index);
+	void noteOff(uint index);
+	bool setupDuration(Channel &channel, byte duration);
+	void setupInstrument(uint index, byte instrument, Channel &channel);
+	void setupRhythm(Channel &channel, const byte *values);
+	void rhythmLevel(byte command, byte mask, byte value);
+	void adjustLevel(uint index);
+	byte operatorLevel(const Channel &channel, uint op) const;
+	void writeRegister(byte reg, byte value);
+	void writeVolume(uint index);
+	void updateVolume();
+	uint16 getRandomNumber();
+
+	Common::Array<byte> _data;
+	Common::Queue<byte> _queue;
+	Channel _channels[10];
+	Audio::Mixer *_mixer;
+	OPL::OPL *_opl;
+	mutable Common::Mutex _mutex;
+	uint32 _timer = 0;
+	uint16 _bank = 0, _random = 0x1234;
+	byte _divider = 12, _tempo = 0, _beatTimer = 255;
+	byte _beatDivider = 0, _beatCount = 0, _beat = 0, _beatWaiting = 0;
+	byte _depth = 0, _rhythm = 0;
+	byte _rhythmLevel[5] = {}, _rhythmLevel1[5] = {}, _rhythmLevel2[5] = {};
+	byte _keyModulation = 0, _keyPeriod = 0, _keyTimer = 0, _keyState = 0;
+	byte _registers[256] = {};
+	int _volume = 255;
+	static const byte kOperators[9];
+	static const byte kArgumentCounts[74];
+};
+
+const byte KitAdLibSound::kOperators[9] = {0, 1, 2, 8, 9, 10, 16, 17, 18};
+const byte KitAdLibSound::kArgumentCounts[74] = {
+	1, 2, 1, 1, 2, 2, 0, 1, 0, 1, 2, 2, 1, 5, 1, 0,
+	1, 3, 0, 1, 0, 4, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0,
+	1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 2, 2, 1, 1,
+	1, 0, 0, 1, 0, 2, 0, 0, 0, 1, 0, 0, 1, 1, 0, 2,
+	0, 9, 1, 0, 2, 2, 2, 0, 1, 2
+};
+
+KitAdLibSound::KitAdLibSound(Audio::Mixer *mixer, const Common::Array<byte> &data) : _data(data), _mixer(mixer) {
+	_opl = OPL::Config::create();
+	if (!_opl || !_opl->init())
+		error("Unable to initialize 3D Construction Kit AdLib sound");
+	_bank = word(0x447c);
+	writeRegister(1, 0x20);
+	writeRegister(8, 0);
+	stopSound(kTypeNormal);
+	_opl->start(new Common::Functor0Mem<void, KitAdLibSound>(this, &KitAdLibSound::onTimer), 1000);
+}
+
+KitAdLibSound::~KitAdLibSound() {
+	_opl->stop();
+	delete _opl;
+}
+
+uint16 KitAdLibSound::program(uint16 bank, byte index) const {
+	uint16 offset = word(bank + index * 2);
+	return offset && contains(offset, 2) && _data[offset] < 10 ? offset : 0;
+}
+
+bool KitAdLibSound::isSoundAvailable(int index) const {
+	if (index < 0 || index > 0xffff)
+		return false;
+	uint16 sound = mapSound(index);
+	return program(word(0x447c + (sound > 255 ? 2 : 0)), sound & 255) != 0;
+}
+
+void KitAdLibSound::playSound(int index, Type type) {
+	if (!isSoundAvailable(index))
+		return;
+	Common::StackLock lock(_mutex);
+	uint16 sound = mapSound(index);
+	_bank = word(0x447c + (sound > 255 ? 2 : 0));
+	if (_queue.size() == 16)
+		_queue.pop();
+	_queue.push(sound & 255);
+}
+
+void KitAdLibSound::stopSound(Type type) {
+	if (type == kTypeMovement)
+		return;
+	Common::StackLock lock(_mutex);
+	_queue.clear();
+	_keyModulation = _rhythm = _depth = 0;
+	writeRegister(0xbd, 0);
+	for (uint i = 0; i < 10; i++) {
+		_channels[i] = Channel();
+		stopChannel(i, true);
+	}
+}
+
+bool KitAdLibSound::isPlayingSound(Type type) const {
+	Common::StackLock lock(_mutex);
+	if (!_queue.empty())
+		return true;
+	for (const Channel &channel : _channels)
+		if (channel.ip)
+			return true;
+	return false;
+}
+
+void KitAdLibSound::onTimer() {
+	Common::StackLock lock(_mutex);
+	updateVolume();
+	// PIT reload 1365, with one sequencer update every twelve interrupts.
+	_timer += kKitPITClock;
+	while (_timer >= 1365 * 1000) {
+		_timer -= 1365 * 1000;
+		if (_keyModulation && !--_keyTimer) {
+			_keyState ^= 0x20;
+			_channels[0].regB = (_channels[0].regB & 0x1f) | _keyState;
+			writeRegister(0xb0, _channels[0].regB);
+			_keyTimer = _keyPeriod;
+		}
+		if (!--_divider) {
+			_divider = 12;
+			tick();
+		}
+	}
+}
+
+void KitAdLibSound::startProgram(byte index) {
+	uint16 offset = program(_bank, index);
+	if (!offset)
+		return;
+	byte channelIndex = _data[offset];
+	int8 priority = _data[offset + 1];
+	Channel &channel = _channels[channelIndex];
+	if (!channelIndex)
+		_keyModulation = 0;
+	if (priority < channel.priority)
+		return;
+	byte level = channel.level2;
+	channel = Channel();
+	channel.level2 = level;
+	channel.priority = priority;
+	channel.ip = offset + 2;
+	channel.duration = 1;
+	if (channelIndex < 9 && (!_rhythm || channelIndex < 6)) {
+		byte op = kOperators[channelIndex];
+		writeRegister(0x60 + op, 255);
+		writeRegister(0x63 + op, 255);
+		writeRegister(0x80 + op, 255);
+		writeRegister(0x83 + op, 255);
+		writeRegister(0xb0 + channelIndex, 0);
+		writeRegister(0xb0 + channelIndex, 0x20);
+	}
+}
+
+void KitAdLibSound::tick() {
+	while (!_queue.empty())
+		startProgram(_queue.pop());
+	for (int i = 9; i >= 0; i--) {
+		Channel &channel = _channels[i];
+		if (!channel.ip)
+			continue;
+		if (channel.tempoReset)
+			channel.tempo = _tempo;
+		if (advance(channel.timer, channel.tempo)) {
+			if (!--channel.duration) {
+				if (!executeChannel(i))
+					continue;
+			} else if (channel.duration == channel.spacing || channel.duration == channel.gateDuration) {
+				noteOff(i);
+			}
+		}
+		if (channel.ip)
+			updateEffects(i);
+	}
+	if (advance(_beatTimer, _tempo) && !--_beatCount) {
+		_beatCount = _beatDivider;
+		_beat++;
+	}
+}
+
+bool KitAdLibSound::executeChannel(uint index) {
+	Channel &channel = _channels[index];
+	for (uint budget = 4096; budget && channel.ip; budget--) {
+		if (!contains(channel.ip, 1))
+			break;
+		byte opcode = _data[channel.ip++];
+		if (opcode & 0x80) {
+			opcode &= 0x7f;
+			if (opcode >= ARRAYSIZE(kArgumentCounts) || !contains(channel.ip, kArgumentCounts[opcode]))
+				break;
+			const byte *values = _data.data() + channel.ip;
+			channel.ip += kArgumentCounts[opcode];
+			if (executeCommand(index, opcode, values))
+				return channel.ip != 0 && opcode != kWaitBeat;
+		} else {
+			uint size = 1 + ((channel.noteLevelScale[0] || channel.noteLevelScale[1]) ? 1 : 0);
+			if (!contains(channel.ip, size))
+				break;
+			setupNote(index, opcode);
+			noteOn(index);
+			bool wait = setupDuration(channel, _data[channel.ip++]);
+			if (size == 2) {
+				channel.noteLevel = _data[channel.ip++];
+				adjustLevel(index);
+			}
+			if (wait)
+				return true;
+		}
+	}
+	if (channel.ip)
+		warning("Invalid 3D Construction Kit AdLib program at %x", channel.ip);
+	stopChannel(index, false);
+	return false;
+}
+
+bool KitAdLibSound::executeCommand(uint index, byte command, const byte *v) {
+	Channel &channel = _channels[index];
+	switch (command) {
+	case kRepeat: channel.repeat = v[0]; break;
+	case kCheckRepeat:
+		if (!--channel.repeat)
+			break;
+		// fall through
+	case kJump: channel.ip = READ_LE_UINT16(v); break;
+	case kCall:
+		if (channel.stackSize == ARRAYSIZE(channel.stack)) {
+			stopChannel(index, false);
+			return true;
+		}
+		channel.stack[channel.stackSize++] = channel.ip;
+		channel.ip = READ_LE_UINT16(v);
+		break;
+	case kReturn:
+		if (!channel.stackSize) {
+			stopChannel(index, false);
+			return true;
+		}
+		channel.ip = channel.stack[--channel.stackSize];
+		break;
+	case kStartProgram:
+		if (v[0] != 255)
+			startProgram(v[0]);
+		break;
+	case kNoteSpacing: channel.spacing = v[0]; break;
+	case kBaseOctave: channel.baseOctave = v[0]; break;
+	case kRest:
+		noteOff(index);
+		return setupDuration(channel, v[0]);
+	case kWriteRegister: writeRegister(v[0], v[1]); break;
+	case kNoteDuration:
+		setupNote(index, v[0]);
+		return setupDuration(channel, v[1]);
+	case kBaseNote: channel.baseNote = v[0]; break;
+	case kSecondaryEffect:
+		channel.secondaryTempo = channel.secondaryTimer = v[0];
+		channel.secondarySize = channel.secondaryPosition = v[1];
+		channel.secondaryRegister = v[2];
+		channel.secondaryData = READ_LE_UINT16(v + 3);
+		break;
+	case kStopOther:
+		if (v[0] < 10) {
+			_channels[v[0]].ip = 0;
+			_channels[v[0]].priority = 0;
+		}
+		break;
+	case kInstrument: setupInstrument(index, v[0], channel); break;
+	case kSlide:
+		channel.slideTempo = v[0];
+		channel.slideTimer = 255;
+		channel.slideStep = READ_BE_UINT16(v + 1);
+		channel.effect = kSlideEffect;
+		break;
+	case kRemoveSlide:
+		channel.slideStep = 0;
+		// fall through
+	case kRemoveVibrato: channel.effect = kNoEffect; break;
+	case kBaseFrequency: channel.baseFrequency = v[0]; break;
+	case kVibrato:
+		channel.vibratoTempo = v[0];
+		channel.vibratoRange = v[1];
+		channel.vibratoCount = v[2] + 1;
+		channel.vibratoSteps = v[2] * 2;
+		channel.vibratoDelay = v[3];
+		channel.effect = kVibratoEffect;
+		break;
+	case kPriority: channel.priority = v[0]; break;
+	case kBeat:
+		_beatDivider = _beatCount = v[0] >> 1;
+		_beatTimer = 255;
+		_beat = _beatWaiting = 0;
+		break;
+	case kWaitBeat:
+		if (_beatWaiting && (_beat & v[0])) {
+			_beatWaiting = 0;
+			break;
+		}
+		if (!_beatWaiting && !(_beat & v[0]))
+			_beatWaiting = 1;
+		channel.ip -= 2;
+		channel.duration = 1;
+		return true;
+	case kLevel1: channel.level1 = v[0]; adjustLevel(index); break;
+	case kDuration: return setupDuration(channel, v[0]);
+	case kNoteOn:
+		noteOn(index);
+		return setupDuration(channel, v[0]);
+	case kFractionalSpacing: channel.fractionalSpacing = v[0] & 7; break;
+	case kTempo: _tempo = v[0]; break;
+	case kRemoveSecondary: channel.secondaryData = 0; break;
+	case kChannelTempo: channel.tempo = v[0]; break;
+	case kLevel3: channel.level3 = v[0]; break;
+	case kLevel2:
+	case kChangeLevel2:
+		if (v[0] < 10) {
+			_channels[v[0]].level2 = v[1] + (command == kChangeLevel2 ? _channels[v[0]].level2 : 0);
+			adjustLevel(v[0]);
+		}
+		break;
+	case kAMDepth:
+		_depth = (_depth & 0x7f) | ((v[0] & 1) << 7);
+		writeRegister(0xbd, _depth);
+		break;
+	case kVibratoDepth:
+		_depth = (_depth & 0xbf) | ((v[0] & 1) << 6);
+		writeRegister(0xbd, _depth);
+		break;
+	case kChangeLevel1: channel.level1 += v[0]; adjustLevel(index); break;
+	case kClearChannel:
+		if (v[0] < 10)
+			stopChannel(v[0], true);
+		break;
+	case kRandomNote: {
+		uint16 note = (((channel.regB & 0x1f) << 8) | channel.regA) + (getRandomNumber() & READ_BE_UINT16(v));
+		if (index < 9) {
+			writeRegister(0xa0 + index, note & 255);
+			writeRegister(0xb0 + index, (note >> 8) | (channel.regB & 0x20));
+		}
+		break;
+	}
+	case kPitchBend: channel.pitchBend = v[0]; setupNote(index, channel.rawNote); break;
+	case kResetTempo: channel.tempo = _tempo; break;
+	case kNop: break;
+	case kRandomDuration: channel.durationRandomness = v[0]; break;
+	case kChangeTempo: channel.tempo = CLIP<int>(channel.tempo + int8(v[0]), 1, 255); break;
+	case kKeyModulation:
+		if (v[1] < 5) {
+			_keyModulation = v[0];
+			if (v[0] == 2) {
+				uint16 period = word(0x21b5 + v[1] * 2);
+				uint16 frequency = word(0x21b5 + (v[1] + 1) * 2);
+				if (contains(period, 1) && contains(frequency, 1)) {
+					_keyPeriod = _data[period];
+					writeRegister(0xa0, _data[frequency]);
+				}
+			}
+		}
+		break;
+	case kRemoveKeyModulation: _keyModulation = 0; break;
+	case kSetupRhythm: setupRhythm(channel, v); break;
+	case kPlayRhythm:
+		writeRegister(0xbd, (_rhythm & ~(v[0] & 31)) | 0x20);
+		_rhythm |= v[0];
+		writeRegister(0xbd, _depth | 0x20 | _rhythm);
+		break;
+	case kRemoveRhythm:
+		_rhythm = 0;
+		writeRegister(0xbd, _depth & 0xc0);
+		break;
+	case kRhythmLevel2:
+	case kChangeRhythmLevel1:
+	case kRhythmLevel1: rhythmLevel(command, v[0], v[1]); break;
+	case kTempoReset: channel.tempoReset = v[0]; break;
+	case kNoteLevels:
+		channel.noteLevelScale[0] = v[0];
+		channel.noteLevelScale[1] = v[1];
+		break;
+	default:
+		stopChannel(index, false);
+		return true;
+	}
+	return false;
+}
+
+void KitAdLibSound::stopChannel(uint index, bool clear) {
+	Channel &channel = _channels[index];
+	channel.ip = 0;
+	channel.duration = 0;
+	channel.priority = 0;
+	if (!clear) {
+		noteOff(index);
+		return;
+	}
+	channel.level2 = 0;
+	if (!index)
+		_keyModulation = 0;
+	if (index < 9) {
+		writeRegister(0xc0 + index, 0);
+		writeRegister(0x43 + kOperators[index], 0x3f);
+		writeRegister(0x83 + kOperators[index], 0xff);
+		writeRegister(0xb0 + index, 0);
+	}
+}
+
+void KitAdLibSound::setupNote(uint index, byte note) {
+	if (index >= 9)
+		return;
+	Channel &channel = _channels[index];
+	channel.rawNote = note;
+	int pitch = int8((note & 15) + channel.baseNote);
+	int octave = byte((note & 0xf0) + channel.baseOctave);
+	while (pitch < 0) { pitch += 12; octave -= 16; }
+	while (pitch >= 12) { pitch -= 12; octave += 16; }
+	uint16 frequency = word(0x63e + pitch * 2) + channel.baseFrequency;
+	uint16 value = frequency | (((octave >> 2) & 0x1c) << 8);
+	if (channel.pitchBend) {
+		uint table = (note & 15) + (channel.pitchBend > 0 ? 2 : 0);
+		uint offset = word(0x11a + table * 2) + ABS(int(channel.pitchBend));
+		if (contains(offset, 1))
+			value += channel.pitchBend > 0 ? _data[offset] : -_data[offset];
+	}
+	channel.regA = value;
+	channel.regB = (value >> 8) | (channel.regB & 0x20);
+	writeRegister(0xa0 + index, channel.regA);
+	writeRegister(0xb0 + index, channel.regB);
+}
+
+void KitAdLibSound::noteOn(uint index) {
+	if (index >= 9)
+		return;
+	Channel &channel = _channels[index];
+	channel.regB |= 0x20;
+	writeRegister(0xb0 + index, channel.regB);
+	uint frequency = ((channel.regB << 8) | channel.regA) & 0x3ff;
+	channel.vibratoStep = (frequency >> (9 - MIN<uint>(channel.vibratoRange, 9))) & 255;
+	channel.vibratoWait = channel.vibratoDelay;
+}
+
+void KitAdLibSound::noteOff(uint index) {
+	if (index >= 9 || (_rhythm && index >= 6))
+		return;
+	_channels[index].regB &= ~0x20;
+	writeRegister(0xb0 + index, _channels[index].regB);
+}
+
+bool KitAdLibSound::setupDuration(Channel &channel, byte duration) {
+	channel.duration = duration;
+	if (channel.durationRandomness)
+		channel.duration += getRandomNumber() & channel.durationRandomness;
+	else if (channel.fractionalSpacing)
+		channel.gateDuration = (duration >> 3) * channel.fractionalSpacing;
+	return duration != 0;
+}
+
+void KitAdLibSound::setupInstrument(uint index, byte instrument, Channel &channel) {
+	uint offset = word(0x1892 + instrument * 2);
+	if (index >= 9 || !offset || !contains(offset, 11))
+		return;
+	const byte *data = &_data[offset];
+	byte op = kOperators[index];
+	writeRegister(0x20 + op, data[0]);
+	writeRegister(0x23 + op, data[1]);
+	writeRegister(0xc0 + index, data[2]);
+	channel.additive = data[2] & 1;
+	writeRegister(0xe0 + op, data[3]);
+	writeRegister(0xe3 + op, data[4]);
+	channel.opLevel[0] = data[5];
+	channel.opLevel[1] = data[6];
+	writeRegister(0x40 + op, operatorLevel(channel, 0));
+	writeRegister(0x43 + op, operatorLevel(channel, 1));
+	writeRegister(0x60 + op, data[7]);
+	writeRegister(0x63 + op, data[8]);
+	writeRegister(0x80 + op, data[9]);
+	writeRegister(0x83 + op, data[10]);
+}
+
+byte KitAdLibSound::operatorLevel(const Channel &channel, uint op) const {
+	byte level = channel.opLevel[op] & 63;
+	if (op || channel.additive)
+		level += channel.level1 + channel.level2 + channel.level3;
+	if (channel.noteLevelScale[op]) {
+		uint shift = channel.noteLevelScale[op] + 1;
+		if (shift < 16)
+			level -= uint16(channel.noteLevel << shift) >> 8;
+	}
+	return (channel.opLevel[op] & 0xc0) | (int8(level) < 0 ? 0 : MIN<int>(level, 63));
+}
+
+void KitAdLibSound::adjustLevel(uint index) {
+	if (index >= 9)
+		return;
+	Channel &channel = _channels[index];
+	writeRegister(0x43 + kOperators[index], operatorLevel(channel, 1));
+	writeRegister(0x40 + kOperators[index], operatorLevel(channel, 0));
+}
+
+void KitAdLibSound::updateEffects(uint index) {
+	if (index >= 9)
+		return;
+	Channel &channel = _channels[index];
+	bool changed = false;
+	uint16 frequency = ((channel.regB << 8) | channel.regA) & 0x3ff;
+	if (channel.effect == kSlideEffect && advance(channel.slideTimer, channel.slideTempo)) {
+		byte octave = channel.regB & 0x1c;
+		frequency += channel.slideStep;
+		if (channel.slideStep >= 0 && int16(frequency) >= 0x2de) {
+			frequency >>= 1;
+			octave = (octave + 4) & 0x1c;
+		} else if (channel.slideStep < 0 && int16(frequency) <= 0x184) {
+			frequency <<= 1;
+			octave = (octave - 4) & 0x1c;
+		}
+		frequency &= 0x3ff;
+		channel.regB = (channel.regB & 0x20) | octave;
+		changed = true;
+	} else if (channel.effect == kVibratoEffect) {
+		if (channel.vibratoWait) {
+			channel.vibratoWait--;
+		} else if (advance(channel.vibratoTimer, channel.vibratoTempo)) {
+			if (!--channel.vibratoCount) {
+				channel.vibratoStep = -channel.vibratoStep;
+				channel.vibratoCount = channel.vibratoSteps;
+			}
+			frequency += channel.vibratoStep;
+			changed = true;
+		}
+	}
+	if (changed) {
+		channel.regA = frequency;
+		channel.regB = (channel.regB & 0xfc) | (frequency >> 8);
+		writeRegister(0xa0 + index, channel.regA);
+		writeRegister(0xb0 + index, channel.regB);
+	}
+	if (channel.secondaryData && advance(channel.secondaryTimer, channel.secondaryTempo)) {
+		if (--channel.secondaryPosition < 0)
+			channel.secondaryPosition = channel.secondarySize;
+		uint offset = channel.secondaryData + byte(channel.secondaryPosition);
+		if (contains(offset, 1))
+			writeRegister(channel.secondaryRegister + kOperators[index], _data[offset]);
+	}
+}
+
+void KitAdLibSound::setupRhythm(Channel &channel, const byte *values) {
+	for (uint i = 0; i < 3; i++) {
+		setupInstrument(6 + i, values[i], channel);
+		if (i == 0)
+			_rhythmLevel[4] = channel.opLevel[1];
+		else if (i == 1) {
+			_rhythmLevel[0] = channel.opLevel[0];
+			_rhythmLevel[3] = channel.opLevel[1];
+		} else {
+			_rhythmLevel[2] = channel.opLevel[0];
+			_rhythmLevel[1] = channel.opLevel[1];
+		}
+	}
+	for (uint i = 0; i < 3; i++) {
+		_channels[6 + i].regB = values[3 + i * 2] & 0x2f;
+		writeRegister(0xb6 + i, _channels[6 + i].regB);
+		writeRegister(0xa6 + i, values[4 + i * 2]);
+	}
+	_rhythm = 0x20;
+}
+
+void KitAdLibSound::rhythmLevel(byte command, byte mask, byte value) {
+	static const byte registers[5] = {0x51, 0x55, 0x52, 0x54, 0x53};
+	for (uint i = 0; i < 5; i++) {
+		if (!(mask & (1 << i)))
+			continue;
+		byte level = value + _rhythmLevel[i] + _rhythmLevel2[i];
+		if (command == kRhythmLevel2) {
+			_rhythmLevel2[i] = value;
+			level = value + _rhythmLevel[i] + _rhythmLevel1[i] + value;
+		} else if (command == kChangeRhythmLevel1) {
+			level += _rhythmLevel1[i];
+			_rhythmLevel1[i] = MIN<int>(level, 63);
+		} else {
+			_rhythmLevel1[i] = value;
+		}
+		writeRegister(registers[i], MIN<int>(level, 63));
+	}
+}
+
+void KitAdLibSound::writeVolume(uint index) {
+	byte op = kOperators[index];
+	for (uint i = 0; i < 2; i++) {
+		byte reg = 0x40 + op + i * 3;
+		byte value = _registers[reg];
+		if (i || (_registers[0xc0 + index] & 1) || ((_registers[0xbd] & 0x20) && index >= 7))
+			value = (value & 0xc0) | (63 - (63 - (value & 63)) * _volume / 255);
+		_opl->writeReg(reg, value);
+	}
+}
+
+void KitAdLibSound::writeRegister(byte reg, byte value) {
+	_registers[reg] = value;
+	if (reg >= 0x40 && reg <= 0x55) {
+		for (uint i = 0; i < 9; i++) {
+			if (reg == 0x40 + kOperators[i] || reg == 0x43 + kOperators[i]) {
+				writeVolume(i);
+				return;
+			}
+		}
+	}
+	_opl->writeReg(reg, value);
+	if (reg >= 0xc0 && reg <= 0xc8)
+		writeVolume(reg - 0xc0);
+	else if (reg == 0xbd)
+		for (uint i = 6; i < 9; i++)
+			writeVolume(i);
+}
+
+void KitAdLibSound::updateVolume() {
+	int volume = _mixer->isSoundTypeMuted(Audio::Mixer::kSFXSoundType) ? 0 :
+		_mixer->getVolumeForSoundType(Audio::Mixer::kSFXSoundType);
+	if (_volume == volume)
+		return;
+	_volume = volume;
+	for (uint i = 0; i < 9; i++)
+		writeVolume(i);
+}
+
+uint16 KitAdLibSound::getRandomNumber() {
+	_random += 0x9248;
+	_random = (_random >> 3) | (_random << 13);
+	return _random;
+}
+
+Sound *createKitAdLibSound(Audio::Mixer *mixer, const Common::Array<byte> &data) {
+	return new KitAdLibSound(mixer, data);
+}
+
+} // namespace Freescape


Commit: fa5891998224ade6d6d11746875587420de93351
    https://github.com/scummvm/scummvm/commit/fa5891998224ade6d6d11746875587420de93351
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-08T13:33:08+02:00

Commit Message:
FREESCAPE: fixes to run Desert Maze (3dck game)

Changed paths:
    engines/freescape/games/3dck/3dck.cpp
    engines/freescape/language/execution_3dck16.cpp


diff --git a/engines/freescape/games/3dck/3dck.cpp b/engines/freescape/games/3dck/3dck.cpp
index d8811d6f550..0cbb56f3b42 100644
--- a/engines/freescape/games/3dck/3dck.cpp
+++ b/engines/freescape/games/3dck/3dck.cpp
@@ -339,6 +339,13 @@ Object *KitEngine::loadObject(Common::SeekableReadStream &file, ObjectData &data
 	if (geometric) {
 		ObjectType type = ObjectType(data.type);
 		int colorCount = GeometricObject::numberOfColoursForObjectOfType(type);
+		int ordinateCount = GeometricObject::numberOfOrdinatesForType(type);
+		// Hidden editor remnants can lack geometry (Desert Maze).
+		if ((data.flags & kKitInitiallyInvisible) && payload.size() < colorCount + 2 * ordinateCount) {
+			warning("Ignoring incomplete hidden 3D Construction Kit object %u", data.id);
+			file.seek(end);
+			return nullptr;
+		}
 		colors = new Common::Array<uint8>();
 		for (int i = 0; i < colorCount; i += 2) {
 			byte first, second;
@@ -352,7 +359,6 @@ Object *KitEngine::loadObject(Common::SeekableReadStream &file, ObjectData &data
 			for (uint i = 0; i < ARRAYSIZE(sides); i++)
 				(*colors)[i] = sides[i];
 		}
-		int ordinateCount = GeometricObject::numberOfOrdinatesForType(type);
 		if (ordinateCount) {
 			requireBytes(payload, 2 * ordinateCount);
 			ordinates = new Common::Array<float>();
@@ -414,18 +420,22 @@ void KitEngine::initGameState() {
 void KitEngine::gotoArea(uint16 areaID, int entranceID) {
 	if (!_areaMap.contains(areaID))
 		error("Unknown 3D Construction Kit area %u", areaID);
+	float oldScale = _currentArea ? _currentArea->getScale() : 1;
 	if (_currentArea)
 		_kitVariables[9] = _currentArea->getAreaID();
 	_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();
+	if (entrance) {
+		_position = entrance->getOrigin();
+		_position.y() += _playerHeight;
+		Math::Vector3d rotation = entrance->getRotation();
+		_pitch = rotation.x();
+		_yaw = 90.0f - rotation.y();
+		_roll = rotation.z();
+	} else {
+		// RUNVGA retains world coordinates and rotation when the entrance is absent.
+		_position *= oldScale / _currentArea->getScale();
+	}
 	_lastPosition = _position;
 	_gfx->_scale = _currentArea->getScale();
 	_gotoExecuted = true;
diff --git a/engines/freescape/language/execution_3dck16.cpp b/engines/freescape/language/execution_3dck16.cpp
index 5599a6521ee..f0e12ce7dcd 100644
--- a/engines/freescape/language/execution_3dck16.cpp
+++ b/engines/freescape/language/execution_3dck16.cpp
@@ -604,7 +604,7 @@ bool KitEngine::executeGoto(const FCLInstruction &instruction) {
 	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)) {
+	if (!_areaMap.contains(area)) {
 		warning("Invalid 3D Construction Kit GOTO (%d, %u)", entrance, area);
 		return false;
 	}


Commit: d36c070008f1652233caf5a07f28e0724d514222
    https://github.com/scummvm/scummvm/commit/d36c070008f1652233caf5a07f28e0724d514222
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-08T13:33:09+02:00

Commit Message:
FREESCAPE: corrected palette in CPC 3dck games

Changed paths:
    engines/freescape/detection.cpp
    engines/freescape/games/3dck/8bit.cpp
    engines/freescape/games/3dck/8bit.h
    engines/freescape/games/3dck/8bitUI.cpp
    engines/freescape/language/execution_3dck8.cpp


diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index ad80610c043..602ba9531c8 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -1165,6 +1165,16 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE,
 		GUIO2(GUIO_NOMIDI, GUIO_RENDERCPC)
 	},
+	{
+		"3dkit",
+		"Ciudadela Fantasma",
+		AD_ENTRY2s("DATA.DAT", "284bd42e1ce459a9c97e30496d35803f", 6271,
+			"CIUDAD10.BIN", "ad9339f91dd579351f0a1a7b4d963c49", 25216),
+		Common::ES_ESP,
+		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
index a23aa372257..9e307cd8eaf 100644
--- a/engines/freescape/games/3dck/8bit.cpp
+++ b/engines/freescape/games/3dck/8bit.cpp
@@ -20,6 +20,7 @@
  */
 
 #include "common/algorithm.h"
+#include "common/endian.h"
 #include "common/substream.h"
 #include "math/utils.h"
 
@@ -43,11 +44,27 @@ Kit8Engine::Kit8Engine(OSystem *syst, const ADGameDescription *gd) : FreescapeEn
 }
 
 void Kit8Engine::loadAssets() {
-	Common::File file;
-	if (!file.open(_gameDescription->filesDescriptions[0].fileName))
+	Common::File dataFile;
+	if (!dataFile.open(_gameDescription->filesDescriptions[0].fileName))
 		error("Unable to open 8-bit 3D Construction Kit data");
+	requireBytes(dataFile, 160);
+	uint32 signature = dataFile.readUint32BE();
+	uint32 dataOffset = 0;
+	if (signature != MKTAG('K', 'I', 'T', 'A') && signature != MKTAG('K', 'I', 'T', 'C')) {
+		byte header[128];
+		dataFile.seek(0);
+		dataFile.read(header, sizeof(header));
+		uint16 checksum = 0;
+		for (uint i = 0; i < 67; i++)
+			checksum += header[i];
+		if (checksum != READ_LE_UINT16(header + 67) || READ_LE_UINT24(header + 64) != dataFile.size() - sizeof(header))
+			error("Invalid 3D Construction Kit AMSDOS header");
+		dataOffset = sizeof(header);
+	}
+	Common::SeekableSubReadStream file(&dataFile, dataOffset, dataFile.size());
 	requireBytes(file, 160);
-	if (file.readUint32BE() != MKTAG('K', 'I', 'T', 'C') || file.readUint16LE() != file.size())
+	signature = file.readUint32BE();
+	if ((signature != MKTAG('K', 'I', 'T', 'A') && signature != 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();
@@ -126,8 +143,11 @@ void Kit8Engine::loadAssets() {
 		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);
+			// The CPC runner ignores references to absent globals.
+			if (!_areaMap.contains(255) || !_areaMap[255]->objectWithID(id))
+				continue;
+			if (entry._value->objectWithID(id))
+				error("Duplicate 8-bit 3D Construction Kit global object %u", id);
 			entry._value->addObjectFromArea(id, _areaMap[255]);
 		}
 	}
@@ -186,6 +206,12 @@ Area *Kit8Engine::loadArea(Common::SeekableReadStream &file) {
 		byte type = header[0] & 0x0f;
 		byte objectID = header[7];
 		byte size = header[8];
+		if (id == 255 && !objectID && !size) {
+			// Ciudadela Fantasma ends its globals with an unused, incomplete record.
+			warning("Ignoring incomplete 8-bit 3D Construction Kit global object");
+			file.seek(conditions);
+			break;
+		}
 		if (size < 9 || start + size > conditions)
 			error("Invalid 8-bit 3D Construction Kit object size");
 		if (objectID == 255) {
@@ -249,6 +275,15 @@ GeometricObject *Kit8Engine::loadGeometricObject(Common::SeekableReadStream &fil
 		colors->push_back(color & 15);
 		colors->push_back(color >> 4);
 	}
+	if (type == kCubeType) {
+		// CPC Kit stores the positive X face first.
+		SWAP((*colors)[0], (*colors)[1]);
+	} else if (GeometricObject::isPyramid(type)) {
+		// Kit stores opposite sides together; the renderer walks around the base.
+		const byte sides[] = {(*colors)[2], (*colors)[0], (*colors)[3], (*colors)[1]};
+		for (uint i = 0; i < ARRAYSIZE(sides); i++)
+			(*colors)[i] = sides[i];
+	}
 	Common::Array<float> *ordinates = nullptr;
 	if (ordinateCount) {
 		static const byte pyramidAxes[3][2] = {{1, 2}, {0, 2}, {0, 1}};
@@ -304,6 +339,8 @@ void Kit8Engine::gotoArea(uint16 areaID, int entranceID) {
 	memcpy(_palette, data.palette, sizeof(_palette));
 	for (byte id : data.globals) {
 		Object *object = _currentArea->objectWithID(id);
+		if (!object)
+			continue;
 		object->restore();
 		object->makeVisible();
 	}
diff --git a/engines/freescape/games/3dck/8bit.h b/engines/freescape/games/3dck/8bit.h
index 0bb35b8c734..a0e55c56b67 100644
--- a/engines/freescape/games/3dck/8bit.h
+++ b/engines/freescape/games/3dck/8bit.h
@@ -104,6 +104,7 @@ private:
 	byte _colorPatterns[15][4] = {};
 	byte _instruments[8][6] = {};
 	byte _textColor = 7, _movementMode = 1;
+	bool _textOutputEnabled = false;
 	byte _climbHeight = 0, _fallHeight = 0, _walkSpeed = 0, _activationRange = 0;
 	byte _shotObject = 0, _hitObject = 0, _activatedObject = 0;
 	bool _fallen = false, _crushed = false, _crossVisible = true;
diff --git a/engines/freescape/games/3dck/8bitUI.cpp b/engines/freescape/games/3dck/8bitUI.cpp
index 06a87a06ca5..a64ea3eeada 100644
--- a/engines/freescape/games/3dck/8bitUI.cpp
+++ b/engines/freescape/games/3dck/8bitUI.cpp
@@ -33,12 +33,14 @@ static const byte kCPCInks[27] = {
 };
 
 void Kit8Engine::loadPresentation() {
+	// Shade 0 is transparent; the CPC runner indexes these patterns with shade - 1.
 	static const byte patterns[15][4] = {
+		{0x00, 0x00, 0x00, 0x00},
 		{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}
+		{0x2d, 0x87, 0x2d, 0x87}, {0x2f, 0x8f, 0x2f, 0x8f}
 	};
 	memcpy(_colorPatterns, patterns, sizeof(_colorPatterns));
 	_colorMap.resize(ARRAYSIZE(_colorPatterns));
@@ -67,7 +69,8 @@ void Kit8Engine::loadPresentation() {
 	}
 	_borderSurface.fillRect(_viewArea, 255);
 	// A saved editor data file omits the runner's font and border.
-	if (file.open("DISC.BIN") && file.size() == 25216) {
+	const char *runner = _gameDescription->filesDescriptions[1].fileName;
+	if (file.open(runner ? runner : "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) {
@@ -88,7 +91,7 @@ void Kit8Engine::applyPalette() {
 }
 
 void Kit8Engine::printText(const Common::String &text, byte x, byte y, byte color) {
-	if (x >= 40 || y >= 25)
+	if (!_textOutputEnabled || x >= 40 || y >= 25)
 		return;
 	Graphics::DosFont font;
 	byte background = ((color >> 3) & 1) | ((color >> 1) & 2);
diff --git a/engines/freescape/language/execution_3dck8.cpp b/engines/freescape/language/execution_3dck8.cpp
index 5e4e31d73cf..bd68d657b77 100644
--- a/engines/freescape/language/execution_3dck8.cpp
+++ b/engines/freescape/language/execution_3dck8.cpp
@@ -29,7 +29,6 @@ void Kit8Engine::resetScripts() {
 	memset(_kitVariables, 0, sizeof(_kitVariables));
 	_changedVariables = 0;
 	_currentKey = 255;
-	_textColor = 7;
 	_kitVariables[121] = _kitVariables[125] = 255;
 	_kitVariables[127] = 0x9c;
 	_script = ScriptState();
@@ -137,6 +136,8 @@ void Kit8Engine::updateScripts() {
 	uint budget = 4096;
 	while (budget) {
 		if (_script.stack.empty()) {
+			// The CPC runner enables text after its first initialization condition.
+			_textOutputEnabled = true;
 			if (_conditionIndex == _activeConditions->size()) {
 				if (!_globalPhase)
 					break;


Commit: 21f0ea581dceff4101034d6f15036cbc57fca5a7
    https://github.com/scummvm/scummvm/commit/21f0ea581dceff4101034d6f15036cbc57fca5a7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-08T13:33:09+02:00

Commit Message:
FREESCAPE: input/movement fixes for 3dck games

Changed paths:
    engines/freescape/area.h
    engines/freescape/freescape.cpp
    engines/freescape/freescape.h
    engines/freescape/games/3dck/8bit.cpp
    engines/freescape/games/3dck/8bit.h
    engines/freescape/games/3dck/8bitUI.cpp
    engines/freescape/movement.cpp


diff --git a/engines/freescape/area.h b/engines/freescape/area.h
index 988c476f799..5dc65ecb921 100644
--- a/engines/freescape/area.h
+++ b/engines/freescape/area.h
@@ -85,6 +85,7 @@ public:
 	void resetArea();
 	void resetAreaGroups();
 	bool isOutside();
+	bool hasDrawableObjects() const { return !_drawableObjects.empty(); }
 	bool hasActiveGroups();
 
 	Common::Array<Common::String> _conditionSources;
diff --git a/engines/freescape/freescape.cpp b/engines/freescape/freescape.cpp
index bfbed6299a5..c36689b45b8 100644
--- a/engines/freescape/freescape.cpp
+++ b/engines/freescape/freescape.cpp
@@ -749,7 +749,8 @@ void FreescapeEngine::drawFrame() {
 
 	drawBackground();
 	if (_avoidRenderingFrames == 0) { // Avoid rendering inside objects
-		_currentArea->draw(_gfx, _ticks / 10, renderPosition, _cameraFront, _roll, false, fov, aspectRatio, _nearClipPlane, farClipPlane);
+		if (_currentArea->hasDrawableObjects())
+			_currentArea->draw(_gfx, _ticks / 10, renderPosition, _cameraFront, _roll, false, fov, aspectRatio, _nearClipPlane, farClipPlane);
 		if (_gameStateControl == kFreescapeGameStatePlaying &&
 		    _currentArea->hasActiveGroups() && _ticks % 50 == 0) {
 			executeMovementConditions();
@@ -822,7 +823,7 @@ void FreescapeEngine::drawFrameStereo(int farClipPlane) {
 	_gfx->positionCamera(renderPosition, renderPosition + _cameraFront, _roll);
 
 	drawBackground();
-	if (_avoidRenderingFrames == 0)
+	if (_avoidRenderingFrames == 0 && _currentArea->hasDrawableObjects())
 		_currentArea->drawDepthLayer(_gfx, _ticks / 10, renderPosition, _cameraFront, _roll, false, Area::kRenderDepthBackground, stereoForegroundDistance, fov, aspectRatio, _nearClipPlane, farClipPlane);
 
 	for (int pass = 0; pass < 2; pass++) {
@@ -832,7 +833,7 @@ void FreescapeEngine::drawFrameStereo(int farClipPlane) {
 
 		_gfx->clearDepthBuffer();
 
-		if (_avoidRenderingFrames == 0) // Avoid rendering inside objects
+		if (_avoidRenderingFrames == 0 && _currentArea->hasDrawableObjects()) // Avoid rendering inside objects
 			_currentArea->drawDepthLayer(_gfx, _ticks / 10, renderPosition, _cameraFront, _roll, false, Area::kRenderDepthForeground, stereoForegroundDistance, fov, aspectRatio, _nearClipPlane, farClipPlane);
 
 		if (_underFireFrames > 0) {
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index d5e4775752a..384be2497de 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -406,6 +406,7 @@ public:
 	void updatePlayerMovementSmooth(float deltaTime);
 	void updatePlayerMovementClassic(float deltaTime);
 	void resolveCollisions(Math::Vector3d newPosition);
+	virtual Math::Vector3d clipPosition(const Math::Vector3d &position) const { return position; }
 	virtual void checkIfStillInArea();
 	void changePlayerHeight(int index);
 	void increaseStepSize();
diff --git a/engines/freescape/games/3dck/8bit.cpp b/engines/freescape/games/3dck/8bit.cpp
index 9e307cd8eaf..5ce87e50516 100644
--- a/engines/freescape/games/3dck/8bit.cpp
+++ b/engines/freescape/games/3dck/8bit.cpp
@@ -372,11 +372,17 @@ void Kit8Engine::setMovementMode(byte mode) {
 	_lastPosition = _position;
 }
 
-void Kit8Engine::checkIfStillInArea() {
+Math::Vector3d Kit8Engine::clipPosition(const Math::Vector3d &position) const {
 	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);
+	// Walking bounds apply to the feet, before restoring the eye height.
+	float height = _flyMode ? 0 : _playerHeight;
+	return Math::Vector3d(CLIP(position.x(), 0.0f, 4063.5f / scale),
+		CLIP(position.y() - height, 0.0f, 2015.5f / scale) + height,
+		CLIP(position.z(), 0.0f, 4063.5f / scale));
+}
+
+void Kit8Engine::checkIfStillInArea() {
+	_position = clipPosition(_position);
 }
 
 void Kit8Engine::updatePlayerMovement(float deltaTime) {
diff --git a/engines/freescape/games/3dck/8bit.h b/engines/freescape/games/3dck/8bit.h
index a0e55c56b67..1b52f8c292a 100644
--- a/engines/freescape/games/3dck/8bit.h
+++ b/engines/freescape/games/3dck/8bit.h
@@ -34,6 +34,7 @@ public:
 	void loadAssets() override;
 	void initGameState() override;
 	void gotoArea(uint16 areaID, int entranceID) override;
+	Math::Vector3d clipPosition(const Math::Vector3d &position) const override;
 	void checkIfStillInArea() override;
 	bool checkIfGameEnded() override { return false; }
 	void borderScreen() override {}
diff --git a/engines/freescape/games/3dck/8bitUI.cpp b/engines/freescape/games/3dck/8bitUI.cpp
index a64ea3eeada..861c4c22710 100644
--- a/engines/freescape/games/3dck/8bitUI.cpp
+++ b/engines/freescape/games/3dck/8bitUI.cpp
@@ -169,6 +169,8 @@ void Kit8Engine::drawUI() {
 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 (event.kbd.keycode == Common::KEYCODE_RETURN || event.kbd.keycode == Common::KEYCODE_KP_ENTER)
+			key = 13;
 		if (key >= 'a' && key <= 'z')
 			key -= 'a' - 'A';
 		if (event.type == Common::EVENT_KEYDOWN)
@@ -177,19 +179,25 @@ bool Kit8Engine::handleInput(const Common::Event &event) {
 			_currentKey = 255;
 		if (!_scriptFrameActive)
 			_kitVariables[121] = _currentKey;
-	} else if (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START) {
+	} else if (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START || event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_END) {
+		if (event.customType == kActionSkip || event.customType == kActionInfoMenu) {
+			byte key = event.customType == kActionSkip ? ' ' : 'I';
+			if (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START)
+				_currentKey = key;
+			else if (_currentKey == key)
+				_currentKey = 255;
+			if (!_scriptFrameActive)
+				_kitVariables[121] = _currentKey;
+			return true;
+		}
+		if (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_END)
+			return false;
 		switch (event.customType) {
 		case kActionShoot:
 		case kActionActivate:
 			if (!_scriptFrameActive)
 				interact(event.customType == kActionShoot);
 			return true;
-		case kActionSkip:
-			_kitVariables[121] = ' ';
-			return true;
-		case kActionInfoMenu:
-			_kitVariables[121] = 'I';
-			return true;
 		case kActionEscape:
 		case kActionChangeMode:
 			return false;
diff --git a/engines/freescape/movement.cpp b/engines/freescape/movement.cpp
index e699693df56..e52b226f2b4 100644
--- a/engines/freescape/movement.cpp
+++ b/engines/freescape/movement.cpp
@@ -31,6 +31,7 @@ namespace Freescape {
 
 void FreescapeEngine::initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *infoScreenKeyMap, const char *target) {
 	Common::Action *act;
+	const bool isKit = Common::String(_gameDescription->gameId) == "3dkit";
 
 	act = new Common::Action(Common::kStandardActionMoveUp, _("Up"));
 	act->setCustomEngineActionEvent(kActionMoveUp);
@@ -93,10 +94,19 @@ void FreescapeEngine::initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *
 	act = new Common::Action("SKIP", _("Skip"));
 	act->setCustomEngineActionEvent(kActionSkip);
 	act->addDefaultInputMapping("SPACE");
-	act->addDefaultInputMapping("RETURN");
+	if (!isKit)
+		act->addDefaultInputMapping("RETURN");
 	act->addDefaultInputMapping("JOY_X");
 	engineKeyMap->addAction(act);
 
+	if (isKit) {
+		act = new Common::Action("RETURN", _("Return"));
+		act->setKeyEvent(Common::KeyState(Common::KEYCODE_RETURN, 13));
+		act->addDefaultInputMapping("RETURN");
+		act->addDefaultInputMapping("KP_ENTER");
+		engineKeyMap->addAction(act);
+	}
+
 	// I18N: Toggles between cursor lock modes, switching between free cursor movement and camera/head movement.
 	act = new Common::Action("SWITCH", _("Change mode"));
 	act->setCustomEngineActionEvent(kActionChangeMode);
@@ -597,12 +607,13 @@ void FreescapeEngine::updatePlayerMovementSmooth(float deltaTime) {
 		clearGameBit(31);
 }
 
-void FreescapeEngine::resolveCollisions(Math::Vector3d const position) {
+void FreescapeEngine::resolveCollisions(Math::Vector3d position) {
 	if (_noClipMode) {
 		_position = position;
 		return;
 	}
 
+	position = clipPosition(position);
 	Math::Vector3d newPosition = position;
 	Math::Vector3d lastPosition = _lastPosition;
 
@@ -653,6 +664,7 @@ void FreescapeEngine::resolveCollisions(Math::Vector3d const position) {
 	Math::Vector3d fallStart = newPosition;   // current standing point
 	Math::Vector3d fallEnd   = fallStart;     // copy for downward probe
 	fallEnd.y() = -8192;                      // probe way down below
+	fallEnd = clipPosition(fallEnd);
 	newPosition = _currentArea->resolveCollisions(fallStart, fallEnd, _playerHeight);
 	int fallen = _lastPosition.y() - newPosition.y();
 


Commit: 3dca7164adc9dece8cb907b362979b8e0a65d30b
    https://github.com/scummvm/scummvm/commit/3dca7164adc9dece8cb907b362979b8e0a65d30b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-08T13:33:09+02:00

Commit Message:
FREESCAPE: implementation of 8bit sounds for 3dck games

Changed paths:
  A engines/freescape/sound/3dck_cpc.cpp
    engines/freescape/freescape.h
    engines/freescape/games/3dck/8bit.cpp
    engines/freescape/games/3dck/8bit.h
    engines/freescape/games/3dck/8bitUI.cpp
    engines/freescape/language/execution_3dck8.cpp
    engines/freescape/module.mk


diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index 384be2497de..b0310b1948e 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -523,7 +523,7 @@ public:
 	void waitForSounds(Sound::Type type = Sound::kTypeNormal);
 	void stopAllSounds(Sound::Type type = Sound::kTypeNormal);
 	bool isPlayingSound(Sound::Type type = Sound::kTypeNormal);
-	void playSound(int index, bool sync, Sound::Type type = Sound::kTypeNormal);
+	virtual void playSound(int index, bool sync, Sound::Type type = Sound::kTypeNormal);
 	void playWav(const Common::Path &filename);
 	void playMusic(const Common::Path &filename);
 
diff --git a/engines/freescape/games/3dck/8bit.cpp b/engines/freescape/games/3dck/8bit.cpp
index 5ce87e50516..beabffcf95c 100644
--- a/engines/freescape/games/3dck/8bit.cpp
+++ b/engines/freescape/games/3dck/8bit.cpp
@@ -41,6 +41,8 @@ Kit8Engine::Kit8Engine(OSystem *syst, const ADGameDescription *gd) : FreescapeEn
 	_playerHeightNumber = _playerHeightMaxNumber = 0;
 	_playerWidth = _playerDepth = 16;
 	_soundIndexShoot = -1;
+	_soundIndexCollide = 5;
+	_soundIndexFall = 7;
 }
 
 void Kit8Engine::loadAssets() {
@@ -154,6 +156,7 @@ void Kit8Engine::loadAssets() {
 	if (!_areaMap.contains(_startArea) || !_areaMap[_startArea]->entranceWithID(_startEntrance))
 		error("Invalid 8-bit 3D Construction Kit starting entrance");
 	loadPresentation();
+	loadSounds();
 }
 
 Common::Array<Kit8Engine::ConditionData> Kit8Engine::loadConditions(Common::SeekableReadStream &file) {
diff --git a/engines/freescape/games/3dck/8bit.h b/engines/freescape/games/3dck/8bit.h
index 1b52f8c292a..5021f42a564 100644
--- a/engines/freescape/games/3dck/8bit.h
+++ b/engines/freescape/games/3dck/8bit.h
@@ -44,6 +44,7 @@ public:
 	void updateTimeVariables() override;
 	void checkSensors() override;
 	void updateScripts() override;
+	void playSound(int index, bool sync, Sound::Type type = Sound::kTypeNormal) 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; }
@@ -67,6 +68,8 @@ private:
 	Area *loadArea(Common::SeekableReadStream &file);
 	GeometricObject *loadGeometricObject(Common::SeekableReadStream &file, const byte header[9]);
 	void loadPresentation();
+	void loadSounds();
+	void playPendingSound();
 	void applyPalette();
 	void setMovementMode(byte mode);
 	void readSystemVariables();
@@ -109,7 +112,9 @@ private:
 	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;
+	bool _timerTriggered = false, _pendingTimer = false;
+	byte _pendingSound = 0;
+	bool _soundSyncReady = false;
 	uint32 _lastTime = 0, _timerTicks = 0, _timerInterval = 0, _delayUntil = 0;
 	byte _fontData[96][8] = {};
 	bool _hasFont = false;
diff --git a/engines/freescape/games/3dck/8bitUI.cpp b/engines/freescape/games/3dck/8bitUI.cpp
index 861c4c22710..6901ad289b6 100644
--- a/engines/freescape/games/3dck/8bitUI.cpp
+++ b/engines/freescape/games/3dck/8bitUI.cpp
@@ -164,6 +164,7 @@ void Kit8Engine::drawUI() {
 		_gfx->setViewport(_fullscreenViewArea);
 		_gfx->renderCrossair(_crossairPosition);
 	}
+	playPendingSound();
 }
 
 bool Kit8Engine::handleInput(const Common::Event &event) {
@@ -231,6 +232,7 @@ void Kit8Engine::interact(bool shot) {
 		if (_kitVariables[125] != 255)
 			_kitVariables[125]--;
 		_shootingFrames = 3;
+		playSound(3, true);
 	}
 	if (!object || !object->isGeometric())
 		return;
diff --git a/engines/freescape/language/execution_3dck8.cpp b/engines/freescape/language/execution_3dck8.cpp
index bd68d657b77..75100fa28b4 100644
--- a/engines/freescape/language/execution_3dck8.cpp
+++ b/engines/freescape/language/execution_3dck8.cpp
@@ -26,6 +26,9 @@
 namespace Freescape {
 
 void Kit8Engine::resetScripts() {
+	stopAllSounds();
+	_pendingSound = 0;
+	_soundSyncReady = false;
 	memset(_kitVariables, 0, sizeof(_kitVariables));
 	_changedVariables = 0;
 	_currentKey = 255;
@@ -159,6 +162,7 @@ void Kit8Engine::updateScripts() {
 		return;
 	updateInstruments();
 	_scriptFrameActive = false;
+	_soundSyncReady = true;
 	_shotObject = _hitObject = _activatedObject = 0;
 }
 
@@ -265,6 +269,7 @@ FCLExecutionResult Kit8Engine::executeCode(ScriptState &script, uint &budget) {
 			writeSystemVariables();
 			_scriptSurface.fillRect(_viewArea, 255);
 			updateInstruments();
+			_soundSyncReady = true;
 			return kFCLPaused;
 		default:
 			error("Unsupported 8-bit 3D Construction Kit instruction %u", op);
@@ -428,10 +433,7 @@ void Kit8Engine::executeColour(const FCLInstruction &instruction) {
 }
 
 void Kit8Engine::executeSound(const FCLInstruction &instruction) {
-	if (instruction._source && !_soundWarning) {
-		warning("8-bit 3D Construction Kit sound effects are not implemented");
-		_soundWarning = true;
-	}
+	playSound(instruction._source, instruction.getType() == Token::SYNCSND);
 }
 
 bool Kit8Engine::executeObjectConditions(GeometricObject *object, bool shot, bool collided, bool activated) {
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index 4edc107e0f9..0d1c9eace2e 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -80,6 +80,7 @@ MODULE_OBJS := \
 	sound/common.o \
 	sound/3dck.o \
 	sound/3dck_adlib.o \
+	sound/3dck_cpc.o \
 	sound/cpc.o \
 	sound/dos.o \
 	sound/fx.o \
diff --git a/engines/freescape/sound/3dck_cpc.cpp b/engines/freescape/sound/3dck_cpc.cpp
new file mode 100644
index 00000000000..5f3532df717
--- /dev/null
+++ b/engines/freescape/sound/3dck_cpc.cpp
@@ -0,0 +1,226 @@
+/* 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 "audio/softsynth/ay8912.h"
+#include "common/endian.h"
+#include "common/mutex.h"
+
+#include "freescape/games/3dck/8bit.h"
+
+namespace Freescape {
+
+// Standard CPC runner bank at 0x9a9f, also used for editor data without a runner.
+static const byte kKitCPCSounds[13][16] = {
+	{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // Silence
+	{0x0f, 0x00, 0x0f, 0x00, 0x0f, 0x00, 0x00, 0x38, 0x0f, 0x0f, 0x0f, 0x01, 0x00, 0xff, 0x00, 0x1e}, // Ping
+	{0x00, 0x0c, 0x01, 0x0c, 0x02, 0x0c, 0x00, 0x38, 0x0f, 0x0f, 0x0f, 0x00, 0x10, 0x00, 0x00, 0x40}, // Buzz
+	{0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x0f, 0x0f, 0x0f, 0x00, 0x14, 0x00, 0x00, 0x10}, // Fire
+	{0x40, 0x01, 0x80, 0x01, 0xc0, 0x01, 0x00, 0x38, 0x0f, 0x0f, 0x0f, 0x01, 0xa0, 0x00, 0x00, 0x10}, // Activate
+	{0xc0, 0x00, 0x00, 0x09, 0x00, 0x07, 0x10, 0x38, 0x0f, 0x0f, 0x0f, 0x00, 0x7f, 0x00, 0xa0, 0x0a}, // Bump
+	{0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x0f, 0x0f, 0x0f, 0x01, 0x08, 0xff, 0x01, 0x1c}, // Fall
+	{0x00, 0x10, 0x10, 0x10, 0x20, 0x10, 0x00, 0x38, 0x0f, 0x0f, 0x0f, 0x00, 0x04, 0x00, 0x00, 0x90}, // Fail
+	{0x00, 0x08, 0x10, 0x08, 0x20, 0x08, 0x00, 0x38, 0x0f, 0x0f, 0x0f, 0x00, 0xf0, 0x00, 0x00, 0x80}, // Bonus 1
+	{0x00, 0x08, 0x40, 0x08, 0x80, 0x08, 0x00, 0x30, 0x0f, 0x0f, 0x0f, 0x00, 0xe0, 0x00, 0x04, 0x41}, // Bonus 2
+	{0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x38, 0x0f, 0x0f, 0x0f, 0x00, 0xf0, 0x00, 0xff, 0x34}, // Bonus 3
+	{0x00, 0x08, 0x10, 0x08, 0x20, 0x08, 0x20, 0x36, 0x0f, 0x0f, 0x0f, 0x00, 0x01, 0x00, 0x01, 0x20}, // Door open
+	{0x00, 0x08, 0x10, 0x08, 0x20, 0x08, 0x20, 0x36, 0x0f, 0x0f, 0x0f, 0x00, 0xff, 0x00, 0xff, 0x20}  // Door close
+};
+
+class KitCPCSound : public Sound, public Audio::AudioStream {
+public:
+	KitCPCSound(Audio::Mixer *mixer, const byte *data);
+	~KitCPCSound() override { _mixer->stopHandle(_handle); }
+
+	void playSound(int index, Type type) override;
+	void stopSound(Type type) override;
+	bool isPlayingSound(Type type) const override;
+	bool isSoundAvailable(int index) const override { return index >= 0 && index < ARRAYSIZE(_effects); }
+	int readBuffer(int16 *buffer, int samples) override;
+	int getRate() const override { return _psg.getRate(); }
+	bool isStereo() const override { return true; }
+	bool endOfData() const override { return false; }
+
+private:
+	struct Effect {
+		uint16 tone[3];
+		byte noise, mixer, volume[3], delay;
+		int8 toneStep, volumeStep, noiseStep;
+		byte duration;
+	};
+	void tick();
+	void silence();
+	void writeRegisters();
+
+	Effect _effects[13], _effect = {};
+	Audio::Mixer *_mixer;
+	Audio::SoundHandle _handle;
+	Audio::AY8912Stream _psg;
+	mutable Common::Mutex _mutex;
+	Type _type = kTypeNormal;
+	bool _active = false;
+	byte _delay = 0;
+	int _samplesLeft = 0;
+};
+
+KitCPCSound::KitCPCSound(Audio::Mixer *mixer, const byte *data) : _mixer(mixer), _psg(62500, 1000000) {
+	// An integral AY clock divisor keeps pitch exact with AY8912Stream.
+	for (Effect &effect : _effects) {
+		for (int channel = 0; channel < 3; channel++) {
+			effect.tone[channel] = READ_LE_UINT16(data + 2 * channel);
+			effect.volume[channel] = data[8 + channel];
+		}
+		effect.noise = data[6];
+		effect.mixer = data[7];
+		effect.delay = data[11];
+		effect.toneStep = int8(data[12]);
+		effect.volumeStep = int8(data[13]);
+		effect.noiseStep = int8(data[14]);
+		effect.duration = data[15];
+		data += 16;
+	}
+	silence();
+	_mixer->playStream(Audio::Mixer::kSFXSoundType, &_handle, this, -1,
+		kFreescapeDefaultVolume, 0, DisposeAfterUse::NO);
+}
+
+void KitCPCSound::silence() {
+	_active = false;
+	_psg.setReg(7, 0x3f);
+	for (int channel = 0; channel < 3; channel++)
+		_psg.setReg(8 + channel, 0);
+}
+
+void KitCPCSound::playSound(int index, Type type) {
+	if (!isSoundAvailable(index))
+		return;
+	Common::StackLock lock(_mutex);
+	_effect = _effects[index];
+	_type = type;
+	_delay = 0;
+	_active = _effect.mixer != 0x3f;
+	if (!_active)
+		silence();
+}
+
+void KitCPCSound::stopSound(Type type) {
+	Common::StackLock lock(_mutex);
+	if (type == kTypeNormal || type == _type)
+		silence();
+}
+
+bool KitCPCSound::isPlayingSound(Type type) const {
+	Common::StackLock lock(_mutex);
+	return _active && (type == kTypeNormal || type == _type);
+}
+
+void KitCPCSound::writeRegisters() {
+	for (int channel = 0; channel < 3; channel++) {
+		_psg.setReg(2 * channel, _effect.tone[channel]);
+		_psg.setReg(2 * channel + 1, _effect.tone[channel] >> 8);
+		_psg.setReg(8 + channel, _effect.volume[channel]);
+	}
+	_psg.setReg(6, _effect.noise);
+	_psg.setReg(7, _effect.mixer);
+	// The runner writes its modulation bytes to envelope registers too.
+	_psg.setReg(11, _effect.delay);
+	_psg.setReg(12, _effect.toneStep);
+	_psg.setReg(13, _effect.volumeStep);
+}
+
+void KitCPCSound::tick() {
+	if (!_active)
+		return;
+	// CPC routine 0x9a14 decrements duration before the modulation delay.
+	if (!--_effect.duration) {
+		silence();
+		return;
+	}
+	if (_delay) {
+		--_delay;
+		return;
+	}
+	_delay = _effect.delay;
+	writeRegisters();
+	for (int channel = 0; channel < 3; channel++) {
+		_effect.tone[channel] += _effect.toneStep;
+		_effect.volume[channel] += _effect.volumeStep;
+	}
+	_effect.noise += _effect.noiseStep;
+}
+
+int KitCPCSound::readBuffer(int16 *buffer, int samples) {
+	Common::StackLock lock(_mutex);
+	assert(!(samples & 1));
+	int done = 0;
+	while (done < samples) {
+		if (!_samplesLeft) {
+			tick();
+			_samplesLeft = 2 * getRate() / 50;
+		}
+		int count = MIN(samples - done, _samplesLeft);
+		_psg.readBuffer(buffer + done, count);
+		done += count;
+		_samplesLeft -= count;
+	}
+	return samples;
+}
+
+void Kit8Engine::loadSounds() {
+	byte data[sizeof(kKitCPCSounds)];
+	memcpy(data, kKitCPCSounds, sizeof(data));
+	Common::File file;
+	const char *runner = _gameDescription->filesDescriptions[1].fileName;
+	if (file.open(runner ? runner : "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 + 0x9a9f - 0x3e00);
+			if (file.read(data, sizeof(data)) != sizeof(data))
+				error("Truncated 3D Construction Kit CPC sound bank");
+		}
+	}
+	_sound = new KitCPCSound(_mixer, data);
+}
+
+void Kit8Engine::playSound(int index, bool sync, Sound::Type type) {
+	if (!_sound || !_sound->isSoundAvailable(index))
+		return;
+	if (type == Sound::kTypeMovement) {
+		// A bump must not replace an already pending shot or scripted effect.
+		if (index != _soundIndexCollide || !_pendingSound)
+			_pendingSound = index;
+	} else if (sync) {
+		// SYNCSND 0 cancels the pending effect in the CPC runner.
+		_pendingSound = index;
+	} else
+		_sound->playSound(index, type);
+}
+
+void Kit8Engine::playPendingSound() {
+	if (!_soundSyncReady)
+		return;
+	if (_pendingSound && _sound)
+		_sound->playSound(_pendingSound, Sound::kTypeNormal);
+	_pendingSound = 0;
+	_soundSyncReady = false;
+}
+
+} // namespace Freescape


Commit: 3cb304bda7e17a996a3d648e18b0c7f14de07dbe
    https://github.com/scummvm/scummvm/commit/3cb304bda7e17a996a3d648e18b0c7f14de07dbe
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-08T13:33:09+02:00

Commit Message:
FREESCAPE: initial support for zx spectrum 3dck games

Changed paths:
  A engines/freescape/games/3dck/zx.cpp
  A engines/freescape/sound/3dck_zx.cpp
    engines/freescape/detection.cpp
    engines/freescape/games/3dck/8bit.cpp
    engines/freescape/games/3dck/8bit.h
    engines/freescape/games/3dck/8bitUI.cpp
    engines/freescape/language/execution_3dck8.cpp
    engines/freescape/metaengine.cpp
    engines/freescape/module.mk
    engines/freescape/sound/3dck_cpc.cpp
    engines/freescape/zx_tape.cpp


diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index 602ba9531c8..911e369c072 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -1156,6 +1156,16 @@ const ADGameDescription gameDescriptions[] = {
 		GUIO3(GUIO_NOMIDI, GUIO_RENDEREGA, GAMEOPTION_WASD_CONTROLS)
 	},
 	// 3D Construction Kit games
+	{
+		"3dkit",
+		"Dead by Dawn",
+		AD_ENTRY2s("3dkit.zx.data", "ec7ff343b0ba9f2e685bde2fb8f6d8d8", 13242,
+			"3dkit.zx.code", "1ef359f328832b77f0adead4d292f21f", 24575),
+		Common::EN_ANY,
+		Common::kPlatformZX,
+		ADGF_UNSTABLE,
+		GUIO2(GUIO_NOMIDI, GUIO_RENDERZX)
+	},
 	{
 		"3dkit",
 		"A Chance in Hell",
@@ -1507,12 +1517,13 @@ ADDetectedGames FreescapeMetaEngineDetection::detectZxTapeGames(const Common::FS
 		Common::File file;
 		Common::String name = node.getName();
 		if ((name.hasSuffixIgnoreCase(".tap") || name.hasSuffixIgnoreCase(".tzx")) && file.open(node)) {
+			Freescape::ZxTapeFileList files;
+			// Decode sampled recordings once, then match each game's virtual filenames.
+			if (!Freescape::extractZxSpectrumTapeFiles(file, "", files))
+				continue;
 			for (const ADGameDescription *desc = Freescape::gameDescriptions; desc->gameId; ++desc) {
 				if (!(desc->flags & skipADFlags) && desc->platform == Common::kPlatformZX) {
-					file.seek(0);
-					Freescape::ZxTapeFileList files;
-					if (Freescape::extractZxSpectrumTapeFiles(file, desc->gameId, files) &&
-							Freescape::matchZxSpectrumTapeFiles(files, *desc, _md5Bytes))
+					if (Freescape::matchZxSpectrumTapeFiles(files, *desc, _md5Bytes))
 						detectedGames.push_back(ADDetectedGame(desc));
 				}
 			}
diff --git a/engines/freescape/games/3dck/8bit.cpp b/engines/freescape/games/3dck/8bit.cpp
index beabffcf95c..4b95216f8e9 100644
--- a/engines/freescape/games/3dck/8bit.cpp
+++ b/engines/freescape/games/3dck/8bit.cpp
@@ -35,8 +35,8 @@ static void requireBytes(Common::SeekableReadStream &file, uint32 size) {
 }
 
 Kit8Engine::Kit8Engine(OSystem *syst, const ADGameDescription *gd) : FreescapeEngine(syst, gd) {
-	_screenW = 320;
-	_screenH = 200;
+	_screenW = isSpectrum() ? 256 : 320;
+	_screenH = isSpectrum() ? 192 : 200;
 	_fullscreenViewArea = Common::Rect(_screenW, _screenH);
 	_playerHeightNumber = _playerHeightMaxNumber = 0;
 	_playerWidth = _playerDepth = 16;
@@ -52,7 +52,7 @@ void Kit8Engine::loadAssets() {
 	requireBytes(dataFile, 160);
 	uint32 signature = dataFile.readUint32BE();
 	uint32 dataOffset = 0;
-	if (signature != MKTAG('K', 'I', 'T', 'A') && signature != MKTAG('K', 'I', 'T', 'C')) {
+	if (isCPC() && signature != MKTAG('K', 'I', 'T', 'A') && signature != MKTAG('K', 'I', 'T', 'C')) {
 		byte header[128];
 		dataFile.seek(0);
 		dataFile.read(header, sizeof(header));
@@ -66,7 +66,9 @@ void Kit8Engine::loadAssets() {
 	Common::SeekableSubReadStream file(&dataFile, dataOffset, dataFile.size());
 	requireBytes(file, 160);
 	signature = file.readUint32BE();
-	if ((signature != MKTAG('K', 'I', 'T', 'A') && signature != MKTAG('K', 'I', 'T', 'C')) || file.readUint16LE() != file.size())
+	bool validSignature = isSpectrum() ? signature == MKTAG('K', 'I', 'T', 'S') :
+		signature == MKTAG('K', 'I', 'T', 'A') || signature == MKTAG('K', 'I', 'T', 'C');
+	if (!validSignature || file.readUint16LE() != file.size())
 		error("Unsupported 8-bit 3D Construction Kit data format");
 	uint16 procedures = file.readUint16LE();
 	uint16 conditions = file.readUint16LE();
@@ -87,7 +89,7 @@ void Kit8Engine::loadAssets() {
 	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.
+	// 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)
@@ -193,7 +195,7 @@ Area *Kit8Engine::loadArea(Common::SeekableReadStream &file) {
 	AreaData &data = _areaData[id];
 	for (uint i = 0; i < 4; i++) {
 		data.palette[i] = file.readByte();
-		if (data.palette[i] > 26)
+		if (data.palette[i] > (isSpectrum() ? (i == 2 ? 1 : 7) : 26))
 			error("Invalid 8-bit 3D Construction Kit palette");
 	}
 	byte scale = file.readByte();
@@ -389,18 +391,32 @@ void Kit8Engine::checkIfStillInArea() {
 }
 
 void Kit8Engine::updatePlayerMovement(float deltaTime) {
-	if (_scriptFrameActive || _initialScriptPending)
+	if (!isFrameReady() || _scriptFrameActive || _initialScriptPending || (isSpectrum() && isPlayingSound()))
 		return;
 	Math::Vector3d front = _cameraFront;
 	if (_movementMode == 3)
 		_cameraFront = directionToVector(0, _yaw, false);
-	FreescapeEngine::updatePlayerMovement(deltaTime);
+	FreescapeEngine::updatePlayerMovement(kFrameDuration / 1000.0f);
 	_cameraFront = front;
 }
 
+void Kit8Engine::pauseEngineIntern(bool pause) {
+	uint32 now = g_system->getMillis();
+	if (pause)
+		_pauseStartTime = now;
+	else {
+		uint32 elapsed = now - _pauseStartTime;
+		_lastTime += elapsed;
+		_nextFrameTime += elapsed;
+		if (_delayUntil)
+			_delayUntil += elapsed;
+	}
+	FreescapeEngine::pauseEngineIntern(pause);
+}
+
 void Kit8Engine::checkSensors() {
 	// TODO: sensor firing.
-	if (_scriptFrameActive || !_currentArea)
+	if (!isFrameReady() || _scriptFrameActive || !_currentArea)
 		return;
 	for (auto *object : _sensors) {
 		Sensor *sensor = static_cast<Sensor *>(object);
diff --git a/engines/freescape/games/3dck/8bit.h b/engines/freescape/games/3dck/8bit.h
index 5021f42a564..24e07b8e590 100644
--- a/engines/freescape/games/3dck/8bit.h
+++ b/engines/freescape/games/3dck/8bit.h
@@ -39,6 +39,7 @@ public:
 	bool checkIfGameEnded() override { return false; }
 	void borderScreen() override {}
 	void drawUI() override;
+	void initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *infoScreenKeyMap, const char *target) override;
 	bool handleInput(const Common::Event &event) override;
 	void updatePlayerMovement(float deltaTime) override;
 	void updateTimeVariables() override;
@@ -52,6 +53,7 @@ public:
 
 private:
 	typedef FCLKit8ExecutionState ScriptState;
+	static const uint kFrameDuration = 100;
 
 	struct ConditionData {
 		byte id;
@@ -68,13 +70,19 @@ private:
 	Area *loadArea(Common::SeekableReadStream &file);
 	GeometricObject *loadGeometricObject(Common::SeekableReadStream &file, const byte header[9]);
 	void loadPresentation();
+	void loadPresentationZX();
 	void loadSounds();
+	void loadSoundsZX();
 	void playPendingSound();
 	void applyPalette();
+	void applyPaletteZX();
+	void setAttributesZX(const Common::Rect &rect, byte color);
 	void setMovementMode(byte mode);
 	void readSystemVariables();
 	void writeSystemVariables();
 	void resetScripts();
+	bool isFrameReady() const { return int32(_lastTime - _nextFrameTime) >= 0; }
+	void pauseEngineIntern(bool pause) override;
 	void beginScriptFrame();
 	void startScript(ScriptState &script, const FCLInstructionVector &code);
 	FCLExecutionResult executeCode(ScriptState &script, uint &budget);
@@ -101,11 +109,14 @@ private:
 	const Common::Array<ConditionData> *_activeConditions = nullptr;
 	uint _conditionIndex = 0;
 	bool _initialScriptPending = true, _scriptFrameActive = false, _globalPhase = false;
+	bool _redrawPending = false;
 	byte _kitVariables[128] = {};
 	uint16 _changedVariables = 0;
 	byte _currentKey = 255;
 	byte _palette[4] = {};
 	byte _colorPatterns[15][4] = {};
+	byte _zxPalette[16 * 3] = {};
+	byte _borderAttributes[32 * 24] = {}, _attributes[32 * 24] = {};
 	byte _instruments[8][6] = {};
 	byte _textColor = 7, _movementMode = 1;
 	bool _textOutputEnabled = false;
@@ -116,6 +127,7 @@ private:
 	byte _pendingSound = 0;
 	bool _soundSyncReady = false;
 	uint32 _lastTime = 0, _timerTicks = 0, _timerInterval = 0, _delayUntil = 0;
+	uint32 _nextFrameTime = 0, _pauseStartTime = 0;
 	byte _fontData[96][8] = {};
 	bool _hasFont = false;
 	Graphics::ManagedSurface _scriptSurface, _overlaySurface, _borderSurface;
diff --git a/engines/freescape/games/3dck/8bitUI.cpp b/engines/freescape/games/3dck/8bitUI.cpp
index 6901ad289b6..f37c522ddb6 100644
--- a/engines/freescape/games/3dck/8bitUI.cpp
+++ b/engines/freescape/games/3dck/8bitUI.cpp
@@ -33,6 +33,19 @@ static const byte kCPCInks[27] = {
 };
 
 void Kit8Engine::loadPresentation() {
+	_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);
+	_colorMap.resize(ARRAYSIZE(_colorPatterns));
+	for (uint i = 0; i < _colorMap.size(); i++)
+		_colorMap[i] = _colorPatterns[i];
+	if (isSpectrum()) {
+		loadPresentationZX();
+		return;
+	}
 	// Shade 0 is transparent; the CPC runner indexes these patterns with shade - 1.
 	static const byte patterns[15][4] = {
 		{0x00, 0x00, 0x00, 0x00},
@@ -43,15 +56,6 @@ void Kit8Engine::loadPresentation() {
 		{0x2d, 0x87, 0x2d, 0x87}, {0x2f, 0x8f, 0x2f, 0x8f}
 	};
 	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")) {
@@ -81,6 +85,10 @@ void Kit8Engine::loadPresentation() {
 }
 
 void Kit8Engine::applyPalette() {
+	if (isSpectrum()) {
+		applyPaletteZX();
+		return;
+	}
 	_gfx->_fourColorBackground = kCPCInks[_palette[0]];
 	_gfx->_underFireBackgroundColor = kCPCInks[_palette[2]];
 	_gfx->_paperColor = kCPCInks[_palette[1]];
@@ -91,22 +99,25 @@ void Kit8Engine::applyPalette() {
 }
 
 void Kit8Engine::printText(const Common::String &text, byte x, byte y, byte color) {
-	if (!_textOutputEnabled || x >= 40 || y >= 25)
+	if (!_textOutputEnabled || x >= _screenW / 8 || y >= _screenH / 8)
 		return;
 	Graphics::DosFont font;
-	byte background = ((color >> 3) & 1) | ((color >> 1) & 2);
-	for (uint i = 0; i < text.size() && x < 40; i++, x++) {
+	byte foreground = isSpectrum() ? 1 : color & 3;
+	byte background = isSpectrum() ? 0 : ((color >> 3) & 1) | ((color >> 1) & 2);
+	for (uint i = 0; i < text.size() && x < _screenW / 8; i++, x++) {
 		byte chr = text[i];
+		if (isSpectrum())
+			_attributes[y * 32 + x] = color;
 		_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);
+						_scriptSurface.setPixel(8 * x + col, 8 * y + row, foreground);
 				}
 			}
 		} else
-			font.drawChar(_scriptSurface.surfacePtr(), chr, 8 * x, 8 * y, color & 3);
+			font.drawChar(_scriptSurface.surfacePtr(), chr, 8 * x, 8 * y, foreground);
 	}
 }
 
@@ -119,36 +130,42 @@ 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)
+		if (!type || type > 3 || x >= _screenW / 8 || y >= _screenH / 8 || !length)
 			continue;
 		uint16 value = _kitVariables[variable];
 		if (type == 1) {
-			if (length > 5 || x + length > 40)
+			if (length > 5 || x + length > _screenW / 8)
 				continue;
 			if (length > 3)
 				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))
+			if ((type == 2 && x + length > _screenW / 8) || (type == 3 && y + length > _screenH / 8))
 				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);
+			if (isSpectrum())
+				setAttributesZX(bar, color);
+			_scriptSurface.fillRect(bar, isSpectrum() ? 0 : (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);
+				_scriptSurface.fillRect(bar, isSpectrum() ? 1 : color & 3);
 		}
 	}
 }
 
 void Kit8Engine::drawUI() {
-	uint32 colors[4];
-	for (uint i = 0; i < 4; i++) {
+	uint32 colors[16];
+	bool flash = (g_system->getMillis() / 320) & 1;
+	for (uint i = 0; i < (isSpectrum() ? 16 : 4); i++) {
 		byte r, g, b;
-		_gfx->selectColorFromFourColorPalette(i, r, g, b);
+		if (isSpectrum())
+			_gfx->readFromPalette(i, r, g, b);
+		else
+			_gfx->selectColorFromFourColorPalette(i, r, g, b);
 		colors[i] = _overlaySurface.format.ARGBToColor(255, r, g, b);
 	}
 	for (int y = 0; y < _screenH; y++) {
@@ -156,6 +173,12 @@ void Kit8Engine::drawUI() {
 			byte pen = _scriptSurface.getPixel(x, y);
 			if (pen == 255)
 				pen = _borderSurface.getPixel(x, y);
+			if (isSpectrum() && pen != 255) {
+				byte attr = _attributes[(y / 8) * 32 + x / 8];
+				if ((attr & 128) && flash)
+					pen ^= 1;
+				pen = ((attr >> 3) & 8) | (pen ? attr & 7 : (attr >> 3) & 7);
+			}
 			_overlaySurface.setPixel(x, y, pen == 255 ? 0 : colors[pen]);
 		}
 	}
@@ -169,6 +192,11 @@ void Kit8Engine::drawUI() {
 
 bool Kit8Engine::handleInput(const Common::Event &event) {
 	if (event.type == Common::EVENT_KEYDOWN || event.type == Common::EVENT_KEYUP) {
+		if (isSpectrum() && event.kbd.keycode == Common::KEYCODE_BREAK) {
+			if (event.type == Common::EVENT_KEYDOWN)
+				_gameStateControl = kFreescapeGameStateRestart;
+			return true;
+		}
 		byte key = event.kbd.ascii < 128 ? event.kbd.ascii : 255;
 		if (event.kbd.keycode == Common::KEYCODE_RETURN || event.kbd.keycode == Common::KEYCODE_KP_ENTER)
 			key = 13;
@@ -181,11 +209,20 @@ bool Kit8Engine::handleInput(const Common::Event &event) {
 		if (!_scriptFrameActive)
 			_kitVariables[121] = _currentKey;
 	} else if (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START || event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_END) {
+		if (isSpectrum() && (event.customType == kActionRiseOrFlyUp || event.customType == kActionLowerOrFlyDown)) {
+			if (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START && !_scriptFrameActive && !isPlayingSound())
+				setMovementMode(event.customType == kActionRiseOrFlyUp ? 1 : 0);
+			return true;
+		}
 		if (event.customType == kActionSkip || event.customType == kActionInfoMenu) {
 			byte key = event.customType == kActionSkip ? ' ' : 'I';
-			if (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START)
+			if (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START) {
 				_currentKey = key;
-			else if (_currentKey == key)
+				if (isSpectrum() && event.customType == kActionInfoMenu && !_scriptFrameActive) {
+					_pitch = _roll = 0;
+					updateCamera();
+				}
+			} else if (_currentKey == key)
 				_currentKey = 255;
 			if (!_scriptFrameActive)
 				_kitVariables[121] = _currentKey;
@@ -220,7 +257,7 @@ bool Kit8Engine::handleInput(const Common::Event &event) {
 }
 
 void Kit8Engine::interact(bool shot) {
-	if (!_viewArea.contains(_crossairPosition) || (shot && !_kitVariables[125]))
+	if (!_viewArea.contains(_crossairPosition) || (shot && !_kitVariables[125]) || (isSpectrum() && isPlayingSound()))
 		return;
 	float x = 2.0f * (_crossairPosition.x - _viewArea.left) / _viewArea.width() - 1;
 	float y = 1 - 2.0f * (_crossairPosition.y - _viewArea.top) / _viewArea.height();
diff --git a/engines/freescape/games/3dck/zx.cpp b/engines/freescape/games/3dck/zx.cpp
new file mode 100644
index 00000000000..6154ce4c56e
--- /dev/null
+++ b/engines/freescape/games/3dck/zx.cpp
@@ -0,0 +1,136 @@
+/* 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 "backends/keymapper/action.h"
+#include "backends/keymapper/keymap.h"
+#include "common/endian.h"
+#include "common/translation.h"
+#include "image/scr.h"
+
+#include "freescape/games/3dck/8bit.h"
+
+namespace Freescape {
+
+void Kit8Engine::initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *infoScreenKeyMap, const char *target) {
+	FreescapeEngine::initKeymaps(engineKeyMap, infoScreenKeyMap, target);
+	if (!isSpectrum())
+		return;
+	for (Common::Action *action : engineKeyMap->getActions()) {
+		switch (action->event.customType) {
+		case kActionShoot: action->addDefaultInputMapping("b"); break;
+		case kActionMoveLeft: action->addDefaultInputMapping("z"); break;
+		case kActionMoveRight: action->addDefaultInputMapping("x"); break;
+		case kActionInfoMenu: action->description = _("Center view"); break;
+		default: break;
+		}
+	}
+	const struct {
+		const char *id, *label, *key;
+		uint action;
+	} controls[] = {
+		{"ROTL", _s("Rotate left"), "q", kActionRotateLeft},
+		{"ROTR", _s("Rotate right"), "w", kActionRotateRight},
+		{"ACTIVATE", _s("Activate"), "a", kActionActivate},
+		{"RISE", _s("Stand up"), "r", kActionRiseOrFlyUp},
+		{"LOWER", _s("Crouch"), "f", kActionLowerOrFlyDown}
+	};
+	for (const auto &control : controls) {
+		Common::Action *action = new Common::Action(control.id, _(control.label));
+		action->setCustomEngineActionEvent(control.action);
+		action->addDefaultInputMapping(control.key);
+		engineKeyMap->addAction(action);
+	}
+	Common::Action *act = new Common::Action("BREAK", _("Restart"));
+	act->setKeyEvent(Common::KeyState(Common::KEYCODE_BREAK, 27));
+	act->addDefaultInputMapping("S+SPACE");
+	act->addDefaultInputMapping("BREAK");
+	engineKeyMap->addAction(act);
+}
+
+void Kit8Engine::loadPresentationZX() {
+	Common::File file;
+	Image::ScrDecoder decoder;
+	memcpy(_zxPalette, decoder.getPalette().data(), sizeof(_zxPalette));
+	if (file.open("3dkit.zx.title")) {
+		if (file.size() != 6912 || !decoder.loadStream(file))
+			error("Invalid 3D Construction Kit Spectrum title");
+		_title = new Graphics::ManagedSurface;
+		_title->copyFrom(*decoder.getSurface());
+		_title->convertToInPlace(_gfx->_texturePixelFormat, _zxPalette, 16);
+		file.close();
+	}
+	if (!file.open("3dkit.zx.border") || file.size() != 6912)
+		error("Missing 3D Construction Kit Spectrum border");
+	byte bitmap[6144];
+	if (file.read(bitmap, sizeof(bitmap)) != sizeof(bitmap) ||
+			file.read(_borderAttributes, sizeof(_borderAttributes)) != sizeof(_borderAttributes))
+		error("Truncated 3D Construction Kit Spectrum border");
+	memcpy(_attributes, _borderAttributes, sizeof(_attributes));
+	for (int y = 0; y < _screenH; ++y) {
+		for (int x = 0; x < _screenW; ++x) {
+			uint address = ((y & 0xc0) << 5) | ((y & 7) << 8) | ((y & 0x38) << 2) | (x >> 3);
+			_borderSurface.setPixel(x, y, (bitmap[address] >> (7 - (x & 7))) & 1);
+		}
+	}
+	_borderSurface.fillRect(_viewArea, 255);
+	file.close();
+
+	if (!file.open("3dkit.zx.code") || file.size() < 0x5eb7)
+		error("Missing 3D Construction Kit Spectrum runner");
+	file.seek(0x5e7b);
+	if (file.read(_colorPatterns, sizeof(_colorPatterns)) != sizeof(_colorPatterns))
+		error("Truncated 3D Construction Kit Spectrum shades");
+	file.seek(0x1690);
+	uint16 font = file.readUint16LE();
+	if (font >= 0xa000 && font - 0xa000 + sizeof(_fontData) <= uint32(file.size())) {
+		file.seek(font - 0xa000);
+		_hasFont = file.read(_fontData, sizeof(_fontData)) == sizeof(_fontData);
+	} else {
+		// Tapes omit the ROM font; the runner also has a compact character set.
+		file.seek(0x220);
+		for (uint chr = 0; chr < 64; ++chr) {
+			for (uint row = 1; row <= 6; ++row)
+				_fontData[chr][row] = file.readByte() >> 1;
+		}
+		for (uint chr = 64; chr < 96; ++chr)
+			memcpy(_fontData[chr], _fontData[chr - 32], 8);
+		_hasFont = !file.err();
+	}
+}
+
+void Kit8Engine::setAttributesZX(const Common::Rect &rect, byte color) {
+	for (int y = rect.top / 8; y < (rect.bottom + 7) / 8; ++y) {
+		for (int x = rect.left / 8; x < (rect.right + 7) / 8; ++x)
+			_attributes[y * 32 + x] = color;
+	}
+}
+
+void Kit8Engine::applyPaletteZX() {
+	_gfx->_palette = _zxPalette;
+	_gfx->_inkColor = _palette[0] | (_palette[2] << 3);
+	_gfx->_paperColor = _palette[1] | (_palette[2] << 3);
+	_currentArea->_usualBackgroundColor = 1;
+	_currentArea->_skyColor = 1;
+	_currentArea->_underFireBackgroundColor = 2;
+	setAttributesZX(_viewArea, _palette[0] | (_palette[1] << 3) | (_palette[2] << 6));
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/language/execution_3dck8.cpp b/engines/freescape/language/execution_3dck8.cpp
index 75100fa28b4..a12583e29ee 100644
--- a/engines/freescape/language/execution_3dck8.cpp
+++ b/engines/freescape/language/execution_3dck8.cpp
@@ -38,12 +38,15 @@ void Kit8Engine::resetScripts() {
 	_activeConditions = nullptr;
 	_initialScriptPending = true;
 	_scriptFrameActive = false;
+	_redrawPending = false;
 	_shotObject = _hitObject = _activatedObject = 0;
 	_fallen = _crushed = _pendingTimer = _timerTriggered = false;
 	_crossVisible = true;
 	_timerTicks = _timerInterval = _delayUntil = 0;
 	_lastTime = g_system->getMillis();
+	_nextFrameTime = _lastTime;
 	_scriptSurface.fillRect(_fullscreenViewArea, 255);
+	memcpy(_attributes, _borderAttributes, sizeof(_attributes));
 }
 
 void Kit8Engine::readSystemVariables() {
@@ -85,6 +88,11 @@ void Kit8Engine::writeSystemVariables() {
 
 void Kit8Engine::updateTimeVariables() {
 	uint32 now = g_system->getMillis();
+	// The Spectrum beeper disables interrupts until the effect ends.
+	if (isSpectrum() && isPlayingSound()) {
+		_lastTime = now;
+		return;
+	}
 	uint32 elapsed = (now - _lastTime) / 20;
 	_lastTime += 20 * elapsed;
 	uint16 counter = (_kitVariables[122] | (_kitVariables[123] << 8)) + elapsed;
@@ -127,10 +135,15 @@ void Kit8Engine::beginScriptFrame() {
 }
 
 void Kit8Engine::updateScripts() {
+	if (isSpectrum() && isPlayingSound())
+		return;
 	_fallen |= _hasFallen;
 	_crushed |= _playerWasCrushed;
 	_hasFallen = _playerWasCrushed = false;
 	_avoidRenderingFrames = 0;
+	if ((!_scriptFrameActive || _redrawPending) && !isFrameReady())
+		return;
+	_redrawPending = false;
 	if (_delayUntil && int32(_delayUntil - g_system->getMillis()) > 0)
 		return;
 	_delayUntil = 0;
@@ -162,6 +175,8 @@ void Kit8Engine::updateScripts() {
 		return;
 	updateInstruments();
 	_scriptFrameActive = false;
+	// Approximate 8-bit rendering time using Freescape's movement cadence.
+	_nextFrameTime = _lastTime + kFrameDuration;
 	_soundSyncReady = true;
 	_shotObject = _hitObject = _activatedObject = 0;
 }
@@ -255,6 +270,8 @@ FCLExecutionResult Kit8Engine::executeCode(ScriptState &script, uint &budget) {
 		case Token::SOUND:
 		case Token::SYNCSND:
 			executeSound(instruction);
+			if (isSpectrum() && isPlayingSound())
+				return kFCLPaused;
 			break;
 		case Token::DELAY:
 			_delayUntil = g_system->getMillis() + 20 * (instruction._source ? instruction._source : 256);
@@ -269,6 +286,8 @@ FCLExecutionResult Kit8Engine::executeCode(ScriptState &script, uint &budget) {
 			writeSystemVariables();
 			_scriptSurface.fillRect(_viewArea, 255);
 			updateInstruments();
+			_nextFrameTime = _lastTime + kFrameDuration;
+			_redrawPending = true;
 			_soundSyncReady = true;
 			return kFCLPaused;
 		default:
@@ -428,7 +447,8 @@ void Kit8Engine::executeCall(const FCLInstruction &instruction, ScriptState &scr
 }
 
 void Kit8Engine::executeColour(const FCLInstruction &instruction) {
-	_palette[instruction._source & 3] = MIN<int>(26, instruction._destination);
+	uint index = instruction._source & 3;
+	_palette[index] = isSpectrum() ? instruction._destination & (index == 2 ? 1 : 7) : MIN<int>(26, instruction._destination);
 	applyPalette();
 }
 
diff --git a/engines/freescape/metaengine.cpp b/engines/freescape/metaengine.cpp
index 9359f73db00..0ce141c5b8c 100644
--- a/engines/freescape/metaengine.cpp
+++ b/engines/freescape/metaengine.cpp
@@ -211,7 +211,7 @@ 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") {
-		if (gd->platform == Common::kPlatformAmstradCPC)
+		if (gd->platform == Common::kPlatformAmstradCPC || gd->platform == Common::kPlatformZX)
 			*engine = new Freescape::Kit8Engine(syst, gd);
 		else
 			*engine = new Freescape::KitEngine(syst, gd);
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index 0d1c9eace2e..f3d7522dedb 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -56,6 +56,7 @@ MODULE_OBJS := \
 	games/3dck/8bit.o \
 	games/3dck/8bitUI.o \
 	games/3dck/ui.o \
+	games/3dck/zx.o \
 	games/palettes.o \
 	gfx.o \
 	loaders/8bitImage.o \
@@ -81,6 +82,7 @@ MODULE_OBJS := \
 	sound/3dck.o \
 	sound/3dck_adlib.o \
 	sound/3dck_cpc.o \
+	sound/3dck_zx.o \
 	sound/cpc.o \
 	sound/dos.o \
 	sound/fx.o \
diff --git a/engines/freescape/sound/3dck_cpc.cpp b/engines/freescape/sound/3dck_cpc.cpp
index 5f3532df717..ff45be3d756 100644
--- a/engines/freescape/sound/3dck_cpc.cpp
+++ b/engines/freescape/sound/3dck_cpc.cpp
@@ -184,6 +184,10 @@ int KitCPCSound::readBuffer(int16 *buffer, int samples) {
 }
 
 void Kit8Engine::loadSounds() {
+	if (isSpectrum()) {
+		loadSoundsZX();
+		return;
+	}
 	byte data[sizeof(kKitCPCSounds)];
 	memcpy(data, kKitCPCSounds, sizeof(data));
 	Common::File file;
diff --git a/engines/freescape/sound/3dck_zx.cpp b/engines/freescape/sound/3dck_zx.cpp
new file mode 100644
index 00000000000..7595202ec8b
--- /dev/null
+++ b/engines/freescape/sound/3dck_zx.cpp
@@ -0,0 +1,131 @@
+/* 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 "freescape/games/3dck/8bit.h"
+
+namespace Freescape {
+
+class KitZXSound : public Sound {
+public:
+	KitZXSound(Audio::Mixer *mixer, const byte *data, uint size);
+	~KitZXSound() override { _mixer->stopHandle(_handle); }
+
+	void playSound(int index, Type type) override;
+	void stopSound(Type type) override;
+	bool isPlayingSound(Type type) const override;
+	bool isSoundAvailable(int index) const override { return index >= 0 && index <= 255; }
+
+private:
+	struct Sweep {
+		byte steps;
+		int8 step;
+		byte duration;
+	};
+	struct Effect {
+		uint16 period;
+		int8 step;
+		byte repeats;
+		Common::Array<Sweep> sweeps;
+	};
+	Effect _effects[16];
+	Audio::Mixer *_mixer;
+	Audio::SoundHandle _handle;
+	SizedPCSpeaker _speaker;
+	Type _type = kTypeNormal;
+};
+
+KitZXSound::KitZXSound(Audio::Mixer *mixer, const byte *data, uint size) : _mixer(mixer) {
+	for (uint index = 1; index < ARRAYSIZE(_effects); ++index) {
+		uint offset = 16 + data[index];
+		if (offset + 5 > size)
+			error("Invalid 3D Construction Kit Spectrum sound offset");
+		Effect &effect = _effects[index];
+		effect.period = READ_LE_UINT16(data + offset);
+		effect.step = int8(data[offset + 2]);
+		effect.repeats = data[offset + 3];
+		uint count = data[offset + 4] ? data[offset + 4] : 256;
+		offset += 5;
+		if (offset + 3 * count > size)
+			error("Invalid 3D Construction Kit Spectrum sound sweep");
+		for (uint i = 0; i < count; ++i) {
+			Sweep sweep = {data[offset], int8(data[offset + 1]), data[offset + 2]};
+			effect.sweeps.push_back(sweep);
+			offset += 3;
+		}
+	}
+}
+
+void KitZXSound::playSound(int index, Type type) {
+	if (!isSoundAvailable(index) || !(index & 15))
+		return;
+	stopSound(kTypeNormal);
+	_type = type;
+	const Effect &effect = _effects[index & 15];
+	uint16 base = effect.period;
+	uint repeats = effect.repeats ? effect.repeats : 256;
+	for (uint repeat = 0; repeat < repeats; ++repeat) {
+		uint16 period = base;
+		for (const Sweep &sweep : effect.sweeps) {
+			uint steps = sweep.steps ? sweep.steps : 256;
+			for (uint step = 0; step < steps; ++step) {
+				uint16 cycles = period ? (208 * sweep.duration) / period + 1 : 0;
+				int16 delay = 7 * period - 30;
+				if (delay < 0)
+					delay = 1;
+				// Runner 0xba90 uses 8 * HL + 236 T-states per beeper cycle.
+				float frequency = 3500000.0f / (8 * delay + 236);
+				uint32 duration = uint32(1000000.0f * (uint32(cycles) + 1) / frequency);
+				_speaker.playQueue(Audio::PCSpeaker::kWaveFormSquare, frequency, duration);
+				period = (period + sweep.step) & 0xfff;
+			}
+		}
+		base += effect.step;
+	}
+	_mixer->playStream(Audio::Mixer::kSFXSoundType, &_handle, &_speaker, -1,
+		kFreescapeDefaultVolume, 0, DisposeAfterUse::NO);
+}
+
+void KitZXSound::stopSound(Type type) {
+	if (type == kTypeNormal || type == _type) {
+		_mixer->stopHandle(_handle);
+		_speaker.stop();
+	}
+}
+
+bool KitZXSound::isPlayingSound(Type type) const {
+	return (type == kTypeNormal || type == _type) && _speaker.isPlaying();
+}
+
+void Kit8Engine::loadSoundsZX() {
+	Common::File file;
+	if (!file.open("3dkit.zx.code"))
+		error("Missing 3D Construction Kit Spectrum sounds");
+	byte data[118];
+	file.seek(0x1acc);
+	if (file.read(data, sizeof(data)) != sizeof(data))
+		error("Truncated 3D Construction Kit Spectrum sound bank");
+	_sound = new KitZXSound(_mixer, data, sizeof(data));
+}
+
+} // namespace Freescape
+
diff --git a/engines/freescape/zx_tape.cpp b/engines/freescape/zx_tape.cpp
index 3190231fab1..a72bde27be8 100644
--- a/engines/freescape/zx_tape.cpp
+++ b/engines/freescape/zx_tape.cpp
@@ -21,6 +21,7 @@
 
 #include "freescape/zx_tape.h"
 
+#include "common/endian.h"
 #include "common/file.h"
 #include "common/formats/spectrum_tape.h"
 #include "common/fs.h"
@@ -30,6 +31,101 @@
 
 namespace Freescape {
 
+class ZxRecordingDecoder {
+public:
+	ZxRecordingDecoder(Common::SpectrumTapeBlocks &blocks) : _blocks(blocks) {}
+	bool decode(const Common::Array<byte> &data);
+	void finish();
+
+private:
+	void pulse(uint32 length);
+	void endBlock();
+	Common::SpectrumTapeBlocks &_blocks;
+	Common::Array<byte> _tap;
+	uint32 _length = 0;
+	uint _pilot = 0, _bits = 0;
+	int _level = -1, _halfBit = -1;
+	byte _byte = 0, _checksum = 0;
+	bool _sync = false, _reading = false;
+};
+
+void ZxRecordingDecoder::endBlock() {
+	if (!_bits && !_checksum && _tap.size() >= 2 && (_tap[0] == 0 || _tap[0] == 255)) {
+		Common::SpectrumTapeBlock block;
+		block.id = 0x10;
+		block.tap = _tap;
+		_blocks.push_back(block);
+	}
+	_tap.clear();
+	_bits = _byte = _checksum = 0;
+	_halfBit = -1;
+	_reading = false;
+}
+
+void ZxRecordingDecoder::pulse(uint32 length) {
+	// ROM tape encoding: a pilot and two sync pulses, then two pulses per bit.
+	if (_reading) {
+		int bit = length >= 400 && length < 1300 ? 0 : length >= 1300 && length <= 2050 ? 1 : -1;
+		if (bit < 0 || (_halfBit >= 0 && bit != _halfBit) || _tap.size() > 65536) {
+			endBlock();
+		} else {
+			if (_halfBit < 0)
+				_halfBit = bit;
+			else {
+				_halfBit = -1;
+				_byte = (_byte << 1) | bit;
+				if (++_bits == 8) {
+					_tap.push_back(_byte);
+					_checksum ^= _byte;
+					_bits = _byte = 0;
+				}
+			}
+			return;
+		}
+	}
+	if (_sync) {
+		_reading = length >= 400 && length <= 1100;
+		_sync = false;
+	} else if (length >= 1900 && length <= 2500) {
+		++_pilot;
+	} else {
+		_sync = _pilot >= 256 && length >= 400 && length <= 1100;
+		_pilot = 0;
+	}
+}
+
+bool ZxRecordingDecoder::decode(const Common::Array<byte> &data) {
+	if (data.size() < 9 || !READ_LE_UINT16(data.data()) || data[4] < 1 || data[4] > 8 ||
+			READ_LE_UINT24(data.data() + 5) != data.size() - 8)
+		return false;
+	uint period = READ_LE_UINT16(data.data());
+	for (uint i = 8; i < data.size(); ++i) {
+		uint bits = i + 1 == data.size() ? data[4] : 8;
+		for (uint bit = 0; bit < bits; ++bit) {
+			int level = (data[i] >> (7 - bit)) & 1;
+			if (level != _level) {
+				if (_length)
+					pulse(_length);
+				_level = level;
+				_length = 0;
+			}
+			_length = MIN<uint32>(10000, _length + period);
+		}
+	}
+	if (READ_LE_UINT16(data.data() + 2))
+		finish();
+	return true;
+}
+
+void ZxRecordingDecoder::finish() {
+	if (_length)
+		pulse(_length);
+	endBlock();
+	_length = _pilot = 0;
+	_level = -1;
+	_sync = false;
+}
+
 bool extractZxSpectrumTapeFiles(Common::SeekableReadStream &stream, const char *prefix, ZxTapeFileList &files) {
 	files.clear();
 
@@ -37,15 +133,33 @@ bool extractZxSpectrumTapeFiles(Common::SeekableReadStream &stream, const char *
 	if (!Common::parseSpectrumTape(stream, blocks))
 		return false;
 
+	Common::SpectrumTapeBlocks decoded;
+	ZxRecordingDecoder recording(decoded);
+	for (const Common::SpectrumTapeBlock &block : blocks) {
+		if (block.id == 0x15) {
+			if (!recording.decode(block.data))
+				return false;
+		} else {
+			recording.finish();
+			decoded.push_back(block);
+		}
+	}
+	recording.finish();
+
 	Common::Array<byte> title;
 	Common::Array<byte> border;
 	Common::Array<byte> data;
+	Common::Array<byte> kitData;
+	Common::Array<byte> code;
 
-	for (const Common::SpectrumTapeBlock &block : blocks) {
+	for (const Common::SpectrumTapeBlock &block : decoded) {
 		if (block.tap.size() >= 2) {
 			Common::Array<byte> body;
 			body.assign(block.tap.begin() + 1, block.tap.end() - 1);
-			if (body.size() == 6912) {
+			if (body.size() >= 160 && READ_BE_UINT32(body.data()) == MKTAG('K', 'I', 'T', 'S') &&
+					READ_LE_UINT16(body.data() + 4) == body.size()) {
+				kitData = body;
+			} else if (body.size() == 6912) {
 				title = border;
 				border = body;
 			} else if (body.size() >= data.size()) {
@@ -53,6 +167,10 @@ bool extractZxSpectrumTapeFiles(Common::SeekableReadStream &stream, const char *
 			}
 		}
 	}
+	if (!kitData.empty()) {
+		code = data;
+		data = kitData;
+	}
 
 	const struct {
 		const char *suffix;
@@ -60,7 +178,8 @@ bool extractZxSpectrumTapeFiles(Common::SeekableReadStream &stream, const char *
 	} outputs[] = {
 		{ "title", title },
 		{ "border", border },
-		{ "data", data }
+		{ "data", data },
+		{ "code", code }
 	};
 	for (uint i = 0; i < ARRAYSIZE(outputs); ++i) {
 		if (!outputs[i].payload.empty()) {
@@ -80,8 +199,12 @@ bool matchZxSpectrumTapeFiles(const ZxTapeFileList &files, const ADGameDescripti
 	for (const ADGameFileDescription *fileDesc = desc.filesDescriptions; matched && fileDesc->fileName; ++fileDesc) {
 		bool fileMatched = false;
 		Common::Path fileName(fileDesc->fileName, Common::Path::kNoSeparator);
+		Common::String unprefixedName(fileDesc->fileName);
+		if (unprefixedName.hasPrefix(Common::String(desc.gameId) + ".zx."))
+			unprefixedName.erase(0, strlen(desc.gameId));
 		for (uint i = 0; !fileMatched && i < files.size(); ++i) {
-			fileMatched = files[i].name.equalsIgnoreCase(fileName) &&
+			fileMatched = (files[i].name.equalsIgnoreCase(fileName) ||
+					files[i].name.equalsIgnoreCase(Common::Path(unprefixedName, Common::Path::kNoSeparator))) &&
 				(fileDesc->fileSize == AD_NO_SIZE || fileDesc->fileSize == files[i].data.size());
 			if (fileMatched && fileDesc->md5) {
 				Common::MemoryReadStream stream(files[i].data.data(), files[i].data.size());


Commit: dc4b8e34362b7f2c216a98b316e8e8a9685c2e47
    https://github.com/scummvm/scummvm/commit/dc4b8e34362b7f2c216a98b316e8e8a9685c2e47
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-08T13:33:09+02:00

Commit Message:
FREESCAPE: complete implementation of Colour opcode for zx spectrum 3dck games

Changed paths:
    engines/freescape/language/execution_3dck8.cpp


diff --git a/engines/freescape/language/execution_3dck8.cpp b/engines/freescape/language/execution_3dck8.cpp
index a12583e29ee..2904c325e06 100644
--- a/engines/freescape/language/execution_3dck8.cpp
+++ b/engines/freescape/language/execution_3dck8.cpp
@@ -447,6 +447,11 @@ void Kit8Engine::executeCall(const FCLInstruction &instruction, ScriptState &scr
 }
 
 void Kit8Engine::executeColour(const FCLInstruction &instruction) {
+	if (isSpectrum() && instruction._source >= 3) {
+		// The Spectrum runner routes selectors 3 and above to the hardware border.
+		_palette[3] = instruction._destination & 7;
+		return;
+	}
 	uint index = instruction._source & 3;
 	_palette[index] = isSpectrum() ? instruction._destination & (index == 2 ? 1 : 7) : MIN<int>(26, instruction._destination);
 	applyPalette();


Commit: 28c76906affa04ee9c0887e66320753ba0c67b7b
    https://github.com/scummvm/scummvm/commit/28c76906affa04ee9c0887e66320753ba0c67b7b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-08T13:33:09+02:00

Commit Message:
FREESCAPE: initial code to support c64 3dck games

Changed paths:
  A engines/freescape/games/3dck/c64.cpp
    engines/freescape/detection.cpp
    engines/freescape/games/3dck/8bit.cpp
    engines/freescape/games/3dck/8bit.h
    engines/freescape/games/3dck/8bitUI.cpp
    engines/freescape/language/execution_3dck8.cpp
    engines/freescape/metaengine.cpp
    engines/freescape/module.mk
    engines/freescape/sound/3dck_cpc.cpp


diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index 911e369c072..8dae99dcd0f 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -1175,6 +1175,16 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE,
 		GUIO2(GUIO_NOMIDI, GUIO_RENDERCPC)
 	},
+	{
+		"3dkit",
+		"A Chance in Hell",
+		AD_ENTRY2s("3D1", "a3a5df3cf7ef4fec315e2eb18e00de26", 39938,
+			"3D4", "t:252c24449148d4ba4a176b14fed3f15d", 8002),
+		Common::EN_ANY,
+		Common::kPlatformC64,
+		ADGF_UNSTABLE,
+		GUIO2(GUIO_NOMIDI, GUIO_RENDERC64)
+	},
 	{
 		"3dkit",
 		"Ciudadela Fantasma",
diff --git a/engines/freescape/games/3dck/8bit.cpp b/engines/freescape/games/3dck/8bit.cpp
index 4b95216f8e9..2cdf41901af 100644
--- a/engines/freescape/games/3dck/8bit.cpp
+++ b/engines/freescape/games/3dck/8bit.cpp
@@ -52,7 +52,21 @@ void Kit8Engine::loadAssets() {
 	requireBytes(dataFile, 160);
 	uint32 signature = dataFile.readUint32BE();
 	uint32 dataOffset = 0;
-	if (isCPC() && signature != MKTAG('K', 'I', 'T', 'A') && signature != MKTAG('K', 'I', 'T', 'C')) {
+	uint32 dataEnd = dataFile.size();
+	if (isC64()) {
+		dataFile.seek(0);
+		if (dataFile.readUint16LE() != 0x0400)
+			error("Invalid 3D Construction Kit C64 runner address");
+		// The compiled runner embeds its world at $4a00.
+		dataOffset = 2 + 0x4a00 - 0x0400;
+		if (dataOffset + 160 > dataEnd)
+			error("Truncated 3D Construction Kit C64 runner");
+		dataFile.seek(dataOffset + 4);
+		uint16 size = dataFile.readUint16LE();
+		if (size < 160 || dataOffset + size > dataEnd)
+			error("Invalid 3D Construction Kit C64 world size");
+		dataEnd = dataOffset + size;
+	} else if (isCPC() && signature != MKTAG('K', 'I', 'T', 'A') && signature != MKTAG('K', 'I', 'T', 'C')) {
 		byte header[128];
 		dataFile.seek(0);
 		dataFile.read(header, sizeof(header));
@@ -63,11 +77,11 @@ void Kit8Engine::loadAssets() {
 			error("Invalid 3D Construction Kit AMSDOS header");
 		dataOffset = sizeof(header);
 	}
-	Common::SeekableSubReadStream file(&dataFile, dataOffset, dataFile.size());
+	Common::SeekableSubReadStream file(&dataFile, dataOffset, dataEnd);
 	requireBytes(file, 160);
 	signature = file.readUint32BE();
 	bool validSignature = isSpectrum() ? signature == MKTAG('K', 'I', 'T', 'S') :
-		signature == MKTAG('K', 'I', 'T', 'A') || signature == MKTAG('K', 'I', 'T', 'C');
+		signature == MKTAG('K', 'I', 'T', 'C') || (isCPC() && signature == MKTAG('K', 'I', 'T', 'A'));
 	if (!validSignature || file.readUint16LE() != file.size())
 		error("Unsupported 8-bit 3D Construction Kit data format");
 	uint16 procedures = file.readUint16LE();
@@ -92,6 +106,15 @@ void Kit8Engine::loadAssets() {
 	// Projection scales: 125 * 64 / (extent - 1), with a depth scale of 18.
 	int xScale = 8000 / (width - 1);
 	int yScale = 8000 / (height - 1);
+	if (isC64()) {
+		// C64 normalizes the projection scales to 64.
+		xScale = 64;
+		yScale = 64 * width / height;
+		if (yScale >= 64) {
+			xScale = 4096 / yScale;
+			yScale = 64;
+		}
+	}
 	if (xScale > 127 || yScale > 127)
 		error("Unsupported 8-bit 3D Construction Kit viewport size");
 	_fieldOfView = 2 * Math::rad2deg(atan(18.0f / xScale));
@@ -127,6 +150,9 @@ void Kit8Engine::loadAssets() {
 		Common::String text;
 		while (length--)
 			text += char(messageData.readByte());
+		// C64 message lengths include the editor's terminator.
+		if (isC64() && !text.empty() && byte(text.lastChar()) == 0xff)
+			text.deleteLastChar();
 		_kitMessages[id] = text;
 	}
 	Common::SeekableSubReadStream procedureData(&file, procedures, conditions);
@@ -195,7 +221,7 @@ Area *Kit8Engine::loadArea(Common::SeekableReadStream &file) {
 	AreaData &data = _areaData[id];
 	for (uint i = 0; i < 4; i++) {
 		data.palette[i] = file.readByte();
-		if (data.palette[i] > (isSpectrum() ? (i == 2 ? 1 : 7) : 26))
+		if (data.palette[i] > (isSpectrum() ? (i == 2 ? 1 : 7) : isC64() ? 15 : 26))
 			error("Invalid 8-bit 3D Construction Kit palette");
 	}
 	byte scale = file.readByte();
diff --git a/engines/freescape/games/3dck/8bit.h b/engines/freescape/games/3dck/8bit.h
index 24e07b8e590..fb6f3359d7a 100644
--- a/engines/freescape/games/3dck/8bit.h
+++ b/engines/freescape/games/3dck/8bit.h
@@ -71,11 +71,13 @@ private:
 	GeometricObject *loadGeometricObject(Common::SeekableReadStream &file, const byte header[9]);
 	void loadPresentation();
 	void loadPresentationZX();
+	void loadPresentationC64();
 	void loadSounds();
 	void loadSoundsZX();
 	void playPendingSound();
 	void applyPalette();
 	void applyPaletteZX();
+	void applyPaletteC64();
 	void setAttributesZX(const Common::Rect &rect, byte color);
 	void setMovementMode(byte mode);
 	void readSystemVariables();
diff --git a/engines/freescape/games/3dck/8bitUI.cpp b/engines/freescape/games/3dck/8bitUI.cpp
index f37c522ddb6..ff6021df423 100644
--- a/engines/freescape/games/3dck/8bitUI.cpp
+++ b/engines/freescape/games/3dck/8bitUI.cpp
@@ -46,6 +46,10 @@ void Kit8Engine::loadPresentation() {
 		loadPresentationZX();
 		return;
 	}
+	if (isC64()) {
+		loadPresentationC64();
+		return;
+	}
 	// Shade 0 is transparent; the CPC runner indexes these patterns with shade - 1.
 	static const byte patterns[15][4] = {
 		{0x00, 0x00, 0x00, 0x00},
@@ -89,6 +93,10 @@ void Kit8Engine::applyPalette() {
 		applyPaletteZX();
 		return;
 	}
+	if (isC64()) {
+		applyPaletteC64();
+		return;
+	}
 	_gfx->_fourColorBackground = kCPCInks[_palette[0]];
 	_gfx->_underFireBackgroundColor = kCPCInks[_palette[2]];
 	_gfx->_paperColor = kCPCInks[_palette[1]];
@@ -102,8 +110,8 @@ void Kit8Engine::printText(const Common::String &text, byte x, byte y, byte colo
 	if (!_textOutputEnabled || x >= _screenW / 8 || y >= _screenH / 8)
 		return;
 	Graphics::DosFont font;
-	byte foreground = isSpectrum() ? 1 : color & 3;
-	byte background = isSpectrum() ? 0 : ((color >> 3) & 1) | ((color >> 1) & 2);
+	byte foreground = isSpectrum() ? 1 : color & (isC64() ? 15 : 3);
+	byte background = isSpectrum() ? 0 : isC64() ? color >> 4 : ((color >> 3) & 1) | ((color >> 1) & 2);
 	for (uint i = 0; i < text.size() && x < _screenW / 8; i++, x++) {
 		byte chr = text[i];
 		if (isSpectrum())
@@ -145,14 +153,16 @@ void Kit8Engine::updateInstruments() {
 			Common::Rect bar(8 * x, 8 * y, 8 * (x + (type == 2 ? length : 1)), 8 * (y + (type == 3 ? length : 1)));
 			if (isSpectrum())
 				setAttributesZX(bar, color);
-			_scriptSurface.fillRect(bar, isSpectrum() ? 0 : (color >> 2) & 3);
+			_scriptSurface.fillRect(bar, isSpectrum() ? 0 : isC64() ? color >> 4 : (color >> 2) & 3);
 			int filled = MIN<int>(value, 8 * length);
+			if (isC64() && type == 2)
+				filled &= ~1;
 			if (type == 2)
 				bar.right = bar.left + filled;
 			else
 				bar.top = bar.bottom - filled;
 			if (!bar.isEmpty())
-				_scriptSurface.fillRect(bar, isSpectrum() ? 1 : color & 3);
+				_scriptSurface.fillRect(bar, isSpectrum() ? 1 : color & (isC64() ? 15 : 3));
 		}
 	}
 }
@@ -160,9 +170,9 @@ void Kit8Engine::updateInstruments() {
 void Kit8Engine::drawUI() {
 	uint32 colors[16];
 	bool flash = (g_system->getMillis() / 320) & 1;
-	for (uint i = 0; i < (isSpectrum() ? 16 : 4); i++) {
+	for (uint i = 0; i < (isSpectrum() || isC64() ? 16 : 4); i++) {
 		byte r, g, b;
-		if (isSpectrum())
+		if (isSpectrum() || isC64())
 			_gfx->readFromPalette(i, r, g, b);
 		else
 			_gfx->selectColorFromFourColorPalette(i, r, g, b);
@@ -173,6 +183,8 @@ void Kit8Engine::drawUI() {
 			byte pen = _scriptSurface.getPixel(x, y);
 			if (pen == 255)
 				pen = _borderSurface.getPixel(x, y);
+			if (isC64() && pen == 16)
+				pen = _palette[0];
 			if (isSpectrum() && pen != 255) {
 				byte attr = _attributes[(y / 8) * 32 + x / 8];
 				if ((attr & 128) && flash)
diff --git a/engines/freescape/games/3dck/c64.cpp b/engines/freescape/games/3dck/c64.cpp
new file mode 100644
index 00000000000..d08183b2be0
--- /dev/null
+++ b/engines/freescape/games/3dck/c64.cpp
@@ -0,0 +1,86 @@
+/* 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 "freescape/games/3dck/8bit.h"
+
+namespace Freescape {
+
+static Common::Array<byte> loadKitC64Program(const char *name, uint16 address, uint32 size) {
+	Common::File file;
+	if (!file.open(name) || file.size() != size + 2 || file.readUint16LE() != address)
+		error("Invalid 3D Construction Kit C64 file %s", name);
+	Common::Array<byte> data;
+	data.resize(size);
+	if (file.read(data.data(), size) != size)
+		error("Truncated 3D Construction Kit C64 file %s", name);
+	return data;
+}
+
+void Kit8Engine::loadPresentationC64() {
+	Common::Array<byte> runner = loadKitC64Program("3D1", 0x0400, 39936);
+	Common::Array<byte> characters = loadKitC64Program("3D3", 0xc400, 3072);
+	Common::Array<byte> bitmap = loadKitC64Program("3D4", 0x3800, 8000);
+	memcpy(_colorPatterns, runner.data() + 0x672e - 0x0400, sizeof(_colorPatterns));
+
+	for (int y = 0; y < _screenH; ++y) {
+		for (int x = 0; x < _screenW; ++x) {
+			uint cell = (y / 8) * 40 + x / 8;
+			byte pen = (bitmap[8 * cell + (y & 7)] >> (6 - (x & 6))) & 3;
+			byte screen = runner[0x3000 - 0x0400 + cell];
+			// Index 16 follows the VIC background colour of the current area.
+			byte color = pen == 0 ? 16 : pen == 1 ? screen >> 4 : pen == 2 ? screen & 15 :
+				runner[0x3400 - 0x0400 + cell] & 15;
+			_borderSurface.setPixel(x, y, color);
+		}
+	}
+	_borderSurface.fillRect(_viewArea, 255);
+
+	uint16 font = READ_LE_UINT16(runner.data() + 0x1a7b - 0x0400);
+	if (font < 0xc400 || font + 64 * 8 > 0xc400 + characters.size())
+		error("Invalid 3D Construction Kit C64 font address");
+	for (uint chr = 32; chr < 128; ++chr) {
+		uint glyph = chr == 32 ? 0 : chr >= 65 ? chr - 64 : chr - 21;
+		for (uint row = 0; row < 8; ++row) {
+			byte pixels = characters[font - 0xc400 + 8 * glyph + row];
+			byte bits = 0;
+			for (uint col = 0; col < 4; ++col) {
+				if (((pixels >> (6 - 2 * col)) & 3) == 2)
+					bits |= 3 << (6 - 2 * col);
+			}
+			_fontData[chr - 32][row] = bits;
+		}
+	}
+	_hasFont = true;
+}
+
+void Kit8Engine::applyPaletteC64() {
+	_gfx->_fourColorBackground = _palette[0];
+	_gfx->_underFireBackgroundColor = _palette[2];
+	_gfx->_paperColor = _palette[1];
+	_gfx->_inkColor = _palette[3];
+	_currentArea->_usualBackgroundColor = 1;
+	_currentArea->_skyColor = 1;
+	_currentArea->_underFireBackgroundColor = 4;
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/language/execution_3dck8.cpp b/engines/freescape/language/execution_3dck8.cpp
index 2904c325e06..8e41e879b52 100644
--- a/engines/freescape/language/execution_3dck8.cpp
+++ b/engines/freescape/language/execution_3dck8.cpp
@@ -447,13 +447,16 @@ void Kit8Engine::executeCall(const FCLInstruction &instruction, ScriptState &scr
 }
 
 void Kit8Engine::executeColour(const FCLInstruction &instruction) {
+	if (isC64() && instruction._source >= 4)
+		return;
 	if (isSpectrum() && instruction._source >= 3) {
 		// The Spectrum runner routes selectors 3 and above to the hardware border.
 		_palette[3] = instruction._destination & 7;
 		return;
 	}
 	uint index = instruction._source & 3;
-	_palette[index] = isSpectrum() ? instruction._destination & (index == 2 ? 1 : 7) : MIN<int>(26, instruction._destination);
+	_palette[index] = isSpectrum() ? instruction._destination & (index == 2 ? 1 : 7) :
+		isC64() ? instruction._destination & 15 : MIN<int>(26, instruction._destination);
 	applyPalette();
 }
 
diff --git a/engines/freescape/metaengine.cpp b/engines/freescape/metaengine.cpp
index 0ce141c5b8c..add43801236 100644
--- a/engines/freescape/metaengine.cpp
+++ b/engines/freescape/metaengine.cpp
@@ -211,7 +211,7 @@ 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") {
-		if (gd->platform == Common::kPlatformAmstradCPC || gd->platform == Common::kPlatformZX)
+		if (gd->platform == Common::kPlatformAmstradCPC || gd->platform == Common::kPlatformZX || gd->platform == Common::kPlatformC64)
 			*engine = new Freescape::Kit8Engine(syst, gd);
 		else
 			*engine = new Freescape::KitEngine(syst, gd);
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index f3d7522dedb..ef50be2b776 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -55,6 +55,7 @@ MODULE_OBJS := \
 	games/3dck/3dck.o \
 	games/3dck/8bit.o \
 	games/3dck/8bitUI.o \
+	games/3dck/c64.o \
 	games/3dck/ui.o \
 	games/3dck/zx.o \
 	games/palettes.o \
diff --git a/engines/freescape/sound/3dck_cpc.cpp b/engines/freescape/sound/3dck_cpc.cpp
index ff45be3d756..289376d225c 100644
--- a/engines/freescape/sound/3dck_cpc.cpp
+++ b/engines/freescape/sound/3dck_cpc.cpp
@@ -184,6 +184,10 @@ int KitCPCSound::readBuffer(int16 *buffer, int samples) {
 }
 
 void Kit8Engine::loadSounds() {
+	if (isC64()) {
+		warning("3D Construction Kit C64 sound effects are not implemented");
+		return;
+	}
 	if (isSpectrum()) {
 		loadSoundsZX();
 		return;


Commit: d88ede37a6cb6e350a86fe4670153b76143fd246
    https://github.com/scummvm/scummvm/commit/d88ede37a6cb6e350a86fe4670153b76143fd246
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-08T13:33:09+02:00

Commit Message:
COMMON: moved unp64 to common/compression

Changed paths:
  A common/compression/unp64.h
  A common/compression/unp64/6502_emu.cpp
  A common/compression/unp64/6502_emu.h
  A common/compression/unp64/exo_util.cpp
  A common/compression/unp64/exo_util.h
  A common/compression/unp64/scanners/action_packer.cpp
  A common/compression/unp64/scanners/byte_boiler.cpp
  A common/compression/unp64/scanners/caution.cpp
  A common/compression/unp64/scanners/ccs.cpp
  A common/compression/unp64/scanners/cruel.cpp
  A common/compression/unp64/scanners/eca.cpp
  A common/compression/unp64/scanners/exomizer.cpp
  A common/compression/unp64/scanners/expert.cpp
  A common/compression/unp64/scanners/master_compressor.cpp
  A common/compression/unp64/scanners/megabyte.cpp
  A common/compression/unp64/scanners/pu_crunch.cpp
  A common/compression/unp64/scanners/scanners.cpp
  A common/compression/unp64/scanners/section8.cpp
  A common/compression/unp64/scanners/tbc_multicomp.cpp
  A common/compression/unp64/scanners/tcs_crunch.cpp
  A common/compression/unp64/scanners/xtc.cpp
  A common/compression/unp64/unp64.cpp
  A common/compression/unp64/unp64.h
  R engines/glk/scott/unp64/6502_emu.cpp
  R engines/glk/scott/unp64/6502_emu.h
  R engines/glk/scott/unp64/exo_util.cpp
  R engines/glk/scott/unp64/exo_util.h
  R engines/glk/scott/unp64/scanners/action_packer.cpp
  R engines/glk/scott/unp64/scanners/byte_boiler.cpp
  R engines/glk/scott/unp64/scanners/caution.cpp
  R engines/glk/scott/unp64/scanners/ccs.cpp
  R engines/glk/scott/unp64/scanners/cruel.cpp
  R engines/glk/scott/unp64/scanners/eca.cpp
  R engines/glk/scott/unp64/scanners/exomizer.cpp
  R engines/glk/scott/unp64/scanners/expert.cpp
  R engines/glk/scott/unp64/scanners/master_compressor.cpp
  R engines/glk/scott/unp64/scanners/megabyte.cpp
  R engines/glk/scott/unp64/scanners/pu_crunch.cpp
  R engines/glk/scott/unp64/scanners/scanners.cpp
  R engines/glk/scott/unp64/scanners/section8.cpp
  R engines/glk/scott/unp64/scanners/tbc_multicomp.cpp
  R engines/glk/scott/unp64/scanners/tcs_crunch.cpp
  R engines/glk/scott/unp64/scanners/xtc.cpp
  R engines/glk/scott/unp64/unp64.cpp
  R engines/glk/scott/unp64/unp64.h
  R engines/glk/scott/unp64/unp64_interface.h
    common/compression/module.mk
    configure
    engines/glk/configure.engine
    engines/glk/module.mk
    engines/glk/scott/c64_checksums.cpp
    engines/glk/scott/globals.h


diff --git a/common/compression/module.mk b/common/compression/module.mk
index 05c6ec4f545..8ef332a20ee 100644
--- a/common/compression/module.mk
+++ b/common/compression/module.mk
@@ -15,6 +15,29 @@ MODULE_OBJS := \
 	unzip.o \
 	vise.o
 
+ifdef USE_UNP64
+MODULE_OBJS += \
+	unp64/unp64.o \
+	unp64/6502_emu.o \
+	unp64/exo_util.o \
+	unp64/scanners/scanners.o \
+	unp64/scanners/action_packer.o \
+	unp64/scanners/byte_boiler.o \
+	unp64/scanners/caution.o \
+	unp64/scanners/ccs.o \
+	unp64/scanners/cruel.o \
+	unp64/scanners/eca.o \
+	unp64/scanners/exomizer.o \
+	unp64/scanners/expert.o \
+	unp64/scanners/master_compressor.o \
+	unp64/scanners/megabyte.o \
+	unp64/scanners/pu_crunch.o \
+	unp64/scanners/section8.o \
+	unp64/scanners/tbc_multicomp.o \
+	unp64/scanners/tcs_crunch.o \
+	unp64/scanners/xtc.o
+endif
+
 ifdef USE_ZLIB
 MODULE_OBJS += \
 	zlib.o
diff --git a/engines/glk/scott/unp64/unp64_interface.h b/common/compression/unp64.h
similarity index 66%
rename from engines/glk/scott/unp64/unp64_interface.h
rename to common/compression/unp64.h
index c8b262a233e..0e532c50869 100644
--- a/engines/glk/scott/unp64/unp64_interface.h
+++ b/common/compression/unp64.h
@@ -19,17 +19,21 @@
  *
  */
 
-#ifndef GLK_SCOTT_UNP64_INTERFACE_H
-#define GLK_SCOTT_UNP64_INTERFACE_H
+#ifndef COMMON_COMPRESSION_UNP64_H
+#define COMMON_COMPRESSION_UNP64_H
 
-#include "glk/scott/types.h"
+#include "common/scummsys.h"
 
-namespace Glk {
-namespace Scott {
+namespace Common {
+namespace Unp64 {
 
-int unp64(uint8_t *compressed, size_t length, uint8_t *destinationBuffer, size_t *finalLength, const char *settings);
+/**
+ * Unpack a C64 PRG, preserving its two-byte load address.
+ * destinationBuffer must hold 65536 bytes. Returns zero on failure.
+ */
+int unp64(const byte *compressed, uint32 length, byte *destinationBuffer, uint32 *finalLength, const char *settings);
 
-} // End of namespace Scott
-} // End of namespace Glk
+} // End of namespace Unp64
+} // End of namespace Common
 
-#endif 
+#endif
diff --git a/engines/glk/scott/unp64/6502_emu.cpp b/common/compression/unp64/6502_emu.cpp
similarity index 93%
rename from engines/glk/scott/unp64/6502_emu.cpp
rename to common/compression/unp64/6502_emu.cpp
index 527de32bd44..ddeac291362 100644
--- a/engines/glk/scott/unp64/6502_emu.cpp
+++ b/common/compression/unp64/6502_emu.cpp
@@ -41,11 +41,10 @@
  *
  */
 
-#include "glk/scott/globals.h"
-#include "glk/scott/unp64/6502_emu.h"
+#include "common/compression/unp64/6502_emu.h"
 
-namespace Glk {
-namespace Scott {
+namespace Common {
+namespace Unp64 {
 
 #define FLAG_N 128
 #define FLAG_V 64
@@ -55,13 +54,13 @@ namespace Scott {
 #define FLAG_C 1
 
 struct ArgEa {
-	uint16_t _value;
+	uint16 _value;
 };
 struct ArgRelative {
-	int8_t _value;
+	int8 _value;
 };
 struct ArgImmediate {
-	uint8_t _value;
+	uint8 _value;
 };
 
 union InstArg {
@@ -86,7 +85,7 @@ struct ModeInfo {
 struct InstInfo {
 	OpInfo *_op;
 	ModeInfo *_mode;
-	uint8_t _cycles;
+	uint8 _cycles;
 };
 
 #define MODE_IMMEDIATE 0
@@ -116,30 +115,30 @@ static int mode_zp(CpuCtx *r, InstArg *arg) {
 }
 
 static int mode_zpx(CpuCtx *r, InstArg *arg) { /* iAN: ldx #1 lda $ff,x should fetch from $00 and not $100 */
-	uint8_t lsbLo = (r->_mem[r->_pc + 1] + r->_x) & 0xff;
+	uint8 lsbLo = (r->_mem[r->_pc + 1] + r->_x) & 0xff;
 	arg->_ea._value = lsbLo;
 	r->_pc += 2;
 	return MODE_ZERO_PAGE_X;
 }
 
 static int mode_zpy(CpuCtx *r, InstArg *arg) { /* iAN: ldy #1 ldx $ff,y should fetch from $00 and not $100 */
-	uint8_t lsbLo = (r->_mem[r->_pc + 1] + r->_y) & 0xff;
+	uint8 lsbLo = (r->_mem[r->_pc + 1] + r->_y) & 0xff;
 	arg->_ea._value = lsbLo;
 	r->_pc += 2;
 	return MODE_ZERO_PAGE_Y;
 }
 
 static int mode_abs(CpuCtx *r, InstArg *arg) {
-	uint16_t offset = r->_mem[r->_pc + 1];
-	uint16_t base = r->_mem[r->_pc + 2] << 8;
+	uint16 offset = r->_mem[r->_pc + 1];
+	uint16 base = r->_mem[r->_pc + 2] << 8;
 	arg->_ea._value = base + offset;
 	r->_pc += 3;
 	return MODE_ABSOLUTE;
 }
 
 static int mode_absx(CpuCtx *r, InstArg *arg) {
-	uint16_t offset = r->_mem[r->_pc + 1] + r->_x;
-	uint16_t base = r->_mem[r->_pc + 2] << 8;
+	uint16 offset = r->_mem[r->_pc + 1] + r->_x;
+	uint16 base = r->_mem[r->_pc + 2] << 8;
 	arg->_ea._value = base + offset;
 	r->_pc += 3;
 	r->_cycles += (offset > 255);
@@ -147,8 +146,8 @@ static int mode_absx(CpuCtx *r, InstArg *arg) {
 }
 
 static int mode_absy(CpuCtx *r, InstArg *arg) {
-	uint16_t offset = r->_mem[r->_pc + 1] + r->_y;
-	uint16_t base = r->_mem[r->_pc + 2] << 8;
+	uint16 offset = r->_mem[r->_pc + 1] + r->_y;
+	uint16 base = r->_mem[r->_pc + 2] << 8;
 	arg->_ea._value = base + offset;
 	r->_pc += 3;
 	r->_cycles += (offset > 255);
@@ -167,20 +166,20 @@ static int mode_ind(CpuCtx *r, InstArg *arg) {
 }
 
 static int mode_indx(CpuCtx *r, InstArg *arg) {
-	uint8_t lsbLo = r->_mem[r->_pc + 1] + r->_x;
-	uint8_t msbLo = lsbLo + 1;
-	uint16_t base = r->_mem[msbLo] << 8;
-	uint16_t offset = r->_mem[lsbLo];
+	uint8 lsbLo = r->_mem[r->_pc + 1] + r->_x;
+	uint8 msbLo = lsbLo + 1;
+	uint16 base = r->_mem[msbLo] << 8;
+	uint16 offset = r->_mem[lsbLo];
 	arg->_ea._value = base + offset;
 	r->_pc += 2;
 	return MODE_INDIRECT_X;
 }
 
 static int mode_indy(CpuCtx *r, InstArg *arg) {
-	uint8_t lsbLo = r->_mem[r->_pc + 1];
-	uint8_t msbLo = lsbLo + 1;
-	uint16_t base = r->_mem[msbLo] << 8;
-	uint16_t offset = r->_mem[lsbLo] + r->_y;
+	uint8 lsbLo = r->_mem[r->_pc + 1];
+	uint8 msbLo = lsbLo + 1;
+	uint16 base = r->_mem[msbLo] << 8;
+	uint16 offset = r->_mem[lsbLo] + r->_y;
 	arg->_ea._value = base + offset;
 	r->_pc += 2;
 	r->_cycles += (offset > 255);
@@ -188,7 +187,7 @@ static int mode_indy(CpuCtx *r, InstArg *arg) {
 }
 
 static int mode_rel(CpuCtx *r, InstArg *arg) {
-	arg->_rel._value = (int8_t)r->_mem[r->_pc + 1];
+	arg->_rel._value = (int8)r->_mem[r->_pc + 1];
 	r->_pc += 2;
 	return MODE_RELATIVE;
 }
@@ -217,7 +216,7 @@ static ModeInfo mode_rel_o	= { &mode_rel,	"$%02x" };
 static ModeInfo mode_acc_o	= { &mode_acc,	"a" };
 static ModeInfo mode_imp_o	= { &mode_imp,	nullptr };
 
-static void updateFlagsNz(CpuCtx *r, uint8_t value) {
+static void updateFlagsNz(CpuCtx *r, uint8 value) {
 	r->_flags &= ~(FLAG_Z | FLAG_N);
 	r->_flags |= (value == 0 ? FLAG_Z : 0) | (value & FLAG_N);
 }
@@ -226,8 +225,8 @@ static void updateCarry(CpuCtx *r, int boolean) {
 	r->_flags = (r->_flags & ~FLAG_C) | (boolean != 0 ? FLAG_C : 0);
 }
 
-static uint16_t subtract(CpuCtx *r, int carry, uint8_t val1, uint8_t value) {
-	uint16_t target = val1 - value - (1 - !!carry);
+static uint16 subtract(CpuCtx *r, int carry, uint8 val1, uint8 value) {
+	uint16 target = val1 - value - (1 - !!carry);
 	updateCarry(r, !(target & 256));
 	updateFlagsNz(r, target & 255);
 	return target;
@@ -238,8 +237,8 @@ static void update_overflow(CpuCtx *r, int boolean) {
 }
 
 static void op_adc(CpuCtx *r, int mode, InstArg *arg) {
-	uint8_t value;
-	uint16_t result;
+	uint8 value;
+	uint16 result;
 	switch (mode) {
 	case MODE_IMMEDIATE:
 		value = arg->_imm._value;
@@ -256,7 +255,7 @@ static void op_adc(CpuCtx *r, int mode, InstArg *arg) {
 }
 
 static void op_and(CpuCtx *r, int mode, InstArg *arg) {
-	uint8_t value;
+	uint8 value;
 	switch (mode) {
 	case MODE_IMMEDIATE:
 		value = arg->_imm._value;
@@ -270,7 +269,7 @@ static void op_and(CpuCtx *r, int mode, InstArg *arg) {
 }
 
 static void op_asl(CpuCtx *r, int mode, InstArg *arg) {
-	uint8_t *valuep;
+	uint8 *valuep;
 	switch (mode) {
 	case MODE_ACCUMULATOR:
 		valuep = &r->_a;
@@ -285,7 +284,7 @@ static void op_asl(CpuCtx *r, int mode, InstArg *arg) {
 }
 
 static void branch(CpuCtx *r, InstArg *arg) {
-	uint16_t target = r->_pc + arg->_rel._value;
+	uint16 target = r->_pc + arg->_rel._value;
 	r->_cycles += 1 + ((target & ~255) != (r->_pc & ~255));
 	r->_pc = target;
 }
@@ -369,7 +368,7 @@ static void op_clv(CpuCtx *r, int mode, InstArg *arg) {
 
 
 static void op_cmp(CpuCtx *r, int mode, InstArg *arg) {
-	uint8_t value;
+	uint8 value;
 	switch (mode) {
 	case MODE_IMMEDIATE:
 		value = arg->_imm._value;
@@ -382,7 +381,7 @@ static void op_cmp(CpuCtx *r, int mode, InstArg *arg) {
 }
 
 static void op_cpx(CpuCtx *r, int mode, InstArg *arg) {
-	uint8_t value;
+	uint8 value;
 	switch (mode) {
 	case MODE_IMMEDIATE:
 		value = arg->_imm._value;
@@ -395,7 +394,7 @@ static void op_cpx(CpuCtx *r, int mode, InstArg *arg) {
 }
 
 static void op_cpy(CpuCtx *r, int mode, InstArg *arg) {
-	uint8_t value;
+	uint8 value;
 	switch (mode) {
 	case MODE_IMMEDIATE:
 		value = arg->_imm._value;
@@ -423,7 +422,7 @@ static void op_dey(CpuCtx *r, int mode, InstArg *arg) {
 }
 
 static void op_eor(CpuCtx *r, int mode, InstArg *arg) {
-	uint8_t value;
+	uint8 value;
 	switch (mode) {
 	case MODE_IMMEDIATE:
 		value = arg->_imm._value;
@@ -463,7 +462,7 @@ static void op_jsr(CpuCtx *r, int mode, InstArg *arg) {
 }
 
 static void op_lda(CpuCtx *r, int mode, InstArg *arg) {
-	uint8_t value;
+	uint8 value;
 	switch (mode) {
 	case MODE_IMMEDIATE:
 		value = arg->_imm._value;
@@ -477,7 +476,7 @@ static void op_lda(CpuCtx *r, int mode, InstArg *arg) {
 }
 
 static void op_ldx(CpuCtx *r, int mode, InstArg *arg) {
-	uint8_t value;
+	uint8 value;
 	switch (mode) {
 	case MODE_IMMEDIATE:
 		value = arg->_imm._value;
@@ -491,7 +490,7 @@ static void op_ldx(CpuCtx *r, int mode, InstArg *arg) {
 }
 
 static void op_ldy(CpuCtx *r, int mode, InstArg *arg) {
-	uint8_t value;
+	uint8 value;
 	switch (mode) {
 	case MODE_IMMEDIATE:
 		value = arg->_imm._value;
@@ -505,7 +504,7 @@ static void op_ldy(CpuCtx *r, int mode, InstArg *arg) {
 }
 
 static void op_lsr(CpuCtx *r, int mode, InstArg *arg) {
-	uint8_t *valuep;
+	uint8 *valuep;
 	switch (mode) {
 	case MODE_ACCUMULATOR:
 		valuep = &r->_a;
@@ -522,7 +521,7 @@ static void op_lsr(CpuCtx *r, int mode, InstArg *arg) {
 static void op_nop(CpuCtx *r, int mode, InstArg *arg) {}
 
 static void op_ora(CpuCtx *r, int mode, InstArg *arg) {
-	uint8_t value;
+	uint8 value;
 	switch (mode) {
 	case MODE_IMMEDIATE:
 		value = arg->_imm._value;
@@ -553,8 +552,8 @@ static void op_plp(CpuCtx *r, int mode, InstArg *arg) {
 }
 
 static void op_rol(CpuCtx *r, int mode, InstArg *arg) {
-	uint8_t *valuep;
-	uint8_t old_flags;
+	uint8 *valuep;
+	uint8 old_flags;
 	switch (mode) {
 	case MODE_ACCUMULATOR:
 		valuep = &r->_a;
@@ -571,8 +570,8 @@ static void op_rol(CpuCtx *r, int mode, InstArg *arg) {
 }
 
 static void op_ror(CpuCtx *r, int mode, InstArg *arg) {
-	uint8_t *valuep;
-	uint8_t old_flags;
+	uint8 *valuep;
+	uint8 old_flags;
 	switch (mode) {
 	case MODE_ACCUMULATOR:
 		valuep = &r->_a;
@@ -601,8 +600,8 @@ static void op_rts(CpuCtx *r, int mode, InstArg *arg) {
 }
 
 static void op_sbc(CpuCtx *r, int mode, InstArg *arg) {
-	uint8_t value;
-	uint16_t result;
+	uint8 value;
+	uint16 result;
 	switch (mode) {
 	case MODE_IMMEDIATE:
 		value = arg->_imm._value;
@@ -672,7 +671,7 @@ static void op_tya(CpuCtx *r, int mode, InstArg *arg) {
 
 /* iAN */
 static void op_anc(CpuCtx *r, int mode, InstArg *arg) {
-	uint8_t value;
+	uint8 value;
 	switch (mode) {
 	case MODE_IMMEDIATE:
 		value = arg->_imm._value;
@@ -1123,13 +1122,13 @@ static InstInfo g_ops[256] = {
 	{ &op_isb_o, &mode_absx_o,	7 }, /* $ff isb $ffff,x */
 };
 
-int flipfire(void) {
-	_G(_retfire) ^= 0x90;
-	return _G(_retfire);
+static int flipfire(CpuCtx *r) {
+	r->_retfire ^= 0x90;
+	return r->_retfire;
 }
-int flipspace(void) {
-	_G(_retspace) ^= 0x10;
-	return _G(_retspace);
+static int flipspace(CpuCtx *r) {
+	r->_retspace ^= 0x10;
+	return r->_retspace;
 }
 
 int nextInst(CpuCtx* r) {
@@ -1173,13 +1172,13 @@ int nextInst(CpuCtx* r) {
 				if (arg->_ea._value == 0xd011) {
 					switch (opCode) {
 					case 0x8D:
-						_G(_byted011)[0] = r->_a & 0x7f;
+						r->_byted011[0] = r->_a & 0x7f;
 						break;
 					case 0x8E:
-						_G(_byted011)[0] = r->_x & 0x7f;
+						r->_byted011[0] = r->_x & 0x7f;
 						break;
 					case 0x8C:
-						_G(_byted011)[0] = r->_y & 0x7f;
+						r->_byted011[0] = r->_y & 0x7f;
 						break;
 					default:
 						break;
@@ -1187,16 +1186,16 @@ int nextInst(CpuCtx* r) {
 				}
 				WriteToIO = 1;
 			} else {
-				_G(_byted011)[1] = (r->_cycles / 0x3f) % 0x157;
-				_G(_byted011)[0] = (_G(_byted011)[0] & 0x7f) | ((_G(_byted011)[1] & 0x100) >> 1);
-				_G(_byted011)[1] &= 0xff;
+				r->_byted011[1] = (r->_cycles / 0x3f) % 0x157;
+				r->_byted011[0] = (r->_byted011[0] & 0x7f) | ((r->_byted011[1] & 0x100) >> 1);
+				r->_byted011[1] &= 0xff;
 				switch (opCode) {
 				case 0xad:
 				case 0xaf: /* lda $ffff / lax $ffff */
 
 					if ((arg->_ea._value == 0xd011) || (arg->_ea._value == 0xd012)) {
 						r->_cycles += g_ops[opCode]._cycles;
-						r->_a = _G(_byted011)[arg->_ea._value - 0xd011];
+						r->_a = r->_byted011[arg->_ea._value - 0xd011];
 						if (opCode == 0xaf)
 							r->_x = r->_a;
 						updateFlagsNz(r, r->_a);
@@ -1208,9 +1207,9 @@ int nextInst(CpuCtx* r) {
 					if (arg->_ea._value == 0xdc00 || arg->_ea._value == 0xdc01) {
 						r->_cycles += g_ops[opCode]._cycles;
 						if (arg->_ea._value == 0xdc00)
-							r->_a = flipfire();
+							r->_a = flipfire(r);
 						else
-							r->_a = flipspace();
+							r->_a = flipspace(r);
 						if (opCode == 0xaf)
 							r->_x = r->_a;
 						updateFlagsNz(r, r->_a);
@@ -1234,9 +1233,9 @@ int nextInst(CpuCtx* r) {
 					if (arg->_ea._value == 0xdc00 || arg->_ea._value == 0xdc01) {
 						r->_cycles += g_ops[opCode]._cycles;
 						if (arg->_ea._value == 0xdc00)
-							r->_a &= flipfire();
+							r->_a &= flipfire(r);
 						else
-							r->_a &= flipspace();
+							r->_a &= flipspace(r);
 
 						updateFlagsNz(r, r->_a);
 						WriteToIO = 6;
@@ -1247,7 +1246,7 @@ int nextInst(CpuCtx* r) {
 
 					if ((arg->_ea._value == 0xd011) || (arg->_ea._value == 0xd012)) {
 						r->_cycles += g_ops[opCode]._cycles;
-						r->_x = _G(_byted011)[arg->_ea._value - 0xd011];
+						r->_x = r->_byted011[arg->_ea._value - 0xd011];
 						updateFlagsNz(r, r->_x);
 						WriteToIO = 5;
 						break;
@@ -1255,9 +1254,9 @@ int nextInst(CpuCtx* r) {
 					if (arg->_ea._value == 0xdc00 || arg->_ea._value == 0xdc01) {
 						r->_cycles += g_ops[opCode]._cycles;
 						if (arg->_ea._value == 0xdc00)
-							r->_x = flipfire();
+							r->_x = flipfire(r);
 						else
-							r->_x = flipspace();
+							r->_x = flipspace(r);
 						updateFlagsNz(r, r->_x);
 						WriteToIO = 6;
 						break;
@@ -1267,7 +1266,7 @@ int nextInst(CpuCtx* r) {
 
 					if ((arg->_ea._value == 0xd011) || (arg->_ea._value == 0xd012)) {
 						r->_cycles += g_ops[opCode]._cycles;
-						r->_y = _G(_byted011)[arg->_ea._value - 0xd011];
+						r->_y = r->_byted011[arg->_ea._value - 0xd011];
 						updateFlagsNz(r, r->_y);
 						WriteToIO = 5;
 						break;
@@ -1275,9 +1274,9 @@ int nextInst(CpuCtx* r) {
 					if (arg->_ea._value == 0xdc00 || arg->_ea._value == 0xdc01) {
 						r->_cycles += g_ops[opCode]._cycles;
 						if (arg->_ea._value == 0xdc00)
-							r->_y = flipfire();
+							r->_y = flipfire(r);
 						else
-							r->_y = flipspace();
+							r->_y = flipspace(r);
 						updateFlagsNz(r, r->_y);
 						WriteToIO = 6;
 						break;
@@ -1288,7 +1287,7 @@ int nextInst(CpuCtx* r) {
 
 					if ((arg->_ea._value == 0xd011) || (arg->_ea._value == 0xd012)) {
 						r->_cycles += g_ops[opCode]._cycles;
-						bt = _G(_byted011)[arg->_ea._value - 0xd011];
+						bt = r->_byted011[arg->_ea._value - 0xd011];
 						r->_flags &= ~(FLAG_N | FLAG_V | FLAG_Z);
 						r->_flags |= (bt & FLAG_N) != 0 ? FLAG_N : 0;
 						r->_flags |= (bt & FLAG_V) != 0 ? FLAG_V : 0;
@@ -1302,7 +1301,7 @@ int nextInst(CpuCtx* r) {
 				case 0xcc: /* cpy $ffff */
 					if ((arg->_ea._value == 0xd011) || (arg->_ea._value == 0xd012)) {
 						r->_cycles += g_ops[opCode]._cycles;
-						bt = _G(_byted011)[arg->_ea._value - 0xd011];
+						bt = r->_byted011[arg->_ea._value - 0xd011];
 						br = r->_a;
 						if (opCode == 0xec)
 							br = r->_x;
@@ -1321,9 +1320,9 @@ int nextInst(CpuCtx* r) {
 						if (opCode == 0xcc)
 							br = r->_y;
 						if (arg->_ea._value == 0xdc00)
-							bt = flipfire();
+							bt = flipfire(r);
 						else
-							bt = flipspace();
+							bt = flipspace(r);
 						subtract(r, 1, br, bt);
 						WriteToIO = 6;
 						break;
@@ -1345,5 +1344,5 @@ int nextInst(CpuCtx* r) {
 	return 0;
 }
 
-} // End of namespace Scott
-} // End of namespace Glk
+} // End of namespace Unp64
+} // End of namespace Common
diff --git a/engines/glk/scott/unp64/6502_emu.h b/common/compression/unp64/6502_emu.h
similarity index 82%
rename from engines/glk/scott/unp64/6502_emu.h
rename to common/compression/unp64/6502_emu.h
index 377f8368804..50e0615bf4f 100644
--- a/engines/glk/scott/unp64/6502_emu.h
+++ b/common/compression/unp64/6502_emu.h
@@ -41,28 +41,31 @@
  *
  */
 
-#ifndef GLK_SCOTT_6502_EMU_H
-#define GLK_SCOTT_6502_EMU_H
+#ifndef COMMON_COMPRESSION_UNP64_6502_EMU_H
+#define COMMON_COMPRESSION_UNP64_6502_EMU_H
 
-#include "glk/scott/types.h"
+#include "common/scummsys.h"
 
-namespace Glk {
-namespace Scott {
+namespace Common {
+namespace Unp64 {
 
 struct CpuCtx {
-	uint32_t _cycles;
-	uint16_t _pc;
-	uint8_t *_mem;
-	uint8_t _sp;
-	uint8_t _flags;
-	uint8_t _a;
-	uint8_t _x;
-	uint8_t _y;
+	uint32 _cycles;
+	uint16 _pc;
+	uint8 *_mem;
+	uint8 _sp;
+	uint8 _flags;
+	uint8 _a;
+	uint8 _x;
+	uint8 _y;
+	int _byted011[2] = {0, 0};
+	int _retfire = 0xff;
+	int _retspace = 0xff;
 };
 
 int nextInst(CpuCtx *r);
 
-} // End of namespace Scott
-} // End of namespace Glk
+} // End of namespace Unp64
+} // End of namespace Common
 
 #endif
diff --git a/engines/glk/scott/unp64/exo_util.cpp b/common/compression/unp64/exo_util.cpp
similarity index 82%
rename from engines/glk/scott/unp64/exo_util.cpp
rename to common/compression/unp64/exo_util.cpp
index 4a9da958a69..dbfda94060c 100644
--- a/engines/glk/scott/unp64/exo_util.cpp
+++ b/common/compression/unp64/exo_util.cpp
@@ -42,10 +42,10 @@
  */
 
 #include "common/util.h"
-#include "glk/scott/unp64/exo_util.h"
+#include "common/compression/unp64/exo_util.h"
 
-namespace Glk {
-namespace Scott {
+namespace Common {
+namespace Unp64 {
 
 int findSys(const byte *buf, int target) {
 	int outstart = -1;
@@ -123,7 +123,7 @@ int findSys(const byte *buf, int target) {
 	return outstart;
 }
 
-static void loadPrgData(byte mem[65536], uint8_t *data, size_t dataLength, LoadInfo *info) {
+static void loadPrgData(byte mem[65536], const byte *data, uint32 dataLength, LoadInfo *info) {
 	int len = MIN(65536 - info->_start, static_cast<int>(dataLength));
 	memcpy(mem + info->_start, data, (size_t)len);
 
@@ -135,7 +135,7 @@ static void loadPrgData(byte mem[65536], uint8_t *data, size_t dataLength, LoadI
 	}
 }
 
-void loadData(uint8_t *data, size_t dataLength, byte mem[65536], LoadInfo *info) {
+void loadData(const byte *data, uint32 dataLength, byte mem[65536], LoadInfo *info) {
 	int load = data[0] + data[1] * 0x100;
 
 	info->_start = load;
@@ -180,7 +180,7 @@ int strToInt(const char *str, int *value) {
 	return status;
 }
 
-bool u32eq(const unsigned char *addr, uint32_t val)
+bool u32eq(const unsigned char *addr, uint32 val)
 {
 	return addr[3] == (val >> 24) &&
 	addr[2] == ((val >> 16) & 0xff) &&
@@ -188,48 +188,48 @@ bool u32eq(const unsigned char *addr, uint32_t val)
 	addr[0] == (val & 0xff);
 }
 
-bool u32eqmasked(const unsigned char *addr, uint32_t mask, uint32_t val)
+bool u32eqmasked(const unsigned char *addr, uint32 mask, uint32 val)
 {
-	uint32_t val1 = addr[0] | (addr[1] << 8) | (addr[2] << 16) | (addr[3] << 24);
+	uint32 val1 = addr[0] | (addr[1] << 8) | (addr[2] << 16) | (addr[3] << 24);
 	return (val1 & mask) == val;
 }
 
-bool u32eqxored(const unsigned char *addr, uint32_t xormask, uint32_t val)
+bool u32eqxored(const unsigned char *addr, uint32 xormask, uint32 val)
 {
-	uint32_t val1 = addr[0] | (addr[1] << 8) | (addr[2] << 16) | (addr[3] << 24);
+	uint32 val1 = addr[0] | (addr[1] << 8) | (addr[2] << 16) | (addr[3] << 24);
 	return (val1 ^ xormask) == val;
 }
 
-bool u16eqmasked(const unsigned char *addr, uint16_t mask, uint16_t val)
+bool u16eqmasked(const unsigned char *addr, uint16 mask, uint16 val)
 {
-	uint16_t val1 = addr[0] | (addr[1] << 8);
+	uint16 val1 = addr[0] | (addr[1] << 8);
 	return (val1 & mask) == val;
 }
 
 
-bool u16eq(const unsigned char *addr, uint16_t val)
+bool u16eq(const unsigned char *addr, uint16 val)
 {
 	return addr[1] == (val >> 8) &&
 	addr[0] == (val & 0xff);
 }
 
-bool u16noteq(const unsigned char *addr, uint16_t val)
+bool u16noteq(const unsigned char *addr, uint16 val)
 {
 	return addr[1] != (val >> 8) ||
 	addr[0] != (val & 0xff);
 }
 
-bool u16gteq(const unsigned char *addr, uint16_t val)
+bool u16gteq(const unsigned char *addr, uint16 val)
 {
-	uint16_t val2 = addr[0] | (addr[1] << 8);
+	uint16 val2 = addr[0] | (addr[1] << 8);
 	return val2 >= val;
 }
 
-bool u16lteq(const unsigned char *addr, uint16_t val)
+bool u16lteq(const unsigned char *addr, uint16 val)
 {
-	uint16_t val2 = addr[0] | (addr[1] << 8);
+	uint16 val2 = addr[0] | (addr[1] << 8);
 	return val2 <= val;
 }
 
-} // End of namespace Scott
-} // End of namespace Glk
+} // End of namespace Unp64
+} // End of namespace Common
diff --git a/engines/glk/scott/unp64/exo_util.h b/common/compression/unp64/exo_util.h
similarity index 73%
rename from engines/glk/scott/unp64/exo_util.h
rename to common/compression/unp64/exo_util.h
index d4b6b137326..5dc04765f88 100644
--- a/engines/glk/scott/unp64/exo_util.h
+++ b/common/compression/unp64/exo_util.h
@@ -41,13 +41,13 @@
  *
  */
 
-#ifndef GLK_SCOTT_EXO_UTIL_H
-#define GLK_SCOTT_EXO_UTIL_H
+#ifndef COMMON_COMPRESSION_UNP64_EXO_UTIL_H
+#define COMMON_COMPRESSION_UNP64_EXO_UTIL_H
 
-#include "glk/scott/types.h"
+#include "common/scummsys.h"
 
-namespace Glk {
-namespace Scott {
+namespace Common {
+namespace Unp64 {
 
 struct LoadInfo {
 	int _basicTxtStart; /* in */
@@ -59,20 +59,20 @@ struct LoadInfo {
 
 int findSys(const byte *buf, int target);
 
-void loadData(uint8_t *data, size_t dataLength, byte mem[65536], LoadInfo *info);
+void loadData(const byte *data, uint32 dataLength, byte mem[65536], LoadInfo *info);
 
 int strToInt(const char *str, int *value);
 
-bool u32eq(const unsigned char *addr, uint32_t val);
-bool u16eq(const unsigned char *addr, uint16_t val);
-bool u16gteq(const unsigned char *addr, uint16_t val);
-bool u16lteq(const unsigned char *addr, uint16_t val);
-bool u16noteq(const unsigned char *addr, uint16_t val);
-bool u32eqmasked(const unsigned char *addr, uint32_t mask, uint32_t val);
-bool u32eqxored(const unsigned char *addr, uint32_t ormask, uint32_t val);
-bool u16eqmasked(const unsigned char *addr, uint16_t mask, uint16_t val);
+bool u32eq(const unsigned char *addr, uint32 val);
+bool u16eq(const unsigned char *addr, uint16 val);
+bool u16gteq(const unsigned char *addr, uint16 val);
+bool u16lteq(const unsigned char *addr, uint16 val);
+bool u16noteq(const unsigned char *addr, uint16 val);
+bool u32eqmasked(const unsigned char *addr, uint32 mask, uint32 val);
+bool u32eqxored(const unsigned char *addr, uint32 ormask, uint32 val);
+bool u16eqmasked(const unsigned char *addr, uint16 mask, uint16 val);
 
-} // End of namespace Scott
-} // End of namespace Glk
+} // End of namespace Unp64
+} // End of namespace Common
 
 #endif
diff --git a/engines/glk/scott/unp64/scanners/action_packer.cpp b/common/compression/unp64/scanners/action_packer.cpp
similarity index 86%
rename from engines/glk/scott/unp64/scanners/action_packer.cpp
rename to common/compression/unp64/scanners/action_packer.cpp
index 545ed40ed9c..1c2fc23cd18 100644
--- a/engines/glk/scott/unp64/scanners/action_packer.cpp
+++ b/common/compression/unp64/scanners/action_packer.cpp
@@ -20,12 +20,12 @@
  */
 
 #include "common/endian.h"
-#include "glk/scott/types.h"
-#include "glk/scott/unp64/unp64.h"
-#include "glk/scott/unp64/exo_util.h"
+#include "common/scummsys.h"
+#include "common/compression/unp64/unp64.h"
+#include "common/compression/unp64/exo_util.h"
 
-namespace Glk {
-namespace Scott {
+namespace Common {
+namespace Unp64 {
 
 void scnActionPacker(UnpStr *unp) {
 	byte *mem;
@@ -49,5 +49,5 @@ void scnActionPacker(UnpStr *unp) {
 	}
 }
 
-} // End of namespace Scott
-} // End of namespace Glk
+} // End of namespace Unp64
+} // End of namespace Common
diff --git a/engines/glk/scott/unp64/scanners/byte_boiler.cpp b/common/compression/unp64/scanners/byte_boiler.cpp
similarity index 92%
rename from engines/glk/scott/unp64/scanners/byte_boiler.cpp
rename to common/compression/unp64/scanners/byte_boiler.cpp
index cf0a2b50f6d..7c79a712592 100644
--- a/engines/glk/scott/unp64/scanners/byte_boiler.cpp
+++ b/common/compression/unp64/scanners/byte_boiler.cpp
@@ -20,12 +20,12 @@
  */
 
 #include "common/endian.h"
-#include "glk/scott/types.h"
-#include "glk/scott/unp64/unp64.h"
-#include "glk/scott/unp64/exo_util.h"
+#include "common/scummsys.h"
+#include "common/compression/unp64/unp64.h"
+#include "common/compression/unp64/exo_util.h"
 
-namespace Glk {
-namespace Scott {
+namespace Common {
+namespace Unp64 {
 
 void scnByteBoiler(UnpStr *unp) {
 	byte *mem;
@@ -90,5 +90,5 @@ void scnByteBoiler(UnpStr *unp) {
 	}
 }
 
-} // End of namespace Scott
-} // End of namespace Glk
+} // End of namespace Unp64
+} // End of namespace Common
diff --git a/engines/glk/scott/unp64/scanners/caution.cpp b/common/compression/unp64/scanners/caution.cpp
similarity index 94%
rename from engines/glk/scott/unp64/scanners/caution.cpp
rename to common/compression/unp64/scanners/caution.cpp
index 58ecaa117fd..b060b589142 100644
--- a/engines/glk/scott/unp64/scanners/caution.cpp
+++ b/common/compression/unp64/scanners/caution.cpp
@@ -20,12 +20,12 @@
  */
 
 #include "common/endian.h"
-#include "glk/scott/types.h"
-#include "glk/scott/unp64/unp64.h"
-#include "glk/scott/unp64/exo_util.h"
+#include "common/scummsys.h"
+#include "common/compression/unp64/unp64.h"
+#include "common/compression/unp64/exo_util.h"
 
-namespace Glk {
-namespace Scott {
+namespace Common {
+namespace Unp64 {
 
 void scnCaution(UnpStr *unp) {
 	byte *mem;
@@ -131,5 +131,5 @@ void scnCaution(UnpStr *unp) {
 	}
 }
 
-} // End of namespace Scott
-} // End of namespace Glk
+} // End of namespace Unp64
+} // End of namespace Common
diff --git a/engines/glk/scott/unp64/scanners/ccs.cpp b/common/compression/unp64/scanners/ccs.cpp
similarity index 96%
rename from engines/glk/scott/unp64/scanners/ccs.cpp
rename to common/compression/unp64/scanners/ccs.cpp
index 78ca623800a..f5ccb3ac8de 100644
--- a/engines/glk/scott/unp64/scanners/ccs.cpp
+++ b/common/compression/unp64/scanners/ccs.cpp
@@ -20,12 +20,12 @@
  */
 
 #include "common/endian.h"
-#include "glk/scott/types.h"
-#include "glk/scott/unp64/unp64.h"
-#include "glk/scott/unp64/exo_util.h"
+#include "common/scummsys.h"
+#include "common/compression/unp64/unp64.h"
+#include "common/compression/unp64/exo_util.h"
 
-namespace Glk {
-namespace Scott {
+namespace Common {
+namespace Unp64 {
 
 void scnCCS(UnpStr *unp) {
 	byte *mem;
@@ -204,5 +204,5 @@ void scnCCS(UnpStr *unp) {
 	}
 }
 
-} // End of namespace Scott
-} // End of namespace Glk
+} // End of namespace Unp64
+} // End of namespace Common
diff --git a/engines/glk/scott/unp64/scanners/cruel.cpp b/common/compression/unp64/scanners/cruel.cpp
similarity index 98%
rename from engines/glk/scott/unp64/scanners/cruel.cpp
rename to common/compression/unp64/scanners/cruel.cpp
index 97c5070ea83..bcdd823728c 100644
--- a/engines/glk/scott/unp64/scanners/cruel.cpp
+++ b/common/compression/unp64/scanners/cruel.cpp
@@ -20,12 +20,12 @@
  */
 
 #include "common/endian.h"
-#include "glk/scott/types.h"
-#include "glk/scott/unp64/unp64.h"
-#include "glk/scott/unp64/exo_util.h"
+#include "common/scummsys.h"
+#include "common/compression/unp64/unp64.h"
+#include "common/compression/unp64/exo_util.h"
 
-namespace Glk {
-namespace Scott {
+namespace Common {
+namespace Unp64 {
 
 void scnCruel(UnpStr *unp) {
 	byte *mem;
@@ -370,5 +370,5 @@ void scnCruel(UnpStr *unp) {
 	}
 }
 
-} // End of namespace Scott
-} // End of namespace Glk
+} // End of namespace Unp64
+} // End of namespace Common
diff --git a/engines/glk/scott/unp64/scanners/eca.cpp b/common/compression/unp64/scanners/eca.cpp
similarity index 96%
rename from engines/glk/scott/unp64/scanners/eca.cpp
rename to common/compression/unp64/scanners/eca.cpp
index ec443939a66..a3848927d90 100644
--- a/engines/glk/scott/unp64/scanners/eca.cpp
+++ b/common/compression/unp64/scanners/eca.cpp
@@ -20,12 +20,12 @@
  */
 
 #include "common/endian.h"
-#include "glk/scott/types.h"
-#include "glk/scott/unp64/unp64.h"
-#include "glk/scott/unp64/exo_util.h"
+#include "common/scummsys.h"
+#include "common/compression/unp64/unp64.h"
+#include "common/compression/unp64/exo_util.h"
 
-namespace Glk {
-namespace Scott {
+namespace Common {
+namespace Unp64 {
 
 void scnECA(UnpStr *unp) {
 	byte *mem;
@@ -181,5 +181,5 @@ void scnECA(UnpStr *unp) {
 	}
 }
 
-} // End of namespace Scott
-} // End of namespace Glk
+} // End of namespace Unp64
+} // End of namespace Common
diff --git a/engines/glk/scott/unp64/scanners/exomizer.cpp b/common/compression/unp64/scanners/exomizer.cpp
similarity index 95%
rename from engines/glk/scott/unp64/scanners/exomizer.cpp
rename to common/compression/unp64/scanners/exomizer.cpp
index 356d7f50e94..bf6fb2c6934 100644
--- a/engines/glk/scott/unp64/scanners/exomizer.cpp
+++ b/common/compression/unp64/scanners/exomizer.cpp
@@ -20,12 +20,12 @@
  */
 
 #include "common/endian.h"
-#include "glk/scott/types.h"
-#include "glk/scott/unp64/unp64.h"
-#include "glk/scott/unp64/exo_util.h"
+#include "common/scummsys.h"
+#include "common/compression/unp64/unp64.h"
+#include "common/compression/unp64/exo_util.h"
 
-namespace Glk {
-namespace Scott {
+namespace Common {
+namespace Unp64 {
 
 void scnExomizer(UnpStr *unp) {
 	byte *mem;
@@ -173,5 +173,5 @@ void scnExomizer(UnpStr *unp) {
 	}
 }
 
-} // End of namespace Scott
-} // End of namespace Glk
+} // End of namespace Unp64
+} // End of namespace Common
diff --git a/engines/glk/scott/unp64/scanners/expert.cpp b/common/compression/unp64/scanners/expert.cpp
similarity index 96%
rename from engines/glk/scott/unp64/scanners/expert.cpp
rename to common/compression/unp64/scanners/expert.cpp
index 1db0a19944c..4c99f78882c 100644
--- a/engines/glk/scott/unp64/scanners/expert.cpp
+++ b/common/compression/unp64/scanners/expert.cpp
@@ -20,12 +20,12 @@
  */
 
 #include "common/endian.h"
-#include "glk/scott/types.h"
-#include "glk/scott/unp64/unp64.h"
-#include "glk/scott/unp64/exo_util.h"
+#include "common/scummsys.h"
+#include "common/compression/unp64/unp64.h"
+#include "common/compression/unp64/exo_util.h"
 
-namespace Glk {
-namespace Scott {
+namespace Common {
+namespace Unp64 {
 
 void scnExpert(UnpStr *unp) {
 	byte *mem;
@@ -243,5 +243,5 @@ void scnExpert(UnpStr *unp) {
 	}
 }
 
-} // End of namespace Scott
-} // End of namespace Glk
+} // End of namespace Unp64
+} // End of namespace Common
diff --git a/engines/glk/scott/unp64/scanners/master_compressor.cpp b/common/compression/unp64/scanners/master_compressor.cpp
similarity index 94%
rename from engines/glk/scott/unp64/scanners/master_compressor.cpp
rename to common/compression/unp64/scanners/master_compressor.cpp
index 33927026740..b26cb5e044f 100644
--- a/engines/glk/scott/unp64/scanners/master_compressor.cpp
+++ b/common/compression/unp64/scanners/master_compressor.cpp
@@ -20,12 +20,12 @@
  */
 
 #include "common/endian.h"
-#include "glk/scott/types.h"
-#include "glk/scott/unp64/unp64.h"
-#include "glk/scott/unp64/exo_util.h"
+#include "common/scummsys.h"
+#include "common/compression/unp64/unp64.h"
+#include "common/compression/unp64/exo_util.h"
 
-namespace Glk {
-namespace Scott {
+namespace Common {
+namespace Unp64 {
 
 void scnMasterCompressor(UnpStr *unp) {
 	byte *mem;
@@ -121,5 +121,5 @@ void scnMasterCompressor(UnpStr *unp) {
 	}
 }
 
-} // End of namespace Scott
-} // End of namespace Glk
+} // End of namespace Unp64
+} // End of namespace Common
diff --git a/engines/glk/scott/unp64/scanners/megabyte.cpp b/common/compression/unp64/scanners/megabyte.cpp
similarity index 91%
rename from engines/glk/scott/unp64/scanners/megabyte.cpp
rename to common/compression/unp64/scanners/megabyte.cpp
index a63706d451b..6cfa5ab97bc 100644
--- a/engines/glk/scott/unp64/scanners/megabyte.cpp
+++ b/common/compression/unp64/scanners/megabyte.cpp
@@ -20,12 +20,12 @@
  */
 
 #include "common/endian.h"
-#include "glk/scott/types.h"
-#include "glk/scott/unp64/unp64.h"
-#include "glk/scott/unp64/exo_util.h"
+#include "common/scummsys.h"
+#include "common/compression/unp64/unp64.h"
+#include "common/compression/unp64/exo_util.h"
 
-namespace Glk {
-namespace Scott {
+namespace Common {
+namespace Unp64 {
 
 void scnMegabyte(UnpStr *unp) {
 	byte *mem;
@@ -78,5 +78,5 @@ void scnMegabyte(UnpStr *unp) {
 	}
 }
 
-} // End of namespace Scott
-} // End of namespace Glk
+} // End of namespace Unp64
+} // End of namespace Common
diff --git a/engines/glk/scott/unp64/scanners/pu_crunch.cpp b/common/compression/unp64/scanners/pu_crunch.cpp
similarity index 96%
rename from engines/glk/scott/unp64/scanners/pu_crunch.cpp
rename to common/compression/unp64/scanners/pu_crunch.cpp
index 2b3ed04a293..80b65ccdcf9 100644
--- a/engines/glk/scott/unp64/scanners/pu_crunch.cpp
+++ b/common/compression/unp64/scanners/pu_crunch.cpp
@@ -20,12 +20,12 @@
  */
 
 #include "common/endian.h"
-#include "glk/scott/types.h"
-#include "glk/scott/unp64/unp64.h"
-#include "glk/scott/unp64/exo_util.h"
+#include "common/scummsys.h"
+#include "common/compression/unp64/unp64.h"
+#include "common/compression/unp64/exo_util.h"
 
-namespace Glk {
-namespace Scott {
+namespace Common {
+namespace Unp64 {
 
 void scnPuCrunch(UnpStr *unp) {
 	byte *mem;
@@ -190,5 +190,5 @@ void scnPuCrunch(UnpStr *unp) {
 	}
 }
 
-} // End of namespace Scott
-} // End of namespace Glk
+} // End of namespace Unp64
+} // End of namespace Common
diff --git a/engines/glk/scott/unp64/scanners/scanners.cpp b/common/compression/unp64/scanners/scanners.cpp
similarity index 92%
rename from engines/glk/scott/unp64/scanners/scanners.cpp
rename to common/compression/unp64/scanners/scanners.cpp
index e05722f28f6..cdd537bd9cd 100644
--- a/engines/glk/scott/unp64/scanners/scanners.cpp
+++ b/common/compression/unp64/scanners/scanners.cpp
@@ -19,11 +19,11 @@
  *
  */
 
-#include "glk/scott/unp64/unp64.h"
+#include "common/compression/unp64/unp64.h"
 #include "common/util.h"
 
-namespace Glk {
-namespace Scott {
+namespace Common {
+namespace Unp64 {
 
 void scnECA(UnpStr *unp);
 void scnExpert(UnpStr *unp);
@@ -69,5 +69,5 @@ void scanners(UnpStr* unp) {
 	}
 }
 
-} // End of namespace Scott
-} // End of namespace Glk
+} // End of namespace Unp64
+} // End of namespace Common
diff --git a/engines/glk/scott/unp64/scanners/section8.cpp b/common/compression/unp64/scanners/section8.cpp
similarity index 93%
rename from engines/glk/scott/unp64/scanners/section8.cpp
rename to common/compression/unp64/scanners/section8.cpp
index 3361e09c6fc..a268a50fe74 100644
--- a/engines/glk/scott/unp64/scanners/section8.cpp
+++ b/common/compression/unp64/scanners/section8.cpp
@@ -20,12 +20,12 @@
  */
 
 #include "common/endian.h"
-#include "glk/scott/types.h"
-#include "glk/scott/unp64/unp64.h"
-#include "glk/scott/unp64/exo_util.h"
+#include "common/scummsys.h"
+#include "common/compression/unp64/unp64.h"
+#include "common/compression/unp64/exo_util.h"
 
-namespace Glk {
-namespace Scott {
+namespace Common {
+namespace Unp64 {
 
 void scnSection8(UnpStr *unp) {
 	byte *mem;
@@ -99,5 +99,5 @@ void scnSection8(UnpStr *unp) {
 	}
 }
 
-} // End of namespace Scott
-} // End of namespace Glk
+} // End of namespace Unp64
+} // End of namespace Common
diff --git a/engines/glk/scott/unp64/scanners/tbc_multicomp.cpp b/common/compression/unp64/scanners/tbc_multicomp.cpp
similarity index 95%
rename from engines/glk/scott/unp64/scanners/tbc_multicomp.cpp
rename to common/compression/unp64/scanners/tbc_multicomp.cpp
index 068779f1e96..bdb69f780dd 100644
--- a/engines/glk/scott/unp64/scanners/tbc_multicomp.cpp
+++ b/common/compression/unp64/scanners/tbc_multicomp.cpp
@@ -20,12 +20,12 @@
  */
 
 #include "common/endian.h"
-#include "glk/scott/types.h"
-#include "glk/scott/unp64/unp64.h"
-#include "glk/scott/unp64/exo_util.h"
+#include "common/scummsys.h"
+#include "common/compression/unp64/unp64.h"
+#include "common/compression/unp64/exo_util.h"
 
-namespace Glk {
-namespace Scott {
+namespace Common {
+namespace Unp64 {
 
 void scnTBCMultiComp(UnpStr *unp) {
 	byte *mem;
@@ -162,5 +162,5 @@ void scnTBCMultiComp(UnpStr *unp) {
 	}
 }
 
-} // End of namespace Scott
-} // End of namespace Glk
+} // End of namespace Unp64
+} // End of namespace Common
diff --git a/engines/glk/scott/unp64/scanners/tcs_crunch.cpp b/common/compression/unp64/scanners/tcs_crunch.cpp
similarity index 91%
rename from engines/glk/scott/unp64/scanners/tcs_crunch.cpp
rename to common/compression/unp64/scanners/tcs_crunch.cpp
index b10243f5dc9..dbe59279e48 100644
--- a/engines/glk/scott/unp64/scanners/tcs_crunch.cpp
+++ b/common/compression/unp64/scanners/tcs_crunch.cpp
@@ -20,12 +20,12 @@
  */
 
 #include "common/endian.h"
-#include "glk/scott/types.h"
-#include "glk/scott/unp64/unp64.h"
-#include "glk/scott/unp64/exo_util.h"
+#include "common/scummsys.h"
+#include "common/compression/unp64/unp64.h"
+#include "common/compression/unp64/exo_util.h"
 
-namespace Glk {
-namespace Scott {
+namespace Common {
+namespace Unp64 {
 
 void scnTCScrunch(UnpStr *unp) {
 	byte *mem;
@@ -74,5 +74,5 @@ void scnTCScrunch(UnpStr *unp) {
 	}
 }
 
-} // End of namespace Scott
-} // End of namespace Glk
+} // End of namespace Unp64
+} // End of namespace Common
diff --git a/engines/glk/scott/unp64/scanners/xtc.cpp b/common/compression/unp64/scanners/xtc.cpp
similarity index 96%
rename from engines/glk/scott/unp64/scanners/xtc.cpp
rename to common/compression/unp64/scanners/xtc.cpp
index a480e8646c6..79b47ca139a 100644
--- a/engines/glk/scott/unp64/scanners/xtc.cpp
+++ b/common/compression/unp64/scanners/xtc.cpp
@@ -20,12 +20,12 @@
  */
 
 #include "common/endian.h"
-#include "glk/scott/types.h"
-#include "glk/scott/unp64/unp64.h"
-#include "glk/scott/unp64/exo_util.h"
+#include "common/scummsys.h"
+#include "common/compression/unp64/unp64.h"
+#include "common/compression/unp64/exo_util.h"
 
-namespace Glk {
-namespace Scott {
+namespace Common {
+namespace Unp64 {
 
 void scnXTC(UnpStr *unp) {
 	byte *mem;
@@ -158,5 +158,5 @@ void scnXTC(UnpStr *unp) {
 	}
 }
 
-} // End of namespace Scott
-} // End of namespace Glk
+} // End of namespace Unp64
+} // End of namespace Common
diff --git a/engines/glk/scott/unp64/unp64.cpp b/common/compression/unp64/unp64.cpp
similarity index 57%
rename from engines/glk/scott/unp64/unp64.cpp
rename to common/compression/unp64/unp64.cpp
index 95beae50226..140bdc7e6d6 100644
--- a/engines/glk/scott/unp64/unp64.cpp
+++ b/common/compression/unp64/unp64.cpp
@@ -19,9 +19,7 @@
  *
  */
 
-// This is a cut-down version of UNP64 with only the bare minimum
-// needed to decompress a number of Scott Adams Commodore 64 games
-// for the ScottFree interpreter.
+// Adapted from the ScottFree version of UNP64 for shared use in ScummVM.
 
 /*
 UNP64 - generic Commodore 64 prg unpacker
@@ -53,51 +51,51 @@ Follows original disclaimer
  *
  */
 
-#include "glk/scott/globals.h"
-#include "glk/scott/types.h"
-#include "glk/scott/unp64/6502_emu.h"
-#include "glk/scott/unp64/exo_util.h"
-#include "glk/scott/unp64/unp64.h"
+#include "common/compression/unp64.h"
+#include "common/compression/unp64/6502_emu.h"
+#include "common/compression/unp64/exo_util.h"
+#include "common/compression/unp64/unp64.h"
 
+#include "common/str.h"
 #include "common/util.h"
 
-namespace Glk {
-namespace Scott {
-
-void reinitUnp(void) {
-	_G(_unp)._idFlag = 0;
-	_G(_unp)._forced = 0;
-	_G(_unp)._strMem = 0x800;
-	_G(_unp)._retAdr = 0x800;
-	_G(_unp)._depAdr = 0;
-	_G(_unp)._endAdr = 0x10000;
-	_G(_unp)._rtAFrc = 0;
-	_G(_unp)._wrMemF = 0;
-	_G(_unp)._lfMemF = 0;
-	_G(_unp)._exoFnd = 0;
-	_G(_unp)._ecaFlg = 0;
-	_G(_unp)._fEndBf = 0;
-	_G(_unp)._fEndAf = 0;
-	_G(_unp)._fStrAf = 0;
-	_G(_unp)._fStrBf = 0;
-	_G(_unp)._mon1st = 0;
+namespace Common {
+namespace Unp64 {
+
+static void reinitUnp(UnpStr &unp) {
+	unp._idFlag = 0;
+	unp._forced = 0;
+	unp._strMem = 0x800;
+	unp._retAdr = 0x800;
+	unp._depAdr = 0;
+	unp._endAdr = 0x10000;
+	unp._rtAFrc = 0;
+	unp._wrMemF = 0;
+	unp._lfMemF = 0;
+	unp._exoFnd = 0;
+	unp._ecaFlg = 0;
+	unp._fEndBf = 0;
+	unp._fEndAf = 0;
+	unp._fStrAf = 0;
+	unp._fStrBf = 0;
+	unp._mon1st = 0;
 }
 
-int isBasicRun1(int pc) {
+static int isBasicRun1(int pc) {
 	if (pc == 0xa7ae || pc == 0xa7ea || pc == 0xa7b1 || pc == 0xa474 || pc == 0xa533 || pc == 0xa871 || pc == 0xa888 || pc == 0xa8bc)
 		return 1;
 	else
 		return 0;
 }
 
-int isBasicRun2(int pc) {
+static int isBasicRun2(int pc) {
 	if (isBasicRun1(pc) || ((pc >= 0xA57C) && (pc <= 0xA659)) || pc == 0xa660 || pc == 0xa68e)
 		return 1;
 	else
 		return 0;
 }
 
-int unp64(byte *compressed, size_t length, byte *destinationBuffer, size_t *finalLength, const char *switches) {
+int unp64(const byte *compressed, uint32 length, byte *destinationBuffer, uint32 *finalLength, const char *switches) {
 
 	char settings[4][64];
 	int numSettings = 0;
@@ -116,6 +114,8 @@ int unp64(byte *compressed, size_t length, byte *destinationBuffer, size_t *fina
 		}
 	}
 
+	UnpStr unp;
+	int iter = 0;
 	CpuCtx r[1];
 	LoadInfo info[1];
 	char name[260] = {0}, forcedname[260] = {0};
@@ -155,21 +155,21 @@ int unp64(byte *compressed, size_t length, byte *destinationBuffer, size_t *fina
 	int iterMax = ITERMAX;
 	int p;
 
-	memset(&_G(_unp), 0, sizeof(_G(_unp)));
-	reinitUnp();
-	_G(_unp)._fStack = 1;
-	_G(_unp)._mem = mem;
-	_G(_unp)._r = r;
-	_G(_unp)._name = name;
-	_G(_unp)._info = info;
+	memset(&unp, 0, sizeof(unp));
+	reinitUnp(unp);
+	unp._fStack = 1;
+	unp._mem = mem;
+	unp._r = r;
+	unp._name = name;
+	unp._info = info;
 
 	p = 0;
 
 	if (numSettings != 0) {
-		if (settings[0][0] == '-' && _G(_parsePar) && settings[0][1] == 'f') {
-			strToInt(settings[p] + 2, (int *)&_G(_unp)._filler);
-			if (_G(_unp)._filler) {
-				memset(mem + (_G(_unp)._filler >> 16), _G(_unp)._filler & 0xff, 0x10000 - (_G(_unp)._filler >> 16));
+		if (settings[0][0] == '-' && settings[0][1] == 'f') {
+			strToInt(settings[p] + 2, (int *)&unp._filler);
+			if (unp._filler) {
+				memset(mem + (unp._filler >> 16), unp._filler & 0xff, 0x10000 - (unp._filler >> 16));
 			}
 			p++;
 		}
@@ -184,11 +184,11 @@ int unp64(byte *compressed, size_t length, byte *destinationBuffer, size_t *fina
 		info->_run = findSys(mem + info->_basicTxtStart, 0x9e);
 	}
 
-	scanners(&_G(_unp));
-	if (_G(_unp)._idFlag == 2)
+	scanners(&unp);
+	if (unp._idFlag == 2)
 		return 0;
 
-	if ((_G(_unp)._recurs == 0) && (numSettings > 0)) {
+	if ((unp._recurs == 0) && (numSettings > 0)) {
 		while (p < numSettings) {
 			if (settings[p][0] == '-') {
 				switch (settings[p][1]) {
@@ -196,48 +196,48 @@ int unp64(byte *compressed, size_t length, byte *destinationBuffer, size_t *fina
 					p = numSettings;
 					break;
 				case 'e':
-					strToInt(settings[p] + 2, &_G(_unp)._forced);
-					_G(_unp)._forced &= 0xffff;
-					if (_G(_unp)._forced < 0x1)
-						_G(_unp)._forced = 0;
+					strToInt(settings[p] + 2, &unp._forced);
+					unp._forced &= 0xffff;
+					if (unp._forced < 0x1)
+						unp._forced = 0;
 					break;
 				case 'a':
-					_G(_unp)._strMem = 2;
-					_G(_unp)._endAdr = 0x10001;
-					_G(_unp)._fEndAf = 0;
-					_G(_unp)._fStrAf = 0;
-					_G(_unp)._strAdC = 0;
-					_G(_unp)._endAdC = 0;
-					_G(_unp)._monEnd = 0;
-					_G(_unp)._monStr = 0;
+					unp._strMem = 2;
+					unp._endAdr = 0x10001;
+					unp._fEndAf = 0;
+					unp._fStrAf = 0;
+					unp._strAdC = 0;
+					unp._endAdC = 0;
+					unp._monEnd = 0;
+					unp._monStr = 0;
 					break;
 				case 'r':
-					strToInt(settings[p] + 2, &_G(_unp)._retAdr);
-					_G(_unp)._retAdr &= 0xffff;
+					strToInt(settings[p] + 2, &unp._retAdr);
+					unp._retAdr &= 0xffff;
 					break;
 				case 'R':
-					strToInt(settings[p] + 2, &_G(_unp)._retAdr);
-					_G(_unp)._retAdr &= 0xffff;
-					_G(_unp)._rtAFrc = 1;
+					strToInt(settings[p] + 2, &unp._retAdr);
+					unp._retAdr &= 0xffff;
+					unp._rtAFrc = 1;
 					break;
 				case 'd':
-					strToInt(settings[p] + 2, &_G(_unp)._depAdr);
-					_G(_unp)._depAdr &= 0xffff;
+					strToInt(settings[p] + 2, &unp._depAdr);
+					unp._depAdr &= 0xffff;
 					break;
 				case 't':
-					strToInt(settings[p] + 2, &_G(_unp)._endAdr);
-					_G(_unp)._endAdr &= 0xffff;
-					if (_G(_unp)._endAdr >= 0x100)
-						_G(_unp)._endAdr++;
+					strToInt(settings[p] + 2, &unp._endAdr);
+					unp._endAdr &= 0xffff;
+					if (unp._endAdr >= 0x100)
+						unp._endAdr++;
 					break;
 				case 'u':
-					_G(_unp)._wrMemF = 1;
+					unp._wrMemF = 1;
 					break;
 				case 'l':
-					_G(_unp)._lfMemF = info->_end;
+					unp._lfMemF = info->_end;
 					break;
 				case 's':
-					_G(_unp)._fStack = 0;
+					unp._fStack = 0;
 					break;
 				case 'x':
 					break;
@@ -248,7 +248,7 @@ int unp64(byte *compressed, size_t length, byte *destinationBuffer, size_t *fina
 					//copyRoms[1][1] = 1;
 					break;
 				case 'c':
-					_G(_unp)._recurs++;
+					unp._recurs++;
 					break;
 				case 'm': // keep undocumented for now
 					strToInt(settings[p] + 2, &iterMax);
@@ -258,32 +258,32 @@ int unp64(byte *compressed, size_t length, byte *destinationBuffer, size_t *fina
 		}
 	}
 
-	if (_G(_unp)._idOnly) {
-		if (_G(_unp)._depAdr == 0)
+	if (unp._idOnly) {
+		if (unp._depAdr == 0)
 			return 0;
 	}
 
-	if (_G(_unp)._wrMemF | _G(_unp)._lfMemF) {
+	if (unp._wrMemF | unp._lfMemF) {
 		memcpy(oldmem, mem, sizeof(oldmem));
 	}
 
-	if (_G(_unp)._forced) {
-		info->_run = _G(_unp)._forced;
+	if (unp._forced) {
+		info->_run = unp._forced;
 	}
 
 	if (info->_run == -1) {
 		return 0;
 	}
 
-	if (_G(_unp)._strMem > _G(_unp)._retAdr) {
-		_G(_unp)._strMem = _G(_unp)._retAdr;
+	if (unp._strMem > unp._retAdr) {
+		unp._strMem = unp._retAdr;
 	}
 
 	mem[0] = 0x60;
 	r->_cycles = 0;
 	mem[1] = 0x37;
 
-	if (((_G(_unp)._forced >= 0xa000) && (_G(_unp)._forced < 0xc000)) || (_G(_unp)._forced >= 0xd000))
+	if (((unp._forced >= 0xa000) && (unp._forced < 0xc000)) || (unp._forced >= 0xd000))
 		mem[1] = 0x38;
 
 	/* some packers rely on basic pointers already set */
@@ -311,7 +311,7 @@ int unp64(byte *compressed, size_t length, byte *destinationBuffer, size_t *fina
 	mem[0x52] = 0;
 	mem[0x53] = 3;
 
-	if (_G(_unp)._fStack) {
+	if (unp._fStack) {
 		memcpy(mem + 0x100, stack,
 			   sizeof(stack)); /* stack as found on clean start */
 		r->_sp = 0xf6;         /* sys from immediate mode leaves $f6 in stackptr */
@@ -335,8 +335,8 @@ int unp64(byte *compressed, size_t length, byte *destinationBuffer, size_t *fina
 		r->_x = 0;
 	}
 
-	_G(_iter) = 0;
-	while ((_G(_unp)._depAdr ? r->_pc != _G(_unp)._depAdr : r->_pc >= _G(_unp)._retAdr)) {
+	iter = 0;
+	while ((unp._depAdr ? r->_pc != unp._depAdr : r->_pc >= unp._retAdr)) {
 		if ((((mem[1] & 0x7) >= 6) && (r->_pc >= 0xe000)) || ((r->_pc >= 0xa000) && (r->_pc <= 0xbfff) && ((mem[1] & 0x7) > 6))) {
 			/* some packer relies on regs set at return from CLRSCR */
 			if ((r->_pc == 0xe536) || (r->_pc == 0xe544) || (r->_pc == 0xff5b) || ((r->_pc == 0xffd2) && (r->_a == 0x93))) {
@@ -408,39 +408,39 @@ int unp64(byte *compressed, size_t length, byte *destinationBuffer, size_t *fina
 		if (nextInst(r) == 1)
 			return 0;
 
-		_G(_iter)++;
- 		if (_G(_iter) == iterMax) {
+		iter++;
+		if (iter == iterMax) {
 				return 0;
 		}
 
-		if (_G(_unp)._exoFnd && (_G(_unp)._endAdr == 0x10000) && (r->_pc >= 0x100) && (r->_pc <= 0x200) && (_G(_unp)._strMem != 2)) {
-			_G(_unp)._endAdr = r->_mem[0xfe] + (r->_mem[0xff] << 8);
-			if ((_G(_unp)._exoFnd & 0xff) == 0x30) { /* low byte of _endAdr, it's a lda $ff00,y */
-				_G(_unp)._endAdr = (_G(_unp)._exoFnd >> 8) + (r->_mem[0xff] << 8);
-			} else if ((_G(_unp)._exoFnd & 0xff) == 0x32) { /* add 1 */
-				_G(_unp)._endAdr = 1 + ((_G(_unp)._exoFnd >> 8) + (r->_mem[0xff] << 8));
+		if (unp._exoFnd && (unp._endAdr == 0x10000) && (r->_pc >= 0x100) && (r->_pc <= 0x200) && (unp._strMem != 2)) {
+			unp._endAdr = r->_mem[0xfe] + (r->_mem[0xff] << 8);
+			if ((unp._exoFnd & 0xff) == 0x30) { /* low byte of _endAdr, it's a lda $ff00,y */
+				unp._endAdr = (unp._exoFnd >> 8) + (r->_mem[0xff] << 8);
+			} else if ((unp._exoFnd & 0xff) == 0x32) { /* add 1 */
+				unp._endAdr = 1 + ((unp._exoFnd >> 8) + (r->_mem[0xff] << 8));
 			}
 
-			if (_G(_unp)._endAdr == 0)
-				_G(_unp)._endAdr = 0x10001;
+			if (unp._endAdr == 0)
+				unp._endAdr = 0x10001;
 		}
 
-		if (_G(_unp)._fEndBf && (_G(_unp)._endAdr == 0x10000) && (r->_pc == _G(_unp)._depAdr)) {
-			_G(_unp)._endAdr = r->_mem[_G(_unp)._fEndBf] | r->_mem[_G(_unp)._fEndBf + 1] << 8;
-			_G(_unp)._endAdr++;
+		if (unp._fEndBf && (unp._endAdr == 0x10000) && (r->_pc == unp._depAdr)) {
+			unp._endAdr = r->_mem[unp._fEndBf] | r->_mem[unp._fEndBf + 1] << 8;
+			unp._endAdr++;
 
-			if (_G(_unp)._endAdr == 0)
-					_G(_unp)._endAdr = 0x10001;
+			if (unp._endAdr == 0)
+					unp._endAdr = 0x10001;
 
-			_G(_unp)._fEndBf = 0;
+			unp._fEndBf = 0;
 		}
 
-		if (_G(_unp)._fStrBf && (_G(_unp)._strMem != 0x2) && (r->_pc == _G(_unp)._depAdr)) {
-			_G(_unp)._strMem = r->_mem[_G(_unp)._fStrBf] | r->_mem[_G(_unp)._fStrBf + 1] << 8;
-			_G(_unp)._fStrBf = 0;
+		if (unp._fStrBf && (unp._strMem != 0x2) && (r->_pc == unp._depAdr)) {
+			unp._strMem = r->_mem[unp._fStrBf] | r->_mem[unp._fStrBf + 1] << 8;
+			unp._fStrBf = 0;
 		}
 
-		if (_G(_unp)._debugP) {
+		if (unp._debugP) {
 			for (p = 0; p < 0x20; p += 2) {
 				if (*(unsigned short int *)(mem + 0x314 + p) != *(unsigned short int *)(vector + p)) {
 					*(unsigned short int *)(vector + p) = *(unsigned short int *)(mem + 0x314 + p);
@@ -449,22 +449,22 @@ int unp64(byte *compressed, size_t length, byte *destinationBuffer, size_t *fina
 		}
 	}
 
-	_G(_iter) = 0;
-	while (_G(_unp)._rtAFrc ? r->_pc != _G(_unp)._retAdr : r->_pc < _G(_unp)._retAdr) {
-		if (_G(_unp)._monEnd && r->_pc == _G(_unp)._depAdr) {
-			p = r->_mem[_G(_unp)._monEnd >> 16] | r->_mem[_G(_unp)._monEnd & 0xffff] << 8;
-			if (p > (_G(_unp)._endAdr & 0xffff)) {
-				_G(_unp)._endAdr = p;
+	iter = 0;
+	while (unp._rtAFrc ? r->_pc != unp._retAdr : r->_pc < unp._retAdr) {
+		if (unp._monEnd && r->_pc == unp._depAdr) {
+			p = r->_mem[unp._monEnd >> 16] | r->_mem[unp._monEnd & 0xffff] << 8;
+			if (p > (unp._endAdr & 0xffff)) {
+				unp._endAdr = p;
 			}
 		}
-		if (_G(_unp)._monStr && r->_pc == _G(_unp)._depAdr) {
-			p = r->_mem[_G(_unp)._monStr >> 16] | r->_mem[_G(_unp)._monStr & 0xffff] << 8;
+		if (unp._monStr && r->_pc == unp._depAdr) {
+			p = r->_mem[unp._monStr >> 16] | r->_mem[unp._monStr & 0xffff] << 8;
 			if (p > 0) {
-				if (_G(_unp)._mon1st == 0) {
-					_G(_unp)._strMem = p;
+				if (unp._mon1st == 0) {
+					unp._strMem = p;
 				}
-				_G(_unp)._mon1st = (unsigned int)_G(_unp)._strMem;
-				_G(_unp)._strMem = (p < _G(_unp)._strMem ? p : _G(_unp)._strMem);
+				unp._mon1st = (unsigned int)unp._strMem;
+				unp._strMem = (p < unp._strMem ? p : unp._strMem);
 			}
 		}
 
@@ -477,16 +477,16 @@ int unp64(byte *compressed, size_t length, byte *destinationBuffer, size_t *fina
 		if (nextInst(r) == 1)
 			return 0;
 
-		if ((mem[r->_pc] == 0x40) && (_G(_unp)._rtiFrc == 1)) {
-			_G(_unp)._retAdr = r->_pc;
-			_G(_unp)._rtAFrc = 1;
-			if (_G(_unp)._retAdr < _G(_unp)._strMem)
-				_G(_unp)._strMem = 2;
+		if ((mem[r->_pc] == 0x40) && (unp._rtiFrc == 1)) {
+			unp._retAdr = r->_pc;
+			unp._rtAFrc = 1;
+			if (unp._retAdr < unp._strMem)
+				unp._strMem = 2;
 			break;
 		}
 
-		_G(_iter)++;
-		if (_G(_iter) == iterMax) {
+		iter++;
+		if (iter == iterMax) {
 			return 0;
 		}
 
@@ -523,37 +523,37 @@ int unp64(byte *compressed, size_t length, byte *destinationBuffer, size_t *fina
 		}
 	}
 
-	if (_G(_unp)._fEndAf && _G(_unp)._monEnd) {
-		_G(_unp)._endAdC = (unsigned int)(mem[_G(_unp)._fEndAf] | mem[_G(_unp)._fEndAf + 1] << 8);
-		if ((int)_G(_unp)._endAdC > _G(_unp)._endAdr)
-			_G(_unp)._endAdr = (int)_G(_unp)._endAdC;
+	if (unp._fEndAf && unp._monEnd) {
+		unp._endAdC = (unsigned int)(mem[unp._fEndAf] | mem[unp._fEndAf + 1] << 8);
+		if ((int)unp._endAdC > unp._endAdr)
+			unp._endAdr = (int)unp._endAdC;
 
-		_G(_unp)._endAdC = 0;
-		_G(_unp)._fEndAf = 0;
+		unp._endAdC = 0;
+		unp._fEndAf = 0;
 	}
 
-	if (_G(_unp)._fEndAf && (_G(_unp)._endAdr == 0x10000)) {
-		_G(_unp)._endAdr = r->_mem[_G(_unp)._fEndAf] | r->_mem[_G(_unp)._fEndAf + 1] << 8;
-		if (_G(_unp)._endAdr == 0)
-			_G(_unp)._endAdr = 0x10000;
+	if (unp._fEndAf && (unp._endAdr == 0x10000)) {
+		unp._endAdr = r->_mem[unp._fEndAf] | r->_mem[unp._fEndAf + 1] << 8;
+		if (unp._endAdr == 0)
+			unp._endAdr = 0x10000;
 		else
-			_G(_unp)._endAdr++;
-		_G(_unp)._fEndAf = 0;
+			unp._endAdr++;
+		unp._fEndAf = 0;
 	}
 
-	if (_G(_unp)._fStrAf /*&&(_G(_unp)._strMem==0x800)*/) {
-		_G(_unp)._strMem = r->_mem[_G(_unp)._fStrAf] | r->_mem[_G(_unp)._fStrAf + 1] << 8;
-		_G(_unp)._strMem++;
-		_G(_unp)._fStrAf = 0;
+	if (unp._fStrAf /*&&(unp._strMem==0x800)*/) {
+		unp._strMem = r->_mem[unp._fStrAf] | r->_mem[unp._fStrAf + 1] << 8;
+		unp._strMem++;
+		unp._fStrAf = 0;
 	}
 
-	if (_G(_unp)._exoFnd && (_G(_unp)._strMem != 2)) {
-		_G(_unp)._strMem = r->_mem[0xfe] + (r->_mem[0xff] << 8);
+	if (unp._exoFnd && (unp._strMem != 2)) {
+		unp._strMem = r->_mem[0xfe] + (r->_mem[0xff] << 8);
 
-		if ((_G(_unp)._exoFnd & 0xff) == 0x30) {
-			_G(_unp)._strMem += r->_y;
-		} else if ((_G(_unp)._exoFnd & 0xff) == 0x32) {
-			_G(_unp)._strMem += r->_y + 1;
+		if ((unp._exoFnd & 0xff) == 0x30) {
+			unp._strMem += r->_y;
+		} else if ((unp._exoFnd & 0xff) == 0x32) {
+			unp._strMem += r->_y + 1;
 		}
 	}
 
@@ -570,12 +570,12 @@ int unp64(byte *compressed, size_t length, byte *destinationBuffer, size_t *fina
 		}
 	}
 
-	if (_G(_unp)._wrMemF) {
-		_G(_unp)._wrMemF = 0;
+	if (unp._wrMemF) {
+		unp._wrMemF = 0;
 		for (p = 0x800; p < 0x10000; p += 4) {
 			if (*(unsigned int *)(oldmem + p) == *(unsigned int *)(mem + p)) {
 				*(unsigned int *)(mem + p) = 0;
-				_G(_unp)._wrMemF = 1;
+				unp._wrMemF = 1;
 			}
 		}
 		/* clean also the $fd30 table copy in RAM */
@@ -584,13 +584,13 @@ int unp64(byte *compressed, size_t length, byte *destinationBuffer, size_t *fina
 		}
 	}
 
-	if (_G(_unp)._lfMemF) {
+	if (unp._lfMemF) {
 		for (p = 0xffff; p > 0x0800; p--) {
-			if (oldmem[--_G(_unp)._lfMemF] == mem[p])
+			if (oldmem[--unp._lfMemF] == mem[p])
 				mem[p] = 0x0;
 			else {
 				if (p >= 0xffff)
-					_G(_unp)._lfMemF = 0 | _G(_unp)._ecaFlg;
+					unp._lfMemF = 0 | unp._ecaFlg;
 				break;
 			}
 		}
@@ -605,131 +605,131 @@ int unp64(byte *compressed, size_t length, byte *destinationBuffer, size_t *fina
 			ln = 248;
 		}
 
-		Common::sprintf_s(name + ln, sizeof(name) - ln, ".%04x%s", r->_pc, ((_G(_unp)._wrMemF | _G(_unp)._lfMemF) ? ".clean" : ""));
+		Common::sprintf_s(name + ln, sizeof(name) - ln, ".%04x%s", r->_pc, ((unp._wrMemF | unp._lfMemF) ? ".clean" : ""));
 	}
 
 	/*  endadr is set to a ZP location? then use it as a pointer
 	  todo: use __fEndAf instead, it can be used for any location, not only ZP. */
-	if (_G(_unp)._endAdr && (_G(_unp)._endAdr < 0x100)) {
-		p = (mem[_G(_unp)._endAdr] | mem[_G(_unp)._endAdr + 1] << 8) & 0xffff;
-		_G(_unp)._endAdr = p;
+	if (unp._endAdr && (unp._endAdr < 0x100)) {
+		p = (mem[unp._endAdr] | mem[unp._endAdr + 1] << 8) & 0xffff;
+		unp._endAdr = p;
 	}
 
-	if (_G(_unp)._ecaFlg && (_G(_unp)._strMem != 2)) /* checkme */ {
-		if (_G(_unp)._endAdr >= ((_G(_unp)._ecaFlg >> 16) & 0xffff)) {
+	if (unp._ecaFlg && (unp._strMem != 2)) /* checkme */ {
+		if (unp._endAdr >= ((unp._ecaFlg >> 16) & 0xffff)) {
 			/* most of the times transfers $2000 byte from $d000-efff to $e000-ffff but there are exceptions */
-			if (_G(_unp)._lfMemF)
-				memset(mem + ((_G(_unp)._ecaFlg >> 16) & 0xffff), 0, 0x1000);
-			_G(_unp)._endAdr += 0x1000;
+			if (unp._lfMemF)
+				memset(mem + ((unp._ecaFlg >> 16) & 0xffff), 0, 0x1000);
+			unp._endAdr += 0x1000;
 		}
 	}
 
-	if (_G(_unp)._endAdr <= 0)
-		_G(_unp)._endAdr = 0x10000;
+	if (unp._endAdr <= 0)
+		unp._endAdr = 0x10000;
 
-	if (_G(_unp)._endAdr > 0x10000)
-		_G(_unp)._endAdr = 0x10000;
+	if (unp._endAdr > 0x10000)
+		unp._endAdr = 0x10000;
 
-	if (_G(_unp)._endAdr < _G(_unp)._strMem)
-		_G(_unp)._endAdr = 0x10000;
+	if (unp._endAdr < unp._strMem)
+		unp._endAdr = 0x10000;
 
-	if (_G(_unp)._endAdC & 0xffff) {
-		_G(_unp)._endAdr += (_G(_unp)._endAdC & 0xffff);
-		_G(_unp)._endAdr &= 0xffff;
+	if (unp._endAdC & 0xffff) {
+		unp._endAdr += (unp._endAdC & 0xffff);
+		unp._endAdr &= 0xffff;
 	}
 
-	if (_G(_unp)._endAdC & EA_USE_A) {
-		_G(_unp)._endAdr += r->_a;
-		_G(_unp)._endAdr &= 0xffff;
+	if (unp._endAdC & EA_USE_A) {
+		unp._endAdr += r->_a;
+		unp._endAdr &= 0xffff;
 	}
 
-	if (_G(_unp)._endAdC & EA_USE_X) {
-		_G(_unp)._endAdr += r->_x;
-		_G(_unp)._endAdr &= 0xffff;
+	if (unp._endAdC & EA_USE_X) {
+		unp._endAdr += r->_x;
+		unp._endAdr &= 0xffff;
 	}
 
-	if (_G(_unp)._endAdC & EA_USE_Y) {
-		_G(_unp)._endAdr += r->_y;
-		_G(_unp)._endAdr &= 0xffff;
+	if (unp._endAdC & EA_USE_Y) {
+		unp._endAdr += r->_y;
+		unp._endAdr &= 0xffff;
 	}
 
-	if (_G(_unp)._strAdC & 0xffff) {
-		_G(_unp)._strMem += (_G(_unp)._strAdC & 0xffff);
-		_G(_unp)._strMem &= 0xffff;
+	if (unp._strAdC & 0xffff) {
+		unp._strMem += (unp._strAdC & 0xffff);
+		unp._strMem &= 0xffff;
 		/* only if ea_addff, no reg involved */
-		if (((_G(_unp)._strAdC & 0xffff0000) == EA_ADDFF) && ((_G(_unp)._strMem & 0xff) == 0)) {
-			_G(_unp)._strMem += 0x100;
-			_G(_unp)._strMem &= 0xffff;
+		if (((unp._strAdC & 0xffff0000) == EA_ADDFF) && ((unp._strMem & 0xff) == 0)) {
+			unp._strMem += 0x100;
+			unp._strMem &= 0xffff;
 		}
 	}
 
-	if (_G(_unp)._strAdC & EA_USE_A) {
-		_G(_unp)._strMem += r->_a;
-		_G(_unp)._strMem &= 0xffff;
-		if (_G(_unp)._strAdC & EA_ADDFF) {
-			if ((_G(_unp)._strMem & 0xff) == 0xff)
-				_G(_unp)._strMem++;
+	if (unp._strAdC & EA_USE_A) {
+		unp._strMem += r->_a;
+		unp._strMem &= 0xffff;
+		if (unp._strAdC & EA_ADDFF) {
+			if ((unp._strMem & 0xff) == 0xff)
+				unp._strMem++;
 
 			if (r->_a == 0) {
-				_G(_unp)._strMem += 0x100;
-				_G(_unp)._strMem &= 0xffff;
+				unp._strMem += 0x100;
+				unp._strMem &= 0xffff;
 			}
 		}
 	}
 
-	if (_G(_unp)._strAdC & EA_USE_X) {
-		_G(_unp)._strMem += r->_x;
-		_G(_unp)._strMem &= 0xffff;
+	if (unp._strAdC & EA_USE_X) {
+		unp._strMem += r->_x;
+		unp._strMem &= 0xffff;
 
-		if (_G(_unp)._strAdC & EA_ADDFF) {
-			if ((_G(_unp)._strMem & 0xff) == 0xff)
-				_G(_unp)._strMem++;
+		if (unp._strAdC & EA_ADDFF) {
+			if ((unp._strMem & 0xff) == 0xff)
+				unp._strMem++;
 
 			if (r->_x == 0) {
-				_G(_unp)._strMem += 0x100;
-				_G(_unp)._strMem &= 0xffff;
+				unp._strMem += 0x100;
+				unp._strMem &= 0xffff;
 			}
 		}
 	}
 
-	if (_G(_unp)._strAdC & EA_USE_Y) {
-		_G(_unp)._strMem += r->_y;
-		_G(_unp)._strMem &= 0xffff;
+	if (unp._strAdC & EA_USE_Y) {
+		unp._strMem += r->_y;
+		unp._strMem &= 0xffff;
 
-		if (_G(_unp)._strAdC & EA_ADDFF) {
-			if ((_G(_unp)._strMem & 0xff) == 0xff)
-				_G(_unp)._strMem++;
+		if (unp._strAdC & EA_ADDFF) {
+			if ((unp._strMem & 0xff) == 0xff)
+				unp._strMem++;
 
 			if (r->_y == 0) {
-				_G(_unp)._strMem += 0x100;
-				_G(_unp)._strMem &= 0xffff;
+				unp._strMem += 0x100;
+				unp._strMem &= 0xffff;
 			}
 		}
 	}
 
-	if (_G(_unp)._endAdr <= 0)
-		_G(_unp)._endAdr = 0x10000;
+	if (unp._endAdr <= 0)
+		unp._endAdr = 0x10000;
 
-	if (_G(_unp)._endAdr > 0x10000)
-		_G(_unp)._endAdr = 0x10000;
+	if (unp._endAdr > 0x10000)
+		unp._endAdr = 0x10000;
 
-	if (_G(_unp)._endAdr < _G(_unp)._strMem)
-		_G(_unp)._endAdr = 0x10000;
+	if (unp._endAdr < unp._strMem)
+		unp._endAdr = 0x10000;
 
-	mem[_G(_unp)._strMem - 2] = _G(_unp)._strMem & 0xff;
-	mem[_G(_unp)._strMem - 1] = _G(_unp)._strMem >> 8;
+	mem[unp._strMem - 2] = unp._strMem & 0xff;
+	mem[unp._strMem - 1] = unp._strMem >> 8;
 
-	memcpy(destinationBuffer, mem + (_G(_unp)._strMem - 2), (size_t)(_G(_unp)._endAdr - _G(_unp)._strMem + 2));
-	*finalLength = (size_t)(_G(_unp)._endAdr - _G(_unp)._strMem + 2);
+	memcpy(destinationBuffer, mem + (unp._strMem - 2), (size_t)(unp._endAdr - unp._strMem + 2));
+	*finalLength = (size_t)(unp._endAdr - unp._strMem + 2);
 
-	if (_G(_unp)._recurs) {
-		if (++_G(_unp)._recurs > RECUMAX)
+	if (unp._recurs) {
+		if (++unp._recurs > RECUMAX)
 			return 1;
-		reinitUnp();
+		reinitUnp(unp);
 		goto looprecurse;
 	}
 	return 1;
 }
 
-} // End of namespace Scott
-} // End of namespace Glk
+} // End of namespace Unp64
+} // End of namespace Common
diff --git a/engines/glk/scott/unp64/unp64.h b/common/compression/unp64/unp64.h
similarity index 93%
rename from engines/glk/scott/unp64/unp64.h
rename to common/compression/unp64/unp64.h
index 865816e49e9..76b2a37b7a6 100644
--- a/engines/glk/scott/unp64/unp64.h
+++ b/common/compression/unp64/unp64.h
@@ -19,11 +19,11 @@
  *
  */
 
-#ifndef GLK_SCOTT_UNP64_H
-#define GLK_SCOTT_UNP64_H
+#ifndef COMMON_COMPRESSION_UNP64_UNP64_H
+#define COMMON_COMPRESSION_UNP64_UNP64_H
 
-namespace Glk {
-namespace Scott {
+namespace Common {
+namespace Unp64 {
 
 struct LoadInfo;
 struct CpuCtx;
@@ -38,7 +38,7 @@ struct UnpStr {
 	int _rtAFrc;          /* flag, return address must be exactly RetAdr, else anything >= RetAdr */
 	int _wrMemF;          /* flag, clean unwritten memory */
 	int _lfMemF;          /* flag, clean end memory leftovers */
-	int _exoFnd;          /* flag, Exomizer detected */ 
+	int _exoFnd;          /* flag, Exomizer detected */
 	int _fStack;          /* flag, fill stack with 0 and SP=$ff, else as in C64 */
 	int _ecaFlg;          /* ECA found, holds relocated areas high bytes */
 	int _fEndBf;          /* End memory address pointer before unpacking, set when DepAdr is reached */
@@ -72,7 +72,7 @@ typedef void (*Scnptr)(UnpStr *);
 
 void scanners(UnpStr *);
 
-} // End of namespace Scott
-} // End of namespace Glk
+} // End of namespace Unp64
+} // End of namespace Common
 
-#endif 
+#endif
diff --git a/configure b/configure
index eb45d0fda27..f01a8aa289b 100755
--- a/configure
+++ b/configure
@@ -315,6 +315,7 @@ _fmtowns_pc98_audio=auto
 _sid_audio=auto
 _svq1=auto
 _truemotion1=auto
+_unp64=auto
 _vgmtrans_audio=auto
 _xan=auto
 _midi=auto
@@ -366,6 +367,7 @@ add_component svq1 "Sorenson Video 1" "_svq1" "USE_SVQ1"
 add_component tinygl "TinyGL" "_tinygl" "USE_TINYGL"
 add_component truemotion1 "TrueMotion 1" "_truemotion1" "USE_TRUEMOTION1"
 add_component universaltracker "External Tracker Libraries" "_universaltracker" "USE_UNIVERSALTRACKER"
+add_component unp64 "UNP64" "_unp64" "USE_UNP64"
 add_component vgmtrans_audio "VGMTrans Soundfont audio" "_vgmtrans_audio" "USE_VGMTRANS_AUDIO"
 add_component xan "XAN" "_xan" "USE_XAN"
 
diff --git a/engines/glk/configure.engine b/engines/glk/configure.engine
index 7db6d57f739..bf140785752 100644
--- a/engines/glk/configure.engine
+++ b/engines/glk/configure.engine
@@ -1,3 +1,3 @@
 # This file is included from the main "configure" script
 # add_engine [name] [desc] [build-by-default] [subengines] [base games] [deps] [components]
-add_engine glk "Glk Interactive Fiction games" yes "" "" "16bit freetype2 jpeg png"
+add_engine glk "Glk Interactive Fiction games" yes "" "" "16bit freetype2 jpeg png unp64"
diff --git a/engines/glk/module.mk b/engines/glk/module.mk
index 52e0dc24c94..03cf17c394c 100644
--- a/engines/glk/module.mk
+++ b/engines/glk/module.mk
@@ -260,25 +260,6 @@ MODULE_OBJS := \
 	scott/scott.o \
 	scott/seas_of_blood.o \
 	scott/ti99_4a_terp.o \
-	scott/unp64/unp64.o \
-	scott/unp64/6502_emu.o \
-	scott/unp64/exo_util.o \
-	scott/unp64/scanners/scanners.o \
-	scott/unp64/scanners/action_packer.o \
-	scott/unp64/scanners/byte_boiler.o \
-	scott/unp64/scanners/caution.o \
-	scott/unp64/scanners/ccs.o \
-	scott/unp64/scanners/cruel.o \
-	scott/unp64/scanners/eca.o \
-	scott/unp64/scanners/exomizer.o \
-	scott/unp64/scanners/expert.o \
-	scott/unp64/scanners/master_compressor.o \
-	scott/unp64/scanners/megabyte.o \
-	scott/unp64/scanners/pu_crunch.o \
-	scott/unp64/scanners/section8.o \
-	scott/unp64/scanners/tbc_multicomp.o \
-	scott/unp64/scanners/tcs_crunch.o \
-	scott/unp64/scanners/xtc.o \
 	tads/os_banners.o \
 	tads/os_buffer.o \
 	tads/os_glk.o \
diff --git a/engines/glk/scott/c64_checksums.cpp b/engines/glk/scott/c64_checksums.cpp
index 4bf9fb9106c..da9092eb5e1 100644
--- a/engines/glk/scott/c64_checksums.cpp
+++ b/engines/glk/scott/c64_checksums.cpp
@@ -30,6 +30,7 @@
  * https://github.com/angstsmurf/spatterlight/tree/master/terps/scott
  */
 
+#include "common/compression/unp64.h"
 #include "common/str.h"
 #include "common/scummsys.h"
 #include "common/ptr.h"
@@ -41,7 +42,6 @@
 #include "glk/scott/game_info.h"
 #include "glk/scott/resource.h"
 #include "glk/scott/saga_draw.h"
-#include "glk/scott/unp64/unp64_interface.h"
 
 namespace Glk {
 namespace Scott {
@@ -480,7 +480,7 @@ int decrunchC64(uint8_t **sf, size_t *extent, C64Rec record) {
 	uint8_t *uncompressed = nullptr;
 	_G(_fileLength) = *extent;
 
-	size_t decompressedLength = *extent;
+	uint32 decompressedLength = *extent;
 
 	uncompressed = new uint8_t[0xffff];
 
@@ -489,9 +489,9 @@ int decrunchC64(uint8_t **sf, size_t *extent, C64Rec record) {
 	for (int i = 1; i <= record._decompressIterations; i++) {
 		/* We only send switches on the iteration specified by parameter */
 		if (i == record._parameter && record._switches != nullptr) {
-			result = unp64(_G(_entireFile), _G(_fileLength), uncompressed, &decompressedLength, record._switches);
+			result = Common::Unp64::unp64(_G(_entireFile), _G(_fileLength), uncompressed, &decompressedLength, record._switches);
 		} else
-			result = unp64(_G(_entireFile), _G(_fileLength), uncompressed, &decompressedLength, nullptr);
+			result = Common::Unp64::unp64(_G(_entireFile), _G(_fileLength), uncompressed, &decompressedLength, nullptr);
 		if (result) {
 			if (_G(_entireFile) != nullptr)
 				delete[] _G(_entireFile);
diff --git a/engines/glk/scott/globals.h b/engines/glk/scott/globals.h
index aff6425ad2a..d38379ad455 100644
--- a/engines/glk/scott/globals.h
+++ b/engines/glk/scott/globals.h
@@ -41,7 +41,6 @@
 #include "glk/windows.h"
 #include "glk/scott/definitions.h"
 #include "glk/scott/types.h"
-#include "glk/scott/unp64/unp64.h"
 
 namespace Glk {
 namespace Scott {
@@ -182,16 +181,6 @@ public:
 	// detect game
 	Common::HashMap<Common::String, int> _md5Index;
 
-	// unp64
-	UnpStr _unp;
-	int _parsePar = 1;
-	int _iter = 0;
-
-	// 6502 emu
-	int _byted011[2] = {0, 0};
-	int _retfire = 0xff;
-	int _retspace = 0xff;
-
 	// robin of sherwood]
 	uint8_t *_forestImages = nullptr;
 




More information about the Scummvm-git-logs mailing list