[Scummvm-git-logs] scummvm master -> 3474234780145c5014bfd9dedcdd44f232a6ed41

neuromancer noreply at scummvm.org
Sat Sep 12 11:54:44 UTC 2026


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

Summary:
9521ce0db5 FREESCAPE: allow to load 3DCK for atari
4ccc9ad054 FREESCAPE: added Egyptman for atari
cebd12bbf1 SCUMM: RA2: potential fix for L2 enemy partial decoding
e973f7e7fb SCUMM: RA2: added missing blur effect affecting at least L2
3474234780 SCUMM: RA2: properly implement pause menu including film effect


Commit: 9521ce0db5639a9fe9a418ca9e593e6ddfdeb19c
    https://github.com/scummvm/scummvm/commit/9521ce0db5639a9fe9a418ca9e593e6ddfdeb19c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-12T12:21:36+02:00

Commit Message:
FREESCAPE: allow to load 3DCK for atari

Changed paths:
  A engines/freescape/games/3dck/atari.cpp
    engines/freescape/area.cpp
    engines/freescape/area.h
    engines/freescape/detection.cpp
    engines/freescape/games/3dck/3dck.cpp
    engines/freescape/games/3dck/3dck.h
    engines/freescape/games/3dck/ui.cpp
    engines/freescape/gfx_opengl_shaders.cpp
    engines/freescape/language/detokeniser.h
    engines/freescape/language/detokeniser_3dck16.cpp
    engines/freescape/language/execution_3dck16.cpp
    engines/freescape/module.mk


diff --git a/engines/freescape/area.cpp b/engines/freescape/area.cpp
index 5c53b8148b8..3abe174107a 100644
--- a/engines/freescape/area.cpp
+++ b/engines/freescape/area.cpp
@@ -880,13 +880,18 @@ void Area::addGroupFromArea(int16 id, Area *global) {
 }
 
 
-void Area::addFloor() {
+void Area::addFloor(uint8 extraColor) {
 	_hasSyntheticFloor = true;
 	int id = 0;
 	assert(!_objectsByID->contains(id));
 	Common::Array<uint8> *gColors = new Common::Array<uint8>;
 	for (int i = 0; i < 6; i++)
 		gColors->push_back(_groundColor);
+	Common::Array<uint8> *extraColors = nullptr;
+	if (extraColor) {
+		extraColors = new Common::Array<uint8>();
+		extraColors->resize(6, extraColor);
+	}
 
 	int maxSize = 10000000 / 4;
 	Object *obj = (Object *)new GeometricObject(
@@ -896,7 +901,7 @@ void Area::addFloor() {
 		Math::Vector3d(-maxSize, -3, -maxSize),      // Position
 		Math::Vector3d(maxSize * 4, 3, maxSize * 4), // size
 		gColors,
-		nullptr,
+		extraColors,
 		nullptr,
 		FCLInstructionVector());
 	(*_objectsByID)[id] = obj;
diff --git a/engines/freescape/area.h b/engines/freescape/area.h
index 5dc65ecb921..bfacfe8135b 100644
--- a/engines/freescape/area.h
+++ b/engines/freescape/area.h
@@ -79,7 +79,7 @@ public:
 	void addObjectFromArea(int16 id, Area *global);
 	void addGroupFromArea(int16 id, Area *global);
 	void addObject(Object *obj);
-	void addFloor();
+	void addFloor(uint8 extraColor = 0);
 	void addStructure(Area *global);
 	void removeObject(int16 id);
 	void resetArea();
diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index 0b2765dfeb7..75a8fe63c0e 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -1159,6 +1159,46 @@ const ADGameDescription gameDescriptions[] = {
 		GUIO3(GUIO_NOMIDI, GUIO_RENDEREGA, GAMEOPTION_WASD_CONTROLS)
 	},
 	// 3D Construction Kit games
+	{
+		"3dkit",
+		"The Maze",
+		AD_ENTRY2s("THEMAZE.DAT", "0925b7be5b3644cbf725a8ed747beecc", 85162,
+			"THEMAZE.BDR", "ecee4abd508e34df25be8607fb780771", 32128),
+		Common::EN_ANY,
+		Common::kPlatformAtariST,
+		ADGF_UNSTABLE,
+		GUIO4(GUIO_NOMIDI, GUIO_NOMUSIC, GUIO_NOSFX, GUIO_RENDERATARIST)
+	},
+	{
+		"3dkit",
+		"Anarchy Academy",
+		AD_ENTRY2s("RUNNER.DAT", "1cf7c049ee59503dd7218b0f45828132", 42802,
+			"RUNNER.BDR", "2ae0a6e768595d8ef325f8ef14dc9525", 32128),
+		Common::EN_ANY,
+		Common::kPlatformAtariST,
+		ADGF_UNSTABLE,
+		GUIO4(GUIO_NOMIDI, GUIO_NOMUSIC, GUIO_NOSFX, GUIO_RENDERATARIST)
+	},
+	{
+		"3dkit",
+		"Hysula",
+		AD_ENTRY2s("HYSULA.DAT", "6db092c4a6cc56d562b14e1c7219005b", 69472,
+			"HYSULA.BDR", "79e5b9ac9bb679278f64ef0568b50bfe", 32128),
+		Common::EN_ANY,
+		Common::kPlatformAtariST,
+		ADGF_UNSTABLE,
+		GUIO4(GUIO_NOMIDI, GUIO_NOMUSIC, GUIO_NOSFX, GUIO_RENDERATARIST)
+	},
+	{
+		"3dkit",
+		"Supernova",
+		AD_ENTRY2s("SUPNOV.DAT", "a4049142da498339198eac4082f1616d", 72922,
+			"SUPNOV.BDR", "ffad88a56fd096de77edb976dd7d4825", 32128),
+		Common::EN_ANY,
+		Common::kPlatformAtariST,
+		ADGF_UNSTABLE,
+		GUIO4(GUIO_NOMIDI, GUIO_NOMUSIC, GUIO_NOSFX, GUIO_RENDERATARIST)
+	},
 	{
 		"3dkit",
 		"Dead by Dawn",
@@ -1426,16 +1466,6 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSUPPORTED,
 		GUIO1(GUIO_NOMIDI)
 	},
-	{
-		"3dkit",
-		"Anarchy Academy",
-		AD_ENTRY2s("ANARCHY.PRG", "78d543be4aad9608093c1e91bc100307", 270,
-		           "RUNNER.DAT", "1cf7c049ee59503dd7218b0f45828132", 42802),
-		Common::EN_ANY,
-		Common::kPlatformAtariST,
-		ADGF_UNSUPPORTED,
-		GUIO1(GUIO_NOMIDI)
-	},
 	{
 		"3dkit",
 		"The World of Stairs",
diff --git a/engines/freescape/games/3dck/3dck.cpp b/engines/freescape/games/3dck/3dck.cpp
index 0cbb56f3b42..948e01308ed 100644
--- a/engines/freescape/games/3dck/3dck.cpp
+++ b/engines/freescape/games/3dck/3dck.cpp
@@ -80,7 +80,7 @@ static void readColors(Common::SeekableReadStream &file, byte &first, byte &seco
 }
 
 KitEngine::KitEngine(OSystem *syst, const ADGameDescription *gd) : FreescapeEngine(syst, gd), _initialPlayerHeight(0) {
-	_renderMode = Common::kRenderVGA;
+	_renderMode = isAtariST() ? Common::kRenderAtariST : Common::kRenderVGA;
 	_screenW = 320;
 	_screenH = 200;
 	_fullscreenViewArea = Common::Rect(_screenW, _screenH);
@@ -92,6 +92,17 @@ KitEngine::KitEngine(OSystem *syst, const ADGameDescription *gd) : FreescapeEngi
 }
 
 void KitEngine::loadAssets() {
+	if (isAtariST())
+		loadAssetsAtari();
+	else
+		loadAssetsDOS();
+	_gfx->_palette = _palette;
+	_gfx->_keyColor = 0;
+	_scriptSurface.create(_screenW, _screenH, _gfx->_texturePixelFormat);
+	_scriptSurface.fillRect(_fullscreenViewArea, 0);
+}
+
+void KitEngine::loadAssetsDOS() {
 	Common::File file;
 	if (!file.open(_gameDescription->filesDescriptions[0].fileName))
 		error("Unable to open 3D Construction Kit RUN file");
@@ -129,10 +140,6 @@ void KitEngine::loadAssets() {
 	_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);
-	_scriptSurface.fillRect(_fullscreenViewArea, 0);
 	uint16 soundSize = readBlockSize(file);
 	Common::SeekableSubReadStream sounds(&file, file.pos(), file.pos() + soundSize);
 	loadSounds(sounds);
@@ -141,7 +148,9 @@ void KitEngine::loadAssets() {
 void KitEngine::loadWorld(Common::SeekableReadStream &file) {
 	requireBytes(file, 500);
 	uint32 signature = file.readUint32BE();
-	if (signature != MKTAG('C', 'P', 0, 0) && signature != MKTAG('C', 'P', '0', '1'))
+	bool validSignature = isAtariST() ? signature == MKTAG('A', 'M', '0', '1') :
+		signature == MKTAG('C', 'P', 0, 0) || signature == MKTAG('C', 'P', '0', '1');
+	if (!validSignature)
 		error("Unsupported 3D Construction Kit world format");
 	uint16 areaCount = file.readUint16BE();
 	uint32 globalConditions = 2 * file.readUint16BE();
@@ -155,15 +164,20 @@ void KitEngine::loadWorld(Common::SeekableReadStream &file) {
 	int top = _screenH - 1 - centerY - halfHeight;
 	_viewArea = Common::Rect(centerX - halfWidth, top, centerX + halfWidth, top + 2 * halfHeight);
 
-	// RUNVGA derives its projection scales from the viewport dimensions.
-	int xScale = 74 * (_viewArea.height() - 1) / 256;
-	int yScale = 55 * (_viewArea.width() - 1) / 256;
-	if (!xScale || !yScale)
+	int xScale = file.readUint16BE();
+	int yScale = file.readUint16BE();
+	int zScale = file.readUint16BE();
+	if (isDOS()) {
+		// RUNVGA derives its projection scales from the viewport dimensions.
+		xScale = 74 * (_viewArea.height() - 1) / 256;
+		yScale = 55 * (_viewArea.width() - 1) / 256;
+		zScale = 24;
+	}
+	if (!xScale || !yScale || !zScale)
 		error("3D Construction Kit viewport is too small");
-	_fieldOfView = 2.0f * Math::rad2deg(atan(24.0f / xScale));
+	_fieldOfView = 2.0f * Math::rad2deg(atan(float(zScale) / xScale));
 	_viewAspectRatio = float(yScale) / xScale;
 
-	file.skip(6);
 	_timerInterval = file.readUint16BE();
 	_activationRange = file.readUint16BE();
 	_maxFallingDistance = file.readUint16BE();
@@ -183,9 +197,10 @@ void KitEngine::loadWorld(Common::SeekableReadStream &file) {
 	_angleRotations.push_back(angle);
 	_angleRotationIndex = 0;
 
-	file.skip(2);
 	// DOS word offsets wrap at 64 KiB.
-	uint32 indicatorOffset = uint16(2 * file.readUint16BE());
+	uint32 indicatorOffset = 2 * file.readUint32BE();
+	if (isDOS())
+		indicatorOffset = uint16(indicatorOffset);
 	uint16 indicatorCount = file.readUint16BE();
 	_initialCondition = file.readUint16BE();
 	if (indicatorOffset > uint32(file.size()) || (!indicatorOffset && indicatorCount))
@@ -202,7 +217,9 @@ void KitEngine::loadWorld(Common::SeekableReadStream &file) {
 	requireBytes(file, uint32(areaCount) * 4);
 	Common::Array<uint32> areaOffsets;
 	for (uint i = 0; i < areaCount; i++) {
-		uint32 offset = uint16(2 * file.readUint32BE());
+		uint32 offset = 2 * file.readUint32BE();
+		if (isDOS())
+			offset = uint16(offset);
 		if (indicatorCount && offset >= indicatorOffset && offset < indicatorOffset + 2 * _indicatorData.size()) {
 			warning("Ignoring stale 3D Construction Kit area offset %u into indicator data", offset);
 			continue;
@@ -246,7 +263,7 @@ Common::Array<KitEngine::ConditionData> KitEngine::loadConditions(Common::Seekab
 		uint16 words = file.readUint16BE() & 0x7fff;
 		ConditionData condition;
 		condition.name = name;
-		Common::String source = detokeniseKit16Condition(readCode(file, 2 * words), condition.condition);
+		Common::String source = detokeniseKit16Condition(readCode(file, 2 * words), condition.condition, isAtariST());
 		debugC(1, kFreescapeDebugParser, "3DCK condition %s:\n%s", name, source.c_str());
 		conditions.push_back(condition);
 	}
@@ -261,8 +278,10 @@ Area *KitEngine::loadArea(Common::SeekableReadStream &file) {
 	uint16 id = file.readUint16BE();
 	if (_areaMap.contains(id))
 		error("Duplicate 3D Construction Kit area %u", id);
-	file.skip(2);
-	uint32 conditions = start + 2 * file.readUint16BE();
+	uint32 conditionOffset = file.readUint32BE();
+	if (isDOS())
+		conditionOffset = uint16(conditionOffset);
+	uint32 conditions = start + 2 * conditionOffset;
 	uint16 scale = file.readUint16BE();
 	uint16 sky = file.readUint16BE();
 	uint16 ground = file.readUint16BE();
@@ -305,9 +324,14 @@ Area *KitEngine::loadArea(Common::SeekableReadStream &file) {
 	area->_groundColor = ((ground & 0xf) << 4) | ((ground >> 8) & 0xf);
 	area->_usualBackgroundColor = 0;
 	area->_underFireBackgroundColor = 0;
+	byte groundExtraColor = 0;
+	if (isAtariST()) {
+		splitColorAtari(area->_skyColor, data.skyExtraColor);
+		splitColorAtari(area->_groundColor, groundExtraColor);
+	}
 	// The runner supplies a default floor at Y=0.
 	if (id != 255)
-		area->addFloor();
+		area->addFloor(groundExtraColor);
 	return area;
 }
 
@@ -335,6 +359,7 @@ Object *KitEngine::loadObject(Common::SeekableReadStream &file, ObjectData &data
 
 	bool geometric = data.type >= kCubeType && data.type <= kHexagonType && data.type != kSensorType;
 	Common::Array<uint8> *colors = nullptr;
+	Common::Array<uint8> *extraColors = nullptr;
 	Common::Array<float> *ordinates = nullptr;
 	if (geometric) {
 		ObjectType type = ObjectType(data.type);
@@ -347,9 +372,18 @@ Object *KitEngine::loadObject(Common::SeekableReadStream &file, ObjectData &data
 			return nullptr;
 		}
 		colors = new Common::Array<uint8>();
+		if (isAtariST())
+			extraColors = new Common::Array<uint8>();
 		for (int i = 0; i < colorCount; i += 2) {
 			byte first, second;
 			readColors(payload, first, second);
+			if (extraColors) {
+				byte extraFirst, extraSecond;
+				splitColorAtari(first, extraFirst);
+				splitColorAtari(second, extraSecond);
+				extraColors->push_back(extraFirst);
+				extraColors->push_back(extraSecond);
+			}
 			colors->push_back(first);
 			colors->push_back(second);
 		}
@@ -358,6 +392,11 @@ Object *KitEngine::loadObject(Common::SeekableReadStream &file, ObjectData &data
 			const byte sides[] = {(*colors)[2], (*colors)[0], (*colors)[3], (*colors)[1]};
 			for (uint i = 0; i < ARRAYSIZE(sides); i++)
 				(*colors)[i] = sides[i];
+			if (extraColors) {
+				const byte extraSides[] = {(*extraColors)[2], (*extraColors)[0], (*extraColors)[3], (*extraColors)[1]};
+				for (uint i = 0; i < ARRAYSIZE(extraSides); i++)
+					(*extraColors)[i] = extraSides[i];
+			}
 		}
 		if (ordinateCount) {
 			requireBytes(payload, 2 * ordinateCount);
@@ -388,7 +427,7 @@ Object *KitEngine::loadObject(Common::SeekableReadStream &file, ObjectData &data
 		// Entrances can retain editor data after their header.
 		data.extra = readWords(payload, (payload.size() - payload.pos()) / 2);
 	} else {
-		Common::String source = detokeniseKit16Condition(readCode(payload, payload.size() - payload.pos()), data.condition);
+		Common::String source = detokeniseKit16Condition(readCode(payload, payload.size() - payload.pos()), data.condition, isAtariST());
 		debugC(1, kFreescapeDebugParser, "3DCK object %u condition:\n%s", data.id, source.c_str());
 	}
 	file.seek(end);
@@ -407,7 +446,7 @@ Object *KitEngine::loadObject(Common::SeekableReadStream &file, ObjectData &data
 			(*ordinates)[i] += data.initialOrigin.getValue(i % 3);
 	}
 	return new GeometricObject(type, data.id, (data.flags & kKitInitiallyInvisible) ? 0x80 : 0,
-		data.initialOrigin, data.size, colors, nullptr, ordinates, FCLInstructionVector());
+		data.initialOrigin, data.size, colors, extraColors, ordinates, FCLInstructionVector());
 }
 
 void KitEngine::initGameState() {
@@ -424,6 +463,10 @@ void KitEngine::gotoArea(uint16 areaID, int entranceID) {
 	if (_currentArea)
 		_kitVariables[9] = _currentArea->getAreaID();
 	_currentArea = _areaMap[areaID];
+	if (isAtariST()) {
+		_gfx->_palette = _areaData[areaID].palette;
+		updateBorderAtari();
+	}
 	Entrance *entrance = static_cast<Entrance *>(_currentArea->entranceWithID(entranceID));
 	if (entrance) {
 		_position = entrance->getOrigin();
diff --git a/engines/freescape/games/3dck/3dck.h b/engines/freescape/games/3dck/3dck.h
index 49695fee625..53f3c021c59 100644
--- a/engines/freescape/games/3dck/3dck.h
+++ b/engines/freescape/games/3dck/3dck.h
@@ -37,6 +37,7 @@ public:
 	void checkIfStillInArea() override;
 	bool checkIfGameEnded() override;
 	void borderScreen() override {}
+	void drawBackground() override;
 	void drawUI() override;
 	bool handleInput(const Common::Event &event) override;
 	void updatePlayerMovement(float deltaTime) override;
@@ -88,6 +89,8 @@ private:
 		Common::HashMap<uint16, ObjectData> objects;
 		Common::Array<uint16> objectOrder;
 		Common::Array<ConditionData> conditions;
+		byte palette[16 * 3] = {};
+		byte skyExtraColor = 0;
 	};
 
 	struct ScriptEntry {
@@ -96,6 +99,11 @@ private:
 		ScriptEntry(ScriptState *s, bool r) : script(s), resume(r) {}
 	};
 
+	void loadAssetsDOS();
+	void loadAssetsAtari();
+	void loadPaletteAtari(Common::SeekableReadStream &file, byte *palette);
+	void updateBorderAtari();
+	void splitColorAtari(byte &color, byte &extraColor);
 	void loadWorld(Common::SeekableReadStream &file);
 	void loadSounds(Common::SeekableReadStream &file);
 	void playPendingSound();
@@ -147,14 +155,14 @@ private:
 	void interact(bool shot);
 	void printMessage(uint16 indicator, const Common::String &message);
 	void updateIndicators();
-	uint32 indicatorColor(byte color) const;
+	uint32 indicatorColor(byte color, int y) const;
 
 	Common::HashMap<uint16, AreaData> _areaData;
 	Common::Array<ConditionData> _globalConditions;
 	Common::Array<uint16> _indicatorData;
 	Common::Array<uint16> _controlData;
 
-	byte _palette[256 * 3];
+	byte _palette[256 * 3] = {};
 	uint16 _initialPlayerHeight;
 	uint16 _initialCondition = 0, _timerInterval = 0, _activationRange = 0;
 	uint32 _kitVariables[256] = {};
@@ -169,6 +177,7 @@ private:
 	Common::Array<ScriptEntry> _scriptQueue;
 	Common::Array<ScriptState *> _suspendedScripts;
 	Graphics::ManagedSurface _scriptSurface;
+	Graphics::ManagedSurface _borderPixels;
 };
 
 } // namespace Freescape
diff --git a/engines/freescape/games/3dck/atari.cpp b/engines/freescape/games/3dck/atari.cpp
new file mode 100644
index 00000000000..aa74f929c8f
--- /dev/null
+++ b/engines/freescape/games/3dck/atari.cpp
@@ -0,0 +1,125 @@
+/* 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/memstream.h"
+#include "image/neo.h"
+
+#include "freescape/games/3dck/3dck.h"
+
+namespace Freescape {
+
+void KitEngine::loadPaletteAtari(Common::SeekableReadStream &file, byte *palette) {
+	if (file.pos() > file.size() || file.size() - file.pos() < 32)
+		error("Truncated 3D Construction Kit Atari ST palette");
+	for (int i = 0; i < 16; i++) {
+		uint16 color = file.readUint16BE();
+		// The runner converts RGB444 to the ST's three bits per component.
+		for (int c = 0; c < 3; c++)
+			palette[3 * i + c] = ((color >> (9 - 4 * c)) & 7) * 255 / 7;
+	}
+}
+
+void KitEngine::loadAssetsAtari() {
+	Common::File file;
+	if (!file.open(_gameDescription->filesDescriptions[0].fileName))
+		error("Unable to open 3D Construction Kit Atari ST data");
+	if (file.size() < 512 || (file.size() & 1))
+		error("Invalid 3D Construction Kit Atari ST data size");
+	Common::Array<byte> data;
+	data.resize(file.size());
+	if (file.read(data.data(), data.size()) != data.size())
+		error("Truncated 3D Construction Kit Atari ST data");
+	file.close();
+
+	// Standalone runners XOR each word with their export key.
+	const byte key[] = {byte(data[0] ^ 'A'), byte(data[1] ^ 'M')};
+	for (uint i = 0; i < data.size(); i++)
+		data[i] ^= key[i & 1];
+	Common::MemoryReadStream world(data.data(), data.size());
+	loadWorld(world);
+
+	world.seek(world.size() - 8);
+	uint32 paletteOffset = world.readUint32BE();
+	uint32 paletteEnd = world.size() - 12;
+	if (paletteOffset < 500 || paletteOffset > paletteEnd ||
+			paletteEnd - paletteOffset != 34 * _areaData.size())
+		error("Invalid 3D Construction Kit Atari ST palette table");
+	Common::HashMap<uint16, bool> loadedPalettes;
+	world.seek(paletteOffset);
+	while (world.pos() < paletteEnd) {
+		uint16 area = world.readUint16BE();
+		if (!_areaData.contains(area) || loadedPalettes.contains(area))
+			error("Invalid 3D Construction Kit Atari ST palette area %u", area);
+		loadPaletteAtari(world, _areaData[area].palette);
+		loadedPalettes[area] = true;
+	}
+
+	if (!file.open(_gameDescription->filesDescriptions[1].fileName))
+		error("Unable to open 3D Construction Kit Atari ST border");
+	if (file.size() != 32128)
+		error("Invalid 3D Construction Kit Atari ST border size");
+	file.seek(4);
+	loadPaletteAtari(file, _palette);
+	file.seek(0);
+	Image::NeoDecoder decoder;
+	if (!decoder.loadStream(file) || file.err())
+		error("Unable to decode 3D Construction Kit Atari ST border");
+	_borderPixels.copyFrom(*decoder.getSurface());
+	_border = new Graphics::ManagedSurface();
+	_border->copyFrom(*decoder.getSurface());
+	_border->convertToInPlace(_gfx->_texturePixelFormat, _palette, 16);
+}
+
+void KitEngine::updateBorderAtari() {
+	// Palette changes apply to whole scanlines, including the side panels.
+	Graphics::Surface band = _borderPixels.getSubArea(Common::Rect(0, _viewArea.top, _screenW, _viewArea.bottom));
+	Graphics::Surface *colored = band.convertTo(_gfx->_texturePixelFormat, _gfx->_palette, 16);
+	_border->blitFrom(*colored, Common::Point(0, _viewArea.top));
+	colored->free();
+	delete colored;
+	processBorder();
+}
+
+void KitEngine::splitColorAtari(byte &color, byte &extraColor) {
+	extraColor = color & 0xf;
+	color >>= 4;
+	// A zero extra colour means solid fill to the renderer.
+	if (!extraColor)
+		SWAP(color, extraColor);
+}
+
+void KitEngine::drawBackground() {
+	if (!isAtariST()) {
+		FreescapeEngine::drawBackground();
+		return;
+	}
+	clearBackground();
+	byte first = _currentArea->_skyColor;
+	byte second = _areaData[_currentArea->getAreaID()].skyExtraColor;
+	byte r1, g1, b1, r2, g2, b2;
+	_gfx->readFromPalette(first, r1, g1, b1);
+	_gfx->readFromPalette(second, r2, g2, b2);
+	_gfx->clear(r1, g1, b1);
+	if (first != second)
+		_gfx->fillViewportStippled(r1, g1, b1, r2, g2, b2, _gfx->_defaultStippleArray);
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/games/3dck/ui.cpp b/engines/freescape/games/3dck/ui.cpp
index 5f757430745..52efd539943 100644
--- a/engines/freescape/games/3dck/ui.cpp
+++ b/engines/freescape/games/3dck/ui.cpp
@@ -26,9 +26,12 @@
 
 namespace Freescape {
 
-uint32 KitEngine::indicatorColor(byte color) const {
+uint32 KitEngine::indicatorColor(byte color, int y) const {
+	const byte *palette = _palette;
+	if (_renderMode == Common::kRenderAtariST && y >= _viewArea.top && y < _viewArea.bottom)
+		palette = _gfx->_palette;
 	return _scriptSurface.format.ARGBToColor(255,
-		_palette[3 * color], _palette[3 * color + 1], _palette[3 * color + 2]);
+		palette[3 * color], palette[3 * color + 1], palette[3 * color + 2]);
 }
 
 void KitEngine::printMessage(uint16 indicator, const Common::String &message) {
@@ -59,8 +62,8 @@ void KitEngine::printMessage(uint16 indicator, const Common::String &message) {
 		if (y >= surface.h)
 			break;
 		Common::Rect cell(x, y, MIN(x + 8, int(surface.w)), MIN(y + 8, int(surface.h)));
-		surface.fillRect(cell, indicatorColor(data[11]));
-		font.drawChar(&surface, chr, x, y, indicatorColor(data[10]));
+		surface.fillRect(cell, indicatorColor(data[11], rect.top + y));
+		font.drawChar(&surface, chr, x, y, indicatorColor(data[10], rect.top + y));
 		x += 8;
 		if (x >= surface.w) {
 			x = 0;
@@ -82,7 +85,7 @@ void KitEngine::updateIndicators() {
 		int32 first = int32((uint32(data[5]) << 16) | data[6]);
 		int32 last = int32((uint32(data[7]) << 16) | data[8]);
 		int32 value = CLIP<int32>(_kitVariables[data[9] & 0xff], MIN(first, last), MAX(first, last));
-		uint32 foreground = indicatorColor(data[10]), background = indicatorColor(data[11]);
+		uint32 foreground = indicatorColor(data[10], rect.top), background = indicatorColor(data[11], rect.top);
 		_scriptSurface.fillRect(rect, background);
 		if (data[0] == 2) {
 			int digits = MIN<int>(rect.width() / 8, 8);
diff --git a/engines/freescape/gfx_opengl_shaders.cpp b/engines/freescape/gfx_opengl_shaders.cpp
index a7606b1cb88..8860769b27d 100644
--- a/engines/freescape/gfx_opengl_shaders.cpp
+++ b/engines/freescape/gfx_opengl_shaders.cpp
@@ -908,10 +908,8 @@ void OpenGLShaderRenderer::fillViewportStippled(uint8 r1, uint8 g1, uint8 b1, ui
 
 	glBindBuffer(GL_ARRAY_BUFFER, _triangleVBO);
 	glBufferData(GL_ARRAY_BUFFER, 4 * 3 * sizeof(float), _verts, GL_DYNAMIC_DRAW);
-	glEnableVertexAttribArray(0);
 	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), nullptr);
 	glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
-	glDisableVertexAttribArray(0);
 
 	useStipple(false);
 	glDepthMask(GL_TRUE);
diff --git a/engines/freescape/language/detokeniser.h b/engines/freescape/language/detokeniser.h
index b31e54cd732..6afbeebdb22 100644
--- a/engines/freescape/language/detokeniser.h
+++ b/engines/freescape/language/detokeniser.h
@@ -29,7 +29,7 @@ namespace Freescape {
 // Classic games retain their byte-oriented dialect on DOS, Amiga and Atari ST.
 Common::String detokeniseFreescapeCondition(const Common::Array<uint16> &tokenisedCondition, FCLInstructionVector &instructions, bool isAmigaAtari);
 Common::String detokeniseKit8Condition(const Common::Array<byte> &tokenisedCondition, FCLInstructionVector &instructions);
-Common::String detokeniseKit16Condition(const Common::Array<byte> &tokenisedCondition, FCLInstructionVector &instructions);
+Common::String detokeniseKit16Condition(const Common::Array<byte> &tokenisedCondition, FCLInstructionVector &instructions, bool isAmigaAtari);
 
 void normaliseKitOperands(FCLInstruction &instruction);
 
diff --git a/engines/freescape/language/detokeniser_3dck16.cpp b/engines/freescape/language/detokeniser_3dck16.cpp
index dfae0771a4b..9c9a19daac6 100644
--- a/engines/freescape/language/detokeniser_3dck16.cpp
+++ b/engines/freescape/language/detokeniser_3dck16.cpp
@@ -83,18 +83,25 @@ static const FCLOpcode kKitOpcodes[] = {
 	{0xff, Token::END, "END", 0, 0, 0}
 };
 
-Common::String detokeniseKit16Condition(const Common::Array<byte> &tokenisedCondition, FCLInstructionVector &instructions) {
+Common::String detokeniseKit16Condition(const Common::Array<byte> &tokenisedCondition, FCLInstructionVector &instructions, bool isAmigaAtari) {
 	Common::String detokenisedStream;
 	int loops = 0;
 	for (uint32 bytePointer = 0; bytePointer < tokenisedCondition.size();) {
 		if (tokenisedCondition.size() - bytePointer < 2)
 			error("Truncated 16-bit FCL instruction at %u", bytePointer);
 		byte count = tokenisedCondition[bytePointer], opcode = tokenisedCondition[bytePointer + 1];
-		if (tokenisedCondition.size() - bytePointer < 2U + 2U * count)
-			error("Truncated 16-bit FCL instruction at %u", bytePointer);
 		const FCLOpcode *entry = findFCLOpcode(kKitOpcodes, opcode);
+		if (!entry && isAmigaAtari) {
+			// The Atari runner aborts this condition when the command is reached.
+			FCLInstruction instruction(Token::UNKNOWN);
+			instruction.setSource(opcode, Token::CONSTANT);
+			instructions.push_back(instruction);
+			return detokenisedStream + Common::String::format("UNKNOWN (%02x)\n", opcode);
+		}
 		if (!entry)
 			error("Unknown 16-bit FCL opcode %02x at %u", opcode, bytePointer);
+		if (tokenisedCondition.size() - bytePointer < 2U + 2U * count)
+			error("Truncated 16-bit FCL instruction at %u", bytePointer);
 		if (count < entry->minArgs || count > entry->maxArgs)
 			error("Invalid argument count for 16-bit FCL opcode %02x at %u", opcode, bytePointer);
 
diff --git a/engines/freescape/language/execution_3dck16.cpp b/engines/freescape/language/execution_3dck16.cpp
index f0e12ce7dcd..486be263ae6 100644
--- a/engines/freescape/language/execution_3dck16.cpp
+++ b/engines/freescape/language/execution_3dck16.cpp
@@ -319,6 +319,10 @@ FCLExecutionResult KitEngine::executeCode(ScriptState &script, uint &budget) {
 		uint32 ip = script.ip++;
 		const FCLInstruction &instruction = (*script.code)[ip];
 		switch (instruction.getType()) {
+		case Token::UNKNOWN:
+			warning("Unknown 16-bit FCL opcode %02x in area %u, object %u; stopping condition",
+				instruction._source, script.area, script.object ? script.object->id : 0);
+			return kFCLFinished;
 		case Token::NOP:
 		case Token::ENDIF:
 			break;
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index ef50be2b776..1b9eba6f068 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/atari.o \
 	games/3dck/c64.o \
 	games/3dck/ui.o \
 	games/3dck/zx.o \


Commit: 4ccc9ad0540f197ab98648df5061946b7a403768
    https://github.com/scummvm/scummvm/commit/4ccc9ad0540f197ab98648df5061946b7a403768
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-12T12:21:36+02:00

Commit Message:
FREESCAPE: added Egyptman for atari

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


diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index 75a8fe63c0e..04a8553c400 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -1355,6 +1355,15 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSUPPORTED,
 		GUIO1(GUIO_NOMIDI)
 	},
+	{
+		"3dkit",
+		"Egyptman v2.3 (Shareware)",
+		AD_ENTRY1s("EGYPT.RUN", "548472dcf3fd012f3a901a38476f7444", 97634),
+		Common::DE_DEU,
+		Common::kPlatformDOS,
+		ADGF_UNSTABLE,
+		GUIO2(GUIO_NOMIDI, GUIO_RENDERVGA)
+	},
 	{
 		"3dkit",
 		"Funnyrace",
diff --git a/engines/freescape/games/3dck/3dck.h b/engines/freescape/games/3dck/3dck.h
index 53f3c021c59..8bfba2e3e69 100644
--- a/engines/freescape/games/3dck/3dck.h
+++ b/engines/freescape/games/3dck/3dck.h
@@ -31,6 +31,7 @@ class KitEngine : public FreescapeEngine {
 public:
 	KitEngine(OSystem *syst, const ADGameDescription *gd);
 
+	void initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *infoScreenKeyMap, const char *target) override;
 	void loadAssets() override;
 	void initGameState() override;
 	void gotoArea(uint16 areaID, int entranceID) override;
diff --git a/engines/freescape/games/3dck/ui.cpp b/engines/freescape/games/3dck/ui.cpp
index 52efd539943..f346cfecf04 100644
--- a/engines/freescape/games/3dck/ui.cpp
+++ b/engines/freescape/games/3dck/ui.cpp
@@ -19,6 +19,9 @@
  *
  */
 
+#include "backends/keymapper/action.h"
+#include "backends/keymapper/keymap.h"
+#include "common/translation.h"
 #include "graphics/fonts/dosfont.h"
 #include "math/utils.h"
 
@@ -26,6 +29,14 @@
 
 namespace Freescape {
 
+void KitEngine::initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *infoScreenKeyMap, const char *target) {
+	FreescapeEngine::initKeymaps(engineKeyMap, infoScreenKeyMap, target);
+	Common::Action *act = new Common::Action("ACTIVATE", _("Activate"));
+	act->setCustomEngineActionEvent(kActionActivate);
+	act->addDefaultInputMapping(_useWASDControls ? "e" : "a");
+	engineKeyMap->addAction(act);
+}
+
 uint32 KitEngine::indicatorColor(byte color, int y) const {
 	const byte *palette = _palette;
 	if (_renderMode == Common::kRenderAtariST && y >= _viewArea.top && y < _viewArea.bottom)


Commit: cebd12bbf1c6ca41e43343992edebcc9654157ab
    https://github.com/scummvm/scummvm/commit/cebd12bbf1c6ca41e43343992edebcc9654157ab
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-12T12:21:36+02:00

Commit Message:
SCUMM: RA2: potential fix for L2 enemy partial decoding

Changed paths:
    engines/scumm/smush/rebel/codec_ra2.cpp
    engines/scumm/smush/rebel/codec_ra2.h
    engines/scumm/smush/rebel/smush_player_ra2.cpp


diff --git a/engines/scumm/smush/rebel/codec_ra2.cpp b/engines/scumm/smush/rebel/codec_ra2.cpp
index a8681773fd4..9e92fd52702 100644
--- a/engines/scumm/smush/rebel/codec_ra2.cpp
+++ b/engines/scumm/smush/rebel/codec_ra2.cpp
@@ -27,30 +27,31 @@
 
 namespace Scumm {
 
-static void bompDecodeLineOpaqueBounded(byte *dst, const byte *src, const byte *srcEnd, int len) {
+static void bompDecodeLineBounded(byte *dst, const byte *src, const byte *srcEnd, int len, int sourceSkipX, bool opaque) {
 	while (len > 0 && src < srcEnd) {
 		byte code = *src++;
 		int num = (code >> 1) + 1;
-		if (num > len)
-			num = len;
+		int skip = MIN(num, sourceSkipX);
+		sourceSkipX -= skip;
 
 		if (code & 1) {
 			if (src >= srcEnd)
 				break;
-			memset(dst, *src++, num);
-			dst += num;
-			len -= num;
+			byte color = *src++;
+			num = MIN(num - skip, len);
+			if (opaque || color != 0)
+				memset(dst, color, num);
 		} else {
-			int toCopy = num;
-			if (toCopy > (int)(srcEnd - src))
-				toCopy = (int)(srcEnd - src);
-			memcpy(dst, src, toCopy);
-			src += toCopy;
-			dst += toCopy;
-			len -= toCopy;
-			if (toCopy < num)
+			num = MIN(num, (int)(srcEnd - src));
+			if (skip > num)
 				break;
+			src += skip;
+			num = MIN(num - skip, len);
+			memcpy(dst, src, num);
+			src += num;
 		}
+		dst += num;
+		len -= num;
 	}
 }
 
@@ -187,6 +188,10 @@ void copyRA2Handler7PerspectiveViewport(byte *dst, int dstPitch, int dstWidth, i
 
 // RLE decoder for opaque backgrounds, including color 0.
 void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize) {
+	smushDecodeRA2RLE(dst, src, left, top, width, height, pitch, dataSize, 0, true);
+}
+
+void smushDecodeRA2RLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize, int sourceSkipX, bool opaque) {
 	if (dataSize <= 0)
 		return;
 
@@ -200,7 +205,7 @@ void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int wid
 		const byte *lineEnd = src + lineSize;
 
 		dst += left;
-		bompDecodeLineOpaqueBounded(dst, src, lineEnd, width);
+		bompDecodeLineBounded(dst, src, lineEnd, width, sourceSkipX, opaque);
 		src = lineEnd;
 		dst += pitch - left;
 	}
diff --git a/engines/scumm/smush/rebel/codec_ra2.h b/engines/scumm/smush/rebel/codec_ra2.h
index 99428f2f514..f65aceda07f 100644
--- a/engines/scumm/smush/rebel/codec_ra2.h
+++ b/engines/scumm/smush/rebel/codec_ra2.h
@@ -27,6 +27,7 @@
 namespace Scumm {
 
 void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
+void smushDecodeRA2RLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize, int sourceSkipX, bool opaque);
 void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
 void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
 void smushDecodeRA2SkipRemap(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize, const byte *remap, byte addColor);
diff --git a/engines/scumm/smush/rebel/smush_player_ra2.cpp b/engines/scumm/smush/rebel/smush_player_ra2.cpp
index e42e08cd969..bbc7c102270 100644
--- a/engines/scumm/smush/rebel/smush_player_ra2.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra2.cpp
@@ -1388,7 +1388,8 @@ bool SmushPlayerRebel2::handleGameAdjustCoords(int codec, int &left, int &top, i
 	}
 
 	if (codec == SMUSH_CODEC_LINE_UPDATE || codec == SMUSH_CODEC_LINE_UPDATE2 ||
-			codec == SMUSH_CODEC_SKIP_RLE || codec == SMUSH_CODEC_UNCOMPRESSED) {
+			codec == SMUSH_CODEC_SKIP_RLE || codec == SMUSH_CODEC_UNCOMPRESSED ||
+			(_ra2FrameSourceSkipX > 0 && (codec == SMUSH_CODEC_RLE || codec == SMUSH_CODEC_RLE_ALT))) {
 		_ra2FrameSourceSkipY = sourceSkipY;
 		if (srcSkipY)
 			*srcSkipY = 0;
@@ -1406,6 +1407,13 @@ bool SmushPlayerRebel2::handleGameCodecDecode(int codec, const uint8 *src, int l
 	if (isRebel2FullFrameDeltaCodec(codec))
 		return ra2DecodePlacedDeltaCodec(codec, src, left, top, width, height, pitch, dataSize);
 
+	if (_ra2FrameSourceSkipX > 0 && (codec == SMUSH_CODEC_RLE || codec == SMUSH_CODEC_RLE_ALT)) {
+		const bool opaque = codec == SMUSH_CODEC_RLE_ALT || (_curVideoFlags & 0x100) != 0;
+		src = smushSkipRLELines(src, dataSize, _ra2FrameSourceSkipY);
+		smushDecodeRA2RLE(_dst, src, left, top, width, height, pitch, dataSize, _ra2FrameSourceSkipX, opaque);
+		return true;
+	}
+
 	if (codec == SMUSH_CODEC_SKIP_RLE && parm2 >= 0x100) {
 		if (parm2 == 0x100 && dataSize >= 256) {
 			memcpy(_ra2SkipRemapTable, src, 256);


Commit: e973f7e7fb662383d8fb41332d6047feff4c8cf4
    https://github.com/scummvm/scummvm/commit/e973f7e7fb662383d8fb41332d6047feff4c8cf4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-12T13:24:06+02:00

Commit Message:
SCUMM: RA2: added missing blur effect affecting at least L2

Changed paths:
    engines/scumm/insane/rebel2/render.cpp
    engines/scumm/smush/rebel/codec_ra2.h
    engines/scumm/smush/rebel/font_rebel2.cpp


diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 77a3edc8560..656ad55fc71 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -3515,9 +3515,12 @@ void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int wid
 			int overlayDrawX = renderHiRes ? (nativeOverlayX - nativeViewX) * renderScale : nativeOverlayX;
 			int overlayDrawY = renderHiRes ? (nativeOverlayY - nativeViewY) * renderScale : nativeOverlayY;
 
-			renderNutSpriteScaledClipped(renderBitmap, pitch, width, renderHeight,
-				0, 0, width, renderHeight,
-				overlayDrawX, overlayDrawY, _grd005Sprite, overlayIdx, false, renderScale, false);
+			if (!drawRebel2Codec45Sprite(_grd005Sprite, renderBitmap, pitch, width, height,
+					Common::Rect(0, 0, width, renderHeight), overlayDrawX, overlayDrawY, overlayIdx, renderScale)) {
+				renderNutSpriteScaledClipped(renderBitmap, pitch, width, renderHeight,
+					0, 0, width, renderHeight,
+					overlayDrawX, overlayDrawY, _grd005Sprite, overlayIdx, false, renderScale, false);
+			}
 
 			debugC(DEBUG_INSANE, "Handler25 PRE: GRD005 at (%d,%d) nutOff(%d,%d) viewOff(%d,%d) size(%d,%d) mode=%d scale=%d",
 				overlayDrawX, overlayDrawY, overlayXOffset, overlayYOffset,
diff --git a/engines/scumm/smush/rebel/codec_ra2.h b/engines/scumm/smush/rebel/codec_ra2.h
index f65aceda07f..972fc90e7c0 100644
--- a/engines/scumm/smush/rebel/codec_ra2.h
+++ b/engines/scumm/smush/rebel/codec_ra2.h
@@ -31,6 +31,7 @@ void smushDecodeRA2RLE(byte *dst, const byte *src, int left, int top, int width,
 void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
 void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
 void smushDecodeRA2SkipRemap(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize, const byte *remap, byte addColor);
+bool smushPrepareRA2BlurData(const byte *src, int dataSize, byte *palette, byte *lookup, const byte *&maskData, int &maskSize);
 void smushDecodeRA2Blur(byte *dst, const byte *src, int left, int top, int dstWidth, int dstHeight, int pitch, int dataSize, byte *palette, byte *lookup);
 void smushDecodeRA2BlurClip(byte *dst, const byte *src, int left, int top,
 		int clipLeft, int clipTop, int clipRight, int clipBottom,
diff --git a/engines/scumm/smush/rebel/font_rebel2.cpp b/engines/scumm/smush/rebel/font_rebel2.cpp
index 7b903401e63..ceb8e23b885 100644
--- a/engines/scumm/smush/rebel/font_rebel2.cpp
+++ b/engines/scumm/smush/rebel/font_rebel2.cpp
@@ -547,12 +547,20 @@ void Rebel2NutRenderer::loadRebel2SpriteFromData(const byte *data, int32 dataSiz
 						memcpy(_codec23Lookup, frame.data, sizeof(_codec23Lookup));
 					}
 
+					// Codec 45 setup frames have no drawable dimensions.
+					if (frame.codec == 45 && ((int16)frame.width < 0 || (int16)frame.height < 0)) {
+						frame.width = 0;
+						frame.height = 0;
+					}
+
 					const uint64 pixels = (uint64)frame.width * frame.height;
 					if (pixels == 0) {
 						frame.width = 0;
 						frame.height = 0;
-						frame.data = nullptr;
-						frame.dataSize = 0;
+						if (frame.codec != 45) {
+							frame.data = nullptr;
+							frame.dataSize = 0;
+						}
 					} else if (pixels > kRebel2MaxSpritePixels || decodedLength + pixels > kRebel2MaxDecodedSpriteBytes) {
 						warning("Rebel2NutRenderer::loadRebel2SpriteFromData: invalid sprite dimensions %ux%u at frame %d",
 							frame.width, frame.height, frameCount);
@@ -596,6 +604,12 @@ void Rebel2NutRenderer::loadRebel2SpriteFromData(const byte *data, int32 dataSiz
 		_chars[i].height = frame.height;
 		_chars[i].transparency = kDefaultTransparentColor;
 
+		if (frame.codec == 45 && frame.data) {
+			const byte *maskData;
+			int maskSize;
+			smushPrepareRA2BlurData(frame.data, frame.dataSize, _codec45Palette, _codec45Lookup, maskData, maskSize);
+		}
+
 		if (frame.width == 0 || frame.height == 0 || frame.data == nullptr)
 			continue;
 


Commit: 3474234780145c5014bfd9dedcdd44f232a6ed41
    https://github.com/scummvm/scummvm/commit/3474234780145c5014bfd9dedcdd44f232a6ed41
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-12T13:54:23+02:00

Commit Message:
SCUMM: RA2: properly implement pause menu including film effect

Changed paths:
    engines/scumm/insane/rebel2/menu.cpp
    engines/scumm/insane/rebel2/rebel.cpp
    engines/scumm/insane/rebel2/rebel.h
    engines/scumm/smush/rebel/smush_player_ra2.cpp
    engines/scumm/smush/rebel/smush_player_ra2.h
    engines/scumm/smush/smush_player.h


diff --git a/engines/scumm/insane/rebel2/menu.cpp b/engines/scumm/insane/rebel2/menu.cpp
index a4d1a537894..b20bccd4d8a 100644
--- a/engines/scumm/insane/rebel2/menu.cpp
+++ b/engines/scumm/insane/rebel2/menu.cpp
@@ -23,6 +23,7 @@
 #include "common/system.h"
 #include "common/events.h"
 #include "common/util.h"
+#include "graphics/blit.h"
 #include "graphics/paletteman.h"
 
 #include "audio/mixer.h"
@@ -368,78 +369,110 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
 	drawMenuItems(renderBitmap, pitch, width, height, menuItems, 7, _menuSelection);
 }
 
-void pauseFillRect(byte *buf, int bufW, int bufH, int x, int y, int w, int h, byte color) {
-	if (x < 0) { w += x; x = 0; }
-	if (y < 0) { h += y; y = 0; }
-	if (x + w > bufW) w = bufW - x;
-	if (y + h > bufH) h = bufH - y;
-	if (w <= 0 || h <= 0) return;
-	for (int row = y; row < y + h; row++)
-		memset(buf + row * bufW + x, color, w);
+byte blendRebel2PausePixels(byte a, byte b) {
+	if (a > b)
+		SWAP(a, b);
+	if (a >= 16 && b < 80)
+		return (a + b) / 2;
+	if (b == 0 || b == 4 || (b >= 16 && b < 80))
+		return b;
+	return a;
+}
+
+void scaleRebel2PauseBackground(const Graphics::Surface &src, Graphics::Surface &dst) {
+	dst.create(src.w * 2, src.h * 2, src.format);
+	for (int y = 0; y < src.h; y++) {
+		const byte *srcRow = (const byte *)src.getBasePtr(0, y);
+		byte *dstRow = (byte *)dst.getBasePtr(0, y * 2);
+		for (int x = 0; x < src.w; x++) {
+			dstRow[x * 2] = srcRow[x];
+			const byte nextColor = x + 1 < src.w ? srcRow[x + 1] : 0;
+			dstRow[x * 2 + 1] = blendRebel2PausePixels(srcRow[x], nextColor);
+		}
+	}
+	for (int y = 1; y < dst.h - 1; y += 2) {
+		byte *row = (byte *)dst.getBasePtr(0, y);
+		for (int x = 0; x < dst.w; x++)
+			row[x] = blendRebel2PausePixels(row[x - dst.pitch], row[x + dst.pitch]);
+	}
+	memcpy(dst.getBasePtr(0, dst.h - 1), dst.getBasePtr(0, dst.h - 2), dst.w);
 }
 
 void InsaneRebel2::showPauseOverlay() {
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	if (!splayer)
+	if (!splayer || _pauseOverlayActive)
 		return;
 
-	byte *frameBuffer = splayer->_dst;
-	byte *palette = splayer->_pal;
-	int width = splayer->_width;
-	int height = splayer->_height;
-
-	if (!frameBuffer || !palette || width <= 0 || height <= 0)
+	// Capture the visible viewport without modifying the video decoder's buffers.
+	Graphics::Surface *screen = _vm->_system->lockScreen();
+	if (!screen)
 		return;
-
-	int screenW = MIN(width, (int)_vm->_screenWidth);
-	int screenH = MIN(height, (int)_vm->_screenHeight);
-
-	memcpy(_savedPausePalette, palette, 768);
+	_savedPauseScreen.copyFrom(*screen);
+	_vm->_system->unlockScreen();
+	_vm->_system->getPaletteManager()->grabPalette(_savedPausePalette, 0, 256);
 	_pauseOverlayActive = true;
 
-	byte dimPal[768];
-	memcpy(dimPal, palette, 768);
-	for (int i = 0; i < 768; i++)
-		dimPal[i] >>= 2;
-
-	dimPal[0x50 * 3 + 0] = 80; dimPal[0x50 * 3 + 1] = 80; dimPal[0x50 * 3 + 2] = 80;
-	dimPal[0x51 * 3 + 0] = 110; dimPal[0x51 * 3 + 1] = 110; dimPal[0x51 * 3 + 2] = 110;
-	dimPal[1 * 3 + 0] = 255; dimPal[1 * 3 + 1] = 255; dimPal[1 * 3 + 2] = 255;
-	dimPal[2 * 3 + 0] = 188; dimPal[2 * 3 + 1] = 188; dimPal[2 * 3 + 2] = 188;
-	dimPal[3 * 3 + 0] = 128; dimPal[3 * 3 + 1] = 128; dimPal[3 * 3 + 2] = 128;
-	dimPal[4 * 3 + 0] = 0;   dimPal[4 * 3 + 1] = 0;   dimPal[4 * 3 + 2] = 0;
-	dimPal[5 * 3 + 0] = 252; dimPal[5 * 3 + 1] = 252; dimPal[5 * 3 + 2] = 252;
-
-	_vm->_system->getPaletteManager()->setPalette(dimPal, 0, 256);
-
-	pauseFillRect(frameBuffer, width, height, 0, 0x17, 0x140, 2, 0x50);
-	pauseFillRect(frameBuffer, width, height, 0, 0xAF, 0x140, 2, 0x50);
-
-	pauseFillRect(frameBuffer, width, height, 0,     0, 0x28, 200, 0x50);
-	pauseFillRect(frameBuffer, width, height, 0x118, 0, 0x28, 200, 0x50);
-
-	for (int i = 0; i < 6; i++) {
-		int yOff = i * 0x24;  // i * 36
-		pauseFillRect(frameBuffer, width, height, 0x0C, yOff,     0x19, 0x11, 0x51);
-		pauseFillRect(frameBuffer, width, height, 0x0B, yOff + 1, 0x1B, 0x0F, 0x51);
-		pauseFillRect(frameBuffer, width, height, 0x0D, yOff,     0x17, 0x11, 4);
-		pauseFillRect(frameBuffer, width, height, 0x0B, yOff + 2, 0x1B, 0x0D, 4);
-		pauseFillRect(frameBuffer, width, height, 0x0C, yOff + 1, 0x19, 0x0F, 4);
+	Graphics::Surface background;
+	background.create(320, 200, Graphics::PixelFormat::createFormatCLUT8());
+	byte *pixels = (byte *)background.getPixels();
+	Graphics::scaleBlit(pixels, (const byte *)_savedPauseScreen.getPixels(),
+		background.pitch, _savedPauseScreen.pitch, background.w, background.h,
+		_savedPauseScreen.w, _savedPauseScreen.h, background.format);
+
+	// The original uses a 64-shade tint, then repeats the neighbouring film frames.
+	byte previousColor = pixels[0];
+	for (int i = 0; i < background.w * background.h; i++) {
+		const byte color = pixels[i];
+		pixels[i] = ((_savedPausePalette[previousColor * 3 + 1] + _savedPausePalette[color * 3 + 1]) >> 3) + 16;
+		previousColor = color;
+	}
+	memcpy(background.getBasePtr(0, 0), background.getBasePtr(0, 150), 25 * background.pitch);
+	memcpy(background.getBasePtr(0, 175), background.getBasePtr(0, 25), 25 * background.pitch);
+
+	background.fillRect(Common::Rect(0, 23, 320, 25), 80);
+	background.fillRect(Common::Rect(0, 175, 320, 177), 80);
+	background.fillRect(Common::Rect(0, 0, 40, 200), 80);
+	background.fillRect(Common::Rect(280, 0, 320, 200), 80);
+	for (int side = 0; side < 2; side++) {
+		const int x = side ? 282 : 11;
+		for (int i = 0; i < 6; i++) {
+			const int y = i * 36;
+			background.fillRect(Common::Rect(x + 1, y, x + 26, y + 17), 81);
+			background.fillRect(Common::Rect(x, y + 1, x + 27, y + 16), 81);
+			background.fillRect(Common::Rect(x + 2, y, x + 25, y + 17), 4);
+			background.fillRect(Common::Rect(x, y + 2, x + 27, y + 15), 4);
+			background.fillRect(Common::Rect(x + 1, y + 1, x + 26, y + 16), 4);
+		}
 	}
 
-	for (int i = 0; i < 6; i++) {
-		int yOff = i * 0x24;
-		int xBase = 0x11A;
-		pauseFillRect(frameBuffer, width, height, xBase,     yOff,     0x19, 0x11, 0x51);
-		pauseFillRect(frameBuffer, width, height, xBase - 1, yOff + 1, 0x1B, 0x0F, 0x51);
-		pauseFillRect(frameBuffer, width, height, xBase + 1, yOff,     0x17, 0x11, 4);
-		pauseFillRect(frameBuffer, width, height, xBase - 1, yOff + 2, 0x1B, 0x0D, 4);
-		pauseFillRect(frameBuffer, width, height, xBase,     yOff + 1, 0x19, 0x0F, 4);
+	byte pausePalette[768];
+	memcpy(pausePalette, _savedPausePalette, sizeof(pausePalette));
+	const byte textColors[] = {
+		0, 0, 0, 255, 255, 255, 188, 188, 188, 128, 128, 128, 0, 0, 0,
+		0, 255, 0, 0, 199, 0, 0, 147, 0, 0, 95, 0
+	};
+	memcpy(pausePalette, textColors, sizeof(textColors));
+	for (int i = 0; i < 64; i++) {
+		// Original HLS palette: hue 10, lightness 2*i, saturation 78.
+		const int m1 = 354 * i;
+		const int m2 = 666 * i;
+		pausePalette[(16 + i) * 3] = m2 / 255;
+		pausePalette[(16 + i) * 3 + 1] = (m1 + (m2 - m1) * 60 / 255) / 255;
+		pausePalette[(16 + i) * 3 + 2] = m1 / 255;
 	}
+	const byte filmColors[] = {65, 36, 39, 33, 18, 29, 49, 27, 34, 65, 36, 39};
+	memcpy(pausePalette + 80 * 3, filmColors, sizeof(filmColors));
+
+	Graphics::Surface overlay;
+	if (isHiRes())
+		scaleRebel2PauseBackground(background, overlay);
+	else
+		overlay.copyFrom(background);
+	background.free();
 
 	const char *pauseText = splayer->getString(0x78);
 	if (!pauseText || !pauseText[0])
-		pauseText = "Game Paused";
+		pauseText = "^f01^c005Game Paused";
 
 	SmushMultiFont *multiFont = splayer->getMultiFont();
 	if (!multiFont) {
@@ -447,12 +480,27 @@ void InsaneRebel2::showPauseOverlay() {
 		multiFont = splayer->getMultiFont();
 	}
 	if (multiFont) {
-		Common::Rect clipRect(0, 0, screenW, screenH);
-		multiFont->drawString(pauseText, frameBuffer, clipRect, 10, 10, width, 4, kStyleAlignLeft);
+		Common::Rect clipRect(overlay.w, overlay.h);
+		multiFont->drawString(pauseText, (byte *)overlay.getPixels(), clipRect,
+			10, isHiRes() ? 20 : 10, overlay.pitch, 1, kStyleAlignLeft);
 	}
 
-	_vm->_system->copyRectToScreen(frameBuffer, width, 0, 0, screenW, screenH);
+	_vm->_system->getPaletteManager()->setPalette(pausePalette, 0, 256);
+	_vm->_system->copyRectToScreen(overlay.getPixels(), overlay.pitch, 0, 0, overlay.w, overlay.h);
+	_vm->_system->updateScreen();
+	overlay.free();
+}
+
+void InsaneRebel2::hidePauseOverlay() {
+	if (!_pauseOverlayActive)
+		return;
+
+	_vm->_system->getPaletteManager()->setPalette(_savedPausePalette, 0, 256);
+	_vm->_system->copyRectToScreen(_savedPauseScreen.getPixels(), _savedPauseScreen.pitch,
+		0, 0, _savedPauseScreen.w, _savedPauseScreen.h);
 	_vm->_system->updateScreen();
+	_savedPauseScreen.free();
+	_pauseOverlayActive = false;
 }
 
 int InsaneRebel2::runMainMenu() {
diff --git a/engines/scumm/insane/rebel2/rebel.cpp b/engines/scumm/insane/rebel2/rebel.cpp
index d449a8fd3da..9761d3aa758 100644
--- a/engines/scumm/insane/rebel2/rebel.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -592,6 +592,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 InsaneRebel2::~InsaneRebel2() {
 	restoreIOSGamepadController();
 	setVirtualKeyboardVisible(false);
+	_savedPauseScreen.free();
 
 	_vm->_system->getEventManager()->getEventDispatcher()->unregisterObserver(this);
 
@@ -638,10 +639,7 @@ void InsaneRebel2::openGameplayMainMenu(SmushPlayer *splayer) {
 	if (!splayer)
 		return;
 
-	if (_pauseOverlayActive) {
-		_vm->_system->getPaletteManager()->setPalette(_savedPausePalette, 0, 256);
-		_pauseOverlayActive = false;
-	}
+	hidePauseOverlay();
 
 	if (!splayer->_paused)
 		splayer->pause();
@@ -966,10 +964,7 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 
 		if (pressed && splayer && splayer->_paused && _gameState == kStateGameplay) {
 			debugC(DEBUG_INSANE, "Joystick action while paused - unpausing");
-			if (_pauseOverlayActive) {
-				_vm->_system->getPaletteManager()->setPalette(_savedPausePalette, 0, 256);
-				_pauseOverlayActive = false;
-			}
+			hidePauseOverlay();
 			splayer->unpause();
 			return true;
 		}
@@ -1095,10 +1090,7 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 
 		if (splayer && splayer->_paused && _gameState == kStateGameplay) {
 			debugC(DEBUG_INSANE, "Key pressed while paused - unpausing");
-			if (_pauseOverlayActive) {
-				_vm->_system->getPaletteManager()->setPalette(_savedPausePalette, 0, 256);
-				_pauseOverlayActive = false;
-			}
+			hidePauseOverlay();
 			splayer->unpause();
 			if (event.kbd.keycode == Common::KEYCODE_ESCAPE && _rebelHandler != 0) {
 				debugC(DEBUG_INSANE, "ESC during pause - opening global menu");
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index ecaec832099..880451c70a8 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -40,6 +40,8 @@
 #include "audio/audiostream.h"
 #include "audio/mixer.h"
 
+#include "graphics/surface.h"
+
 namespace Audio {
 class QueuingAudioStream;
 class SoundHandle;
@@ -388,6 +390,7 @@ public:
 	NutRenderer *_smush_povfontNut;
 
 	byte _savedPausePalette[768];
+	Graphics::Surface _savedPauseScreen;
 	bool _pauseOverlayActive;
 
 	bool _introCursorPushed;
@@ -918,6 +921,7 @@ public:
 	void renderScoreHUD(byte *renderBitmap, int pitch, int width, int height, int statusBarY);
 
 	void showPauseOverlay();
+	void hidePauseOverlay();
 
 	int _targetLockTimer;
 
diff --git a/engines/scumm/smush/rebel/smush_player_ra2.cpp b/engines/scumm/smush/rebel/smush_player_ra2.cpp
index bbc7c102270..a346b4896ce 100644
--- a/engines/scumm/smush/rebel/smush_player_ra2.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra2.cpp
@@ -131,6 +131,13 @@ SmushPlayerRebel2::~SmushPlayerRebel2() {
 	destroyGamePlayerFields();
 }
 
+void SmushPlayerRebel2::unpause() {
+	// A modal dialog or focus change must not dismiss the in-game pause.
+	if (_insane && static_cast<InsaneRebel2 *>(_insane)->_pauseOverlayActive)
+		return;
+	SmushPlayer::unpause();
+}
+
 void SmushPlayerRebel2::initGamePlayerFields() {
 	_multiFont = nullptr;
 	_storedFobjData = nullptr;
@@ -244,6 +251,10 @@ void SmushPlayerRebel2::initGameVideoState() {
 }
 
 void SmushPlayerRebel2::releaseGameVideoState() {
+	if (_insane && static_cast<InsaneRebel2 *>(_insane)->_pauseOverlayActive) {
+		static_cast<InsaneRebel2 *>(_insane)->hidePauseOverlay();
+		unpause();
+	}
 	delete _loadContinuationStream;
 	_loadContinuationStream = nullptr;
 	_loadPlaybackPending = false;
diff --git a/engines/scumm/smush/rebel/smush_player_ra2.h b/engines/scumm/smush/rebel/smush_player_ra2.h
index 9326c8e8210..8543a8041e3 100644
--- a/engines/scumm/smush/rebel/smush_player_ra2.h
+++ b/engines/scumm/smush/rebel/smush_player_ra2.h
@@ -30,6 +30,7 @@ class SmushPlayerRebel2 : public SmushPlayer {
 public:
 	SmushPlayerRebel2(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane);
 	~SmushPlayerRebel2() override;
+	void unpause() override;
 	bool ra2PromoteCurrentFrameToHiRes(int scrollX, int scrollY);
 	bool ra2PromoteHandler7PerspectiveToHiRes(int perspectiveX, int perspectiveY, int viewShift);
 	bool isPlayingLoadBuffer() const { return _loadContinuationStream != nullptr; }
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index e4ee345163b..0889ae373fa 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -258,7 +258,7 @@ public:
 	virtual ~SmushPlayer();
 
 	void pause();
-	void unpause();
+	virtual void unpause();
 
 	void play(const char *filename, int32 speed, int32 offset = 0, int32 startFrame = 0);
 	void release();




More information about the Scummvm-git-logs mailing list