[Scummvm-git-logs] scummvm master -> 7faac192b0d78c44ba805ed9e1ac57a359d7d815

Helco noreply at scummvm.org
Wed Feb 4 15:56:56 UTC 2026


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

Summary:
6a9b4e8d9b ALCACHOFA: Add detection entry for disk-copied edicion original
83b27e9efc ALCACHOFA: Add abstraction for game startup
48e68a9a71 ALCACHOFA: Add support for V1 world files
b6ad37d4b1 ALCACHOFA: Use SeekableReadStream for object reads
6f74da160b ALCACHOFA: Add support for V1 rooms
81ddc5c1e0 ALCACHOFA: Support reading V1 objects
8e3a73a757 ALCACHOFA: Support V1 text files
a16216b86a ALCACHOFA: Simplify room reading with V1 vs V3
7000df5f76 ALCACHOFA: Support V1 script files
ed18828503 ALCACHOFA: Add script translation maps
c6c0723605 ALCACHOFA: Add reading of V1 images
0d9d8e1efa ALCACHOFA: Support reading V1 animations
b800ace239 ALCACHOFA: Fix V1 animation reading
f89eb3f6a4 ALCACHOFA: Fix script variable count
294faec0cb ALCACHOFA: Implement differences in script instructions
81d9c9c6d2 ALCACHOFA: Fix script variable count
2a92c67e26 ALCACHOFA: Fix V1 backgrounds and empty animations
ab1cbeb172 ALCACHOFA: Replace dependencies on version-dependent script variables
7ff8d738bb ALCACHOFA: Add kernel task Disguise
4d269d4ac1 ALCACHOFA: Fix regression with file-based animation refs
d368ed8354 ALCACHOFA: Use game-specific video paths
5108587f16 ALCACHOFA: Use game-specific sound paths
e8e352c20b ALCACHOFA: Fix V1 image rendering
1d69d82ed2 ALCACHOFA: Disable cursor frame update on V1
ff4195f43e ALCACHOFA: Adapt V1 isAllowedToOpenMenu
1deb9a1156 ALCACHOFA: Adapt different MainCharacterKind values in V1
f880658010 ALCACHOFA: Adapt V1 text layout and rendering
783301dbcf ALCACHOFA: Fix room bounds for V1
d182cef022 ALCACHOFA: Fix faulty rebase
d10f4db574 ALCACHOFA: Remove unused isGameLoaded variable
f27397c0c0 ALCACHOFA: Fix disguise kernel proc and prepare locking changes
1d7468a6cc ALCACHOFA: Adapt interaction and menu conditions for V1
1cdfcdbfe5 ALCACHOFA: Fix room transitions when examining items
cb07e9349d ALCACHOFA: V1: Fix change character button
030df0392a ALCACHOFA: V1: Add inventory button
6cb669d0e9 ALCACHOFA: Add script debug command
ce073b3bec ALCACHOFA: V1: Fix stack handling of ReturnVoid
e8c9400dca ALCACHOFA: V1: Fix animation sprite order
04fc32f14c ALCACHOFA: V1: Fix room bounds and 3D draw positions
c5d17a19f4 ALCACHOFA: V1: Implement V1-style menu
a304228f6f ALCACHOFA: Add subtitles toggle key
f5d7db4017 ALCACHOFA: V1: Fix subtitle position
72f6a63534 ALCACHOFA: Add call address stack
6ab60b8648 ALCACHOFA: V1: Fix missing character icon during dialog menus
f6f596eb1e ALCACHOFA: V1: Fix various original bugs in POBLADO_INDIO
ff43ad36dc ALCACHOFA: V1: Fix object ref from other room
b0f962d68c ALCACHOFA: V1: Downport fix for Puerta_Casa_Freddy_Intermedia
23c14147b2 ALCACHOFA: Fix misuse of variable address for kernel args
aff06afe3d ALCACHOFA: V1: Fix sound paths with extension and mixed-casing
07805df596 ALCACHOFA: Fix invalid music when changing chars
5fdf0dfd99 ALCACHOFA: V1: Adapt ScriptTimerTask
afa758a8e8 ALCACHOFA: V1: Add support for Terror and Vaqueros
92530947ce ALCACHOFA: V1: Fix loading aventuradecine saves
080caa0613 ALCACHOFA: Fix display of untranslated object names
7020ad213a ALCACHOFA: V1: Fix character icon during input waiting
c7a29913bb ALCACHOFA: V1: Fix CamDisguiseTask not moving camera
8fe6f1d12f ALCACHOFA: V1: Fix load being blocked after getting card deck in CARROMATO
0c8ded9504 ALCACHOFA: V1: Disable collision avoidance
e784fb80b9 ALCACHOFA: V1: Fix size of main character
2dc75afa6d ALCACHOFA: V1: Add option to enable texture filtering
25a0e0e193 ALCACHOFA: V1: Fix sound issues in Intro_Terror
7faac192b0 ALCACHOFA: Fix compilation on ScopedPtr conversion


Commit: 6a9b4e8d9be5f3af7615187a5fb59b7f562b2e43
    https://github.com/scummvm/scummvm/commit/6a9b4e8d9be5f3af7615187a5fb59b7f562b2e43
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:25+01:00

Commit Message:
ALCACHOFA: Add detection entry for disk-copied edicion original

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/detection_tables.h


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 065d3e542f2..a83aae6d5a4 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -310,6 +310,8 @@ void AlcachofaEngine::pauseEngineIntern(bool pause) {
 }
 
 bool AlcachofaEngine::canLoadGameStateCurrently(U32String *msg) {
+	if (_menu == nullptr)
+		return false; // the autosave wants to trigger even during error() while starting the game
 	if (!_eventLoopSemaphore.isReleased())
 		return false;
 	return
diff --git a/engines/alcachofa/detection_tables.h b/engines/alcachofa/detection_tables.h
index 63ed05870c2..eb77e0043a7 100644
--- a/engines/alcachofa/detection_tables.h
+++ b/engines/alcachofa/detection_tables.h
@@ -127,6 +127,22 @@ const AlcachofaGameDescription gameDescriptions[] = {
 		EngineVersion::V3_0
 	},
 
+	{
+		// Disk files copied into disk1/disk2
+		{
+			"aventuradecine",
+			"Mortadelo y Filemón: Una Aventura de Cine - Edición Original",
+			AD_ENTRY2s(
+				"disk1/Install/oeste.emc", "b4c1084557d4cfbae336f0e741ec9e9f", 183099320,
+				"disk2/Install/terror.emc", "dc9357ee618bff160e2e2afa168ba913", 170113868
+			),
+			Common::ES_ESP,
+			Common::kPlatformWindows,
+			ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE | ADGF_CD
+		},
+		EngineVersion::V1_0
+	},
+
 	{ AD_TABLE_END_MARKER, EngineVersion::V1_0 }
 };
 


Commit: 83b27e9efc766865a719a9e10712698ebaea721c
    https://github.com/scummvm/scummvm/commit/83b27e9efc766865a719a9e10712698ebaea721c
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:25+01:00

Commit Message:
ALCACHOFA: Add abstraction for game startup

Changed paths:
  A engines/alcachofa/game-movie-adventure-original.cpp
  A engines/alcachofa/game-movie-adventure-special.cpp
  R engines/alcachofa/game-movie-adventure.cpp
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.h
    engines/alcachofa/detection_tables.h
    engines/alcachofa/game.cpp
    engines/alcachofa/game.h
    engines/alcachofa/module.mk
    engines/alcachofa/rooms.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index a83aae6d5a4..17b43f1c126 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -78,8 +78,8 @@ Common::String AlcachofaEngine::getGameId() const {
 Common::Error AlcachofaEngine::run() {
 	g_system->showMouse(false);
 	setDebugger(_console);
-	_game.reset(Game::createForMovieAdventure());
-	_renderer.reset(IRenderer::createOpenGLRenderer(Common::Point(1024, 768)));
+	_game.reset(Game::create());
+	_renderer.reset(IRenderer::createOpenGLRenderer(game().getResolution()));
 	_drawQueue.reset(new DrawQueue(_renderer.get()));
 	_world.reset(new World());
 	_script.reset(new Script());
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index ea68e463690..05123797ebe 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -117,6 +117,10 @@ public:
 	inline bool isV3() const { return gameDescription().isVersionBetween(30, 39); }
 
 	inline const AlcachofaGameDescription &gameDescription() const { return *_gameDescription; }
+	inline bool isV1() const { return _gameDescription->engineVersion == EngineVersion::V1; }
+	inline bool isV2() const { return _gameDescription->engineVersion == EngineVersion::V2; }
+	inline bool isV3() const { return _gameDescription->engineVersion == EngineVersion::V3; }
+
 	inline IRenderer &renderer() { return *_renderer; }
 	inline DrawQueue &drawQueue() { return *_drawQueue; }
 	inline Camera &camera() { return _camera; }
diff --git a/engines/alcachofa/detection_tables.h b/engines/alcachofa/detection_tables.h
index eb77e0043a7..bee3943db5b 100644
--- a/engines/alcachofa/detection_tables.h
+++ b/engines/alcachofa/detection_tables.h
@@ -28,7 +28,7 @@ const PlainGameDescriptor alcachofaGames[] = {
 
 const AlcachofaGameDescription gameDescriptions[] = {
 	//
-	// A Movie Adventure
+	// A Movie Adventure - Edicion Especial
 	//
 	{
 		{
@@ -127,6 +127,9 @@ const AlcachofaGameDescription gameDescriptions[] = {
 		EngineVersion::V3_0
 	},
 
+	//
+	// A Movie Adventure - Edicion Original
+	//
 	{
 		// Disk files copied into disk1/disk2
 		{
@@ -143,7 +146,7 @@ const AlcachofaGameDescription gameDescriptions[] = {
 		EngineVersion::V1_0
 	},
 
-	{ AD_TABLE_END_MARKER, EngineVersion::V1_0 }
+	{ AD_TABLE_END_MARKER }
 };
 
 } // End of namespace Alcachofa
diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
new file mode 100644
index 00000000000..a9e404bcd4b
--- /dev/null
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -0,0 +1,62 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "alcachofa/alcachofa.h"
+#include "alcachofa/game.h"
+#include "alcachofa/script.h"
+
+#include "common/config-manager.h"
+
+using namespace Common;
+
+namespace Alcachofa {
+
+class GameMovieAdventureOriginal : public Game {
+public:
+	GameMovieAdventureOriginal() {
+		const auto &desc = g_engine->gameDescription();
+		if (desc.desc.flags & ADGF_CD) {
+			const Path gameDir = ConfMan.getPath("path");
+			SearchMan.addDirectory(gameDir.append("disk1/Install"));
+			SearchMan.addDirectory(gameDir.append("disk2/Install"));
+		}
+	}
+
+	Point getResolution() {
+		return Point(800, 600);
+	}
+
+	static constexpr const char *kMapFiles[] = {
+		"oeste.emc",
+		"terror.emc",
+		"global.emc",
+		nullptr
+	};
+	const char *const *getMapFiles() {
+		return kMapFiles;
+	}
+};
+
+Game *Game::createForMovieAdventureOriginal() {
+	return new GameMovieAdventureOriginal();
+}
+
+}
diff --git a/engines/alcachofa/game-movie-adventure.cpp b/engines/alcachofa/game-movie-adventure-special.cpp
similarity index 99%
rename from engines/alcachofa/game-movie-adventure.cpp
rename to engines/alcachofa/game-movie-adventure-special.cpp
index 54611ba097c..215ab5a6bec 100644
--- a/engines/alcachofa/game-movie-adventure.cpp
+++ b/engines/alcachofa/game-movie-adventure-special.cpp
@@ -4,7 +4,7 @@
  * 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
+ * This program is free software: you can redistribute it and/or modifyhttps://store.steampowered.com/app/3012980/Moorhuhn_Kart_4/
  * 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.
@@ -458,7 +458,7 @@ public:
 	}
 };
 
-Game *Game::createForMovieAdventure() {
+Game *Game::createForMovieAdventureSpecial() {
 	if (g_engine->version() == EngineVersion::V3_0)
 		return new GameMovieAdventureSpecialV30();
 	else
diff --git a/engines/alcachofa/game.cpp b/engines/alcachofa/game.cpp
index a69f6543798..36c2abbf59c 100644
--- a/engines/alcachofa/game.cpp
+++ b/engines/alcachofa/game.cpp
@@ -199,4 +199,17 @@ void Game::invalidVideo(int32 videoId, const char *context) {
 	_message("Could not play video %d (%s)", videoId, context);
 }
 
+Game *Game::create() {
+	const auto &desc = g_engine->gameDescription();
+	switch (desc.engineVersion) {
+	case EngineVersion::V1:
+		return createForMovieAdventureOriginal();
+	case EngineVersion::V3:
+		return createForMovieAdventureSpecial();
+	default:
+		error("Unsupported or invalid engine version: %d", (int)desc.engineVersion);
+		break;
+	}
+}
+
 }
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index 6f258055afc..0207edd36b6 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -100,7 +100,9 @@ public:
 	virtual bool isKnownBadVideo(int32 videoId);
 	virtual void invalidVideo(int32 videoId, const char *context);
 
-	static Game *createForMovieAdventure();
+	static Game *create();
+	static Game *createForMovieAdventureSpecial();
+	static Game *createForMovieAdventureOriginal();
 
 	const Message _message;
 };
diff --git a/engines/alcachofa/module.mk b/engines/alcachofa/module.mk
index 294ea76ce07..7921ffcc97d 100644
--- a/engines/alcachofa/module.mk
+++ b/engines/alcachofa/module.mk
@@ -6,7 +6,8 @@ MODULE_OBJS = \
 	common.o \
 	console.o \
 	game.o \
-	game-movie-adventure.o \
+	game-movie-adventure-original.o \
+	game-movie-adventure-special.o \
 	game-objects.o \
 	general-objects.o \
 	global-ui.o \
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index c9c8372506a..2bd251f81ff 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -493,18 +493,9 @@ void Room::debugPrint(bool withObjects) const {
 	}
 }
 
-static constexpr const char *kMapFiles[] = {
-	"MAPAS/MAPA5.EMC",
-	"MAPAS/MAPA4.EMC",
-	"MAPAS/MAPA3.EMC",
-	"MAPAS/MAPA2.EMC",
-	"MAPAS/MAPA1.EMC",
-	"MAPAS/GLOBAL.EMC",
-	nullptr
-};
-
 World::World() {
-	for (auto *itMapFile = kMapFiles; *itMapFile != nullptr; itMapFile++) {
+	const char *const *mapFiles = g_engine->game().getMapFiles();
+	for (auto *itMapFile = mapFiles; *itMapFile != nullptr; itMapFile++) {
 		if (loadWorldFile(*itMapFile))
 			_loadedMapCount++;
 	}


Commit: 48e68a9a715dccb6aa91e1980bcfedb8eb4024de
    https://github.com/scummvm/scummvm/commit/48e68a9a715dccb6aa91e1980bcfedb8eb4024de
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:25+01:00

Commit Message:
ALCACHOFA: Add support for V1 world files

Changed paths:
    engines/alcachofa/common.h
    engines/alcachofa/game-movie-adventure-original.cpp
    engines/alcachofa/game-movie-adventure-special.cpp
    engines/alcachofa/game.h
    engines/alcachofa/rooms.cpp
    engines/alcachofa/rooms.h


diff --git a/engines/alcachofa/common.h b/engines/alcachofa/common.h
index 8cb8adad5b3..a274df1c0d9 100644
--- a/engines/alcachofa/common.h
+++ b/engines/alcachofa/common.h
@@ -166,6 +166,40 @@ inline void syncEnum(Common::Serializer &serializer, T &enumValue) {
 	enumValue = static_cast<T>(intValue);
 }
 
+/**
+ * @brief References a game file either as path or as embedded byte range
+ *
+ * In V1 all files (except videos) are stored within the EMC file. Some of
+ * those are within an embedded archive, most animations however are stored
+ * at their graphics and have their original filenames. We reference them
+ * by their byte range.
+ *
+ * V2/V3 store files outside with normal paths to use
+ */
+struct GameFileReference {
+	Common::String _path;
+	uint32
+		_fileIndex = UINT32_MAX,
+		_position = UINT32_MAX,
+		_size = 0;
+
+	GameFileReference() {}
+
+	GameFileReference(const Common::String &path)
+		: _path(path) {}
+
+	// in this case, path is only for debugging purposes
+	GameFileReference(const Common::String &path, uint32 fileIndex, int64 position, uint32 size)
+		: _path(path)
+		, _fileIndex(fileIndex)
+		, _position(position)
+		, _size(size) {}
+
+	inline bool isValid() const {
+		return !_path.empty() || _position >= 0;
+	}
+};
+
 }
 
 #endif // ALCACHOFA_COMMON_H
diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index a9e404bcd4b..b51490f38e7 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -53,6 +53,11 @@ public:
 	const char *const *getMapFiles() {
 		return kMapFiles;
 	}
+
+	GameFileReference getScriptFileRef() {
+		// V1 embeds the script into global.emc, it is overridden during world load
+		return {};
+	}
 };
 
 Game *Game::createForMovieAdventureOriginal() {
diff --git a/engines/alcachofa/game-movie-adventure-special.cpp b/engines/alcachofa/game-movie-adventure-special.cpp
index 215ab5a6bec..51fb99d3d52 100644
--- a/engines/alcachofa/game-movie-adventure-special.cpp
+++ b/engines/alcachofa/game-movie-adventure-special.cpp
@@ -208,6 +208,10 @@ public:
 		return kMapFiles;
 	}
 
+	GameFileReference getScriptFileRef() {
+		return { "Script/SCRIPT.COD" };
+	}
+
 	Span<const ScriptOp> getScriptOpMap() override {
 		return { kScriptOpMap, ARRAYSIZE(kScriptOpMap) };
 	}
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index 0207edd36b6..fa64a1dea70 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -52,6 +52,7 @@ public:
 	virtual void onLoadedGameFiles();
 	virtual Common::Point getResolution() = 0;
 	virtual const char *const *getMapFiles() = 0; ///< Returns a nullptr-terminated list
+	virtual GameFileReference getScriptFileRef() = 0;
 	virtual Common::Span<const ScriptOp> getScriptOpMap() = 0;
 	virtual Common::Span<const ScriptKernelTask> getScriptKernelTaskMap() = 0;
 	virtual void updateScriptVariables() = 0;
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 2bd251f81ff..f4090100bab 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -26,6 +26,7 @@
 #include "alcachofa/menu.h"
 
 #include "common/file.h"
+#include "common/substream.h"
 
 using namespace Common;
 
@@ -494,13 +495,19 @@ void Room::debugPrint(bool withObjects) const {
 }
 
 World::World() {
+	_scriptFileRef = g_engine->game().getScriptFileRef();
+
+	auto loadWorldFile =
+		g_engine->isV1() ? &World::loadWorldFileV1
+		: &World::loadWorldFileV3;
 	const char *const *mapFiles = g_engine->game().getMapFiles();
 	for (auto *itMapFile = mapFiles; *itMapFile != nullptr; itMapFile++) {
-		if (loadWorldFile(*itMapFile))
+		if ((*this.*loadWorldFile)(*itMapFile))
 			_loadedMapCount++;
 	}
 	loadLocalizedNames();
 	loadDialogLines();
+	_isLoading = false;
 
 	_globalRoom = getRoomByName("GLOBAL");
 	if (_globalRoom == nullptr)
@@ -608,7 +615,8 @@ void World::toggleObject(MainCharacterKind character, const char *objName, bool
 const Common::String &World::getGlobalAnimationName(GlobalAnimationKind kind) const {
 	int kindI = (int)kind;
 	assert(kindI >= 0 && kindI < (int)GlobalAnimationKind::Count);
-	return _globalAnimationNames[kindI];
+	error("You broke this, remember?");
+	//return _globalAnimationNames[kindI];
 }
 
 const char *World::getLocalizedName(const String &name) const {
@@ -624,7 +632,7 @@ const char *World::getDialogLine(int32 dialogId) const {
 	return _dialogLines[dialogId];
 }
 
-static Room *readRoom(World *world, SeekableReadStream &stream) {
+static Room *readRoomV3(World *world, SeekableReadStream &stream) {
 	const auto type = readVarString(stream);
 	if (type == Room::kClassName)
 		return new Room(world, stream);
@@ -642,45 +650,98 @@ static Room *readRoom(World *world, SeekableReadStream &stream) {
 	}
 }
 
-bool World::loadWorldFile(const char *path) {
-	Common::File file;
+/* World files start with a self-description of the data format (after 1-2 offsets)
+ * We ignore the self-description and just read the actual data
+ * The first offset points to the room room data
+ * (Only V1) The second offset points to the script file
+ */
+
+bool World::loadWorldFileV3(const char *path) {
+	File file;
 	if (!file.open(path)) {
-		// this is not necessarily an error, apparently the demos just have less
-		// chapter files. Being a demo is then also stored in some script vars
+		// this is not necessarily an error, the demos just have less chapter files.
+		// Being a demo is then also stored in some script vars
 		warning("Could not open world file %s\n", path);
 		return false;
 	}
 
-	// the first chunk seems to be debug symbols and/or info about the file structure
-	// it is ignored in the published game.
-	auto startOffset = file.readUint32LE();
+	uint32 startOffset = file.readUint32LE();
 	file.seek(startOffset, SEEK_SET);
-	skipVarString(file); // some more unused strings related to development files?
-	skipVarString(file);
-	skipVarString(file);
-	skipVarString(file);
-	skipVarString(file);
-	skipVarString(file);
+	skipVarString(file); // always "CMundo"
+	skipVarString(file); // name of the CMundo object
+	skipVarString(file); // path to sound files
+	skipVarString(file); // path to animation files
+	skipVarString(file); // path to background files
+	skipVarString(file); // path to masks (static object animations) files
 
 	_initScriptName = readVarString(file);
 	skipVarString(file); // would be _updateScriptName, but it is never called
 	for (int i = 0; i < (int)GlobalAnimationKind::Count; i++)
-		_globalAnimationNames[i] = readVarString(file);
+		_globalAnimations[i] = readFileRef(file);
+
+	readRooms(readRoomV3, file);
+
+	return true;
+}
+
+static void readEmbeddedArchive(SharedPtr<File> file);
+
+bool World::loadWorldFileV1(const char *path) {
+	auto file = SharedPtr<File>(new File());
+	if (!file->open(path)) {
+		// this is not necessarily an error, the demos just have less chapter files.
+		// Being a demo is then also stored in some script vars
+		warning("Could not open world file %s\n", path);
+		return false;
+	}
+
+	uint32 startOffset = file->readUint32LE();
+	uint32 scriptOffset = file->readUint32LE();
+	file->seek(scriptOffset, SEEK_SET);
+	if (file->readByte() == 1)
+		_scriptFileRef = { "SCRIPT", _files.size(), file->pos(), (uint32)(file->size() - file->pos())};
+
+	file->seek(startOffset, SEEK_SET);
+	skipVarString(*file); // always "CMundo"
+	skipVarString(*file); // name of the CMundo object
+	skipVarString(*file); // *second* name of the CMundo object
+	file->readByte(); // would be "isFinalFile", but always one for released games
+	readEmbeddedArchive(file);
+	_initScriptName = readVarString(*file);
+	skipVarString(*file); // _updateScriptName
+
+	const auto readGlobalAnim = [&] (
+		GlobalAnimationKind kind1,
+		GlobalAnimationKind kind2 = GlobalAnimationKind::Count) {
+		auto fileRef = readFileRef(*file);
+		_globalAnimations[(int)kind1] = fileRef;
+		if (kind2 != GlobalAnimationKind::Count)
+			_globalAnimations[(int)kind2] = fileRef;
+	};
+	readGlobalAnim(GlobalAnimationKind::GeneralFont, GlobalAnimationKind::DialogFont);
+	readGlobalAnim(GlobalAnimationKind::Cursor);
+	readGlobalAnim(GlobalAnimationKind::MortadeloIcon, GlobalAnimationKind::MortadeloDisabledIcon);
+	readGlobalAnim(GlobalAnimationKind::FilemonIcon, GlobalAnimationKind::FilemonDisabledIcon);
+	readGlobalAnim(GlobalAnimationKind::InventoryIcon, GlobalAnimationKind::InventoryDisabledIcon);
+
+	readRooms(readRoomV3, *file);
+	_files.emplace_back(move(file));
+	return true;
+}
 
+void World::readRooms(World::ReadRoomFunc readRoom, File &file) {
 	uint32 roomEnd = file.readUint32LE();
 	while (roomEnd > 0) {
 		Room *room = readRoom(this, file);
 		if (room != nullptr)
 			_rooms.push_back(room);
 		if (file.pos() < roomEnd) {
-			g_engine->game().notEnoughRoomDataRead(path, file.pos(), roomEnd);
+			g_engine->game().notEnoughRoomDataRead(file.getName(), file.pos(), roomEnd);
 			file.seek(roomEnd, SEEK_SET);
 		} else if (file.pos() > roomEnd) // this surely is not recoverable
-			error("Read past the room data for world %s", path);
+			error("Read past the room data for world %s", file.getName());
 		roomEnd = file.readUint32LE();
 	}
-
-	return true;
 }
 
 /**
@@ -790,4 +851,128 @@ void World::syncGame(Serializer &s) {
 		room->syncGame(s);
 }
 
+GameFileReference World::readFileRef(SeekableReadStream &stream) const {
+	assert(_isLoading);
+	auto name = readVarString(stream);
+	if (g_engine->isV1()) {
+		uint32 size = stream.readUint32LE();
+		stream.skip(size);
+		return { name, (uint32)_files.size(), (uint32)stream.pos(), size };
+	} else
+		return { name };
+}
+
+ScopedPtr<SeekableReadStream> World::openFileRef(const GameFileReference &ref) const {
+	assert(!_isLoading);
+	if (!ref.isValid())
+		return nullptr;
+	else if (ref._fileIndex != UINT32_MAX) {
+		assert(ref._fileIndex < _files.size());
+		auto &file = _files[ref._fileIndex];
+		if (!file->seek(ref._position, SEEK_SET))
+			error("Could not seek to inline file %s at %u", ref._path.c_str(), ref._position);
+		return ScopedPtr<SeekableReadStream>(
+			new SeekableSubReadStream(file.get(), ref._position, ref._position + ref._size, DisposeAfterUse::NO));
+	} else {
+		ScopedPtr<File> file(new File());
+		if (!file->open(Path(ref._path)))
+			return nullptr;
+		return file;
+	}
+}
+
+class EmbeddedArchive;
+class EmbeddedArchiveMember : public GenericArchiveMember {
+	friend class EmbeddedArchive;
+	uint32 _offset;
+	uint32 _end;
+
+	EmbeddedArchiveMember(const String &pathStr, const EmbeddedArchive &parent, uint32 offset, uint32 end);
+};
+
+class EmbeddedArchive : public Archive {
+public:
+	EmbeddedArchive(SharedPtr<File> file) : _file(move(file)) {
+		// the stored filenames have their original full paths,
+		// e.g. c:\myf2000\textos\dialogos.txt
+		// but the filenames alone do not clash so we flatten everything
+
+		skipVarString(*file);
+		uint32 totalSize = _file->readUint32LE();
+		int64 endPosition = _file->pos() + totalSize;
+		_file->skip(2);
+		uint32 fileCount = _file->readUint32LE();
+		_members.reserve((uint)fileCount);
+
+		for (uint32 i = 0; i < fileCount; i++) {
+			auto fullPath = readVarString(*_file);
+			auto extension = readVarString(*_file);
+			uint32 fileSize = _file->readUint32LE();
+			uint32 fileOffset = (uint32)_file->pos();
+			_file->skip(fileSize);
+
+			uint32 lastSep = fullPath.findLastOf('\\');
+			if (lastSep == String::npos)
+				lastSep = 0;
+			auto fileName = String::format("%s.%s", fullPath.c_str() + lastSep, extension.c_str());
+			_members.emplace_back(
+				new EmbeddedArchiveMember(fileName, *this, fileOffset, fileOffset + fileSize));
+		}
+
+		if (_file->pos() > endPosition)
+			error("Read past the specified archive total size in %s", _file->getName());
+		_file->seek(endPosition, SEEK_SET);
+		scumm_assert(!_file->err());
+	}
+
+	bool hasFile(const Path &path) const override {
+		return getMember(path) != nullptr;
+	}
+
+	int listMembers(ArchiveMemberList &list) const override {
+		for (const auto &member : _members)
+			list.emplace_back(member);
+		return (int)_members.size();
+	}
+
+	const ArchiveMemberPtr getMember(const Path &path) const {
+		return getMemberInternal(path);
+	}
+
+	SeekableReadStream *createReadStreamForMember(const Path &path) const {
+		auto member = getMemberInternal(path);
+		if (member == nullptr)
+			return nullptr;
+		if (!_file->seek(member->_offset, SEEK_SET))
+			error("Could not seek to embedded file: %s at %u", path.toString().c_str(), member->_offset);
+		return new SeekableSubReadStream(_file.get(), member->_offset, member->_end, DisposeAfterUse::NO);
+	}
+
+private:
+	const SharedPtr<EmbeddedArchiveMember> getMemberInternal(const Path &path) const {
+		for (const auto &member : _members) {
+			if (member->getPathInArchive() == path)
+				return member;
+		}
+		return nullptr;
+	}
+
+	SharedPtr<File> _file;
+	Array<SharedPtr<EmbeddedArchiveMember>> _members;
+};
+
+EmbeddedArchiveMember::EmbeddedArchiveMember(
+	const String &pathStr,
+	const EmbeddedArchive &parent,
+	uint32 offset,
+	uint32 end)
+	: GenericArchiveMember(pathStr, parent)
+	, _offset(offset)
+	, _end(end) {}
+
+static void readEmbeddedArchive(SharedPtr<File> file) {
+	auto archive = new EmbeddedArchive(file);
+	SearchMan.add(file->getName(), archive);
+}
+
 }
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index 2ca7ee43cb1..df73b4f47fc 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -164,6 +164,7 @@ public:
 	inline Inventory &inventory() const { assert(_inventory != nullptr); return *_inventory; }
 	inline MainCharacter &filemon() const { assert(_filemon != nullptr); return *_filemon; }
 	inline MainCharacter &mortadelo() const { assert(_mortadelo != nullptr);  return *_mortadelo; }
+	inline GameFileReference scriptFileRef() const { return _scriptFileRef; }
 	inline const Common::String &initScriptName() const { return _initScriptName; }
 	inline uint8 loadedMapCount() const { return _loadedMapCount; }
 
@@ -185,8 +186,14 @@ public:
 	void toggleObject(MainCharacterKind character, const char *objName, bool isEnabled);
 	void syncGame(Common::Serializer &s);
 
+	GameFileReference readFileRef(Common::SeekableReadStream &stream) const;
+	Common::ScopedPtr<Common::SeekableReadStream> openFileRef(const GameFileReference &ref) const;
+
 private:
-	bool loadWorldFile(const char *path);
+	using ReadRoomFunc = Room *(*)(World *, Common::SeekableReadStream &);
+	bool loadWorldFileV3(const char *path);
+	bool loadWorldFileV1(const char *path);
+	void readRooms(ReadRoomFunc readRoom, Common::File &file);
 	void loadLocalizedNames();
 	void loadDialogLines();
 
@@ -195,9 +202,11 @@ private:
 		bool operator()(const char *a, const char *b) const { return strcmp(a, b) == 0; }
 	};
 
+	Common::Array<Common::SharedPtr<Common::File>> _files; ///< only used in V1 to read embedded files
 	Common::Array<Room *> _rooms;
-	Common::String _globalAnimationNames[(int)GlobalAnimationKind::Count];
+	GameFileReference _globalAnimations[(int)GlobalAnimationKind::Count];
 	Common::String _initScriptName;
+	GameFileReference _scriptFileRef;
 	Room *_globalRoom;
 	Inventory *_inventory;
 	MainCharacter *_filemon, *_mortadelo;
@@ -207,6 +216,7 @@ private:
 		StringEqualTo> _localizedNames;
 	Common::Array<const char *> _dialogLines;
 	Common::Array<char> _namesChunk, _dialogChunk; ///< holds the memory for localizedNames / dialogLines
+	bool _isLoading = true;
 };
 
 }


Commit: b6ad37d4b174593b82e4d28d63126e275d922511
    https://github.com/scummvm/scummvm/commit/b6ad37d4b174593b82e4d28d63126e275d922511
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:25+01:00

Commit Message:
ALCACHOFA: Use SeekableReadStream for object reads

Changed paths:
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/general-objects.cpp
    engines/alcachofa/objects.h
    engines/alcachofa/rooms.cpp
    engines/alcachofa/ui-objects.cpp


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 8bfd05ca866..8883704b68e 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -32,7 +32,7 @@ namespace Alcachofa {
 
 const char *Item::typeName() const { return "Item"; }
 
-Item::Item(Room *room, ReadStream &stream)
+Item::Item(Room *room, SeekableReadStream &stream)
 	: GraphicObject(room, stream) {
 	stream.readByte(); // unused and ignored byte
 }
@@ -70,7 +70,7 @@ void Item::trigger() {
 		player.triggerObject(heldItem, name().c_str());
 }
 
-ITriggerableObject::ITriggerableObject(ReadStream &stream)
+ITriggerableObject::ITriggerableObject(SeekableReadStream &stream)
 	: _interactionPoint(Shape(stream).firstPoint())
 	, _interactionDirection((Direction)stream.readSint32LE()) {}
 
@@ -86,7 +86,7 @@ void ITriggerableObject::onClick() {
 
 const char *InteractableObject::typeName() const { return "InteractableObject"; }
 
-InteractableObject::InteractableObject(Room *room, ReadStream &stream)
+InteractableObject::InteractableObject(Room *room, SeekableReadStream &stream)
 	: PhysicalObject(room, stream)
 	, ITriggerableObject(stream)
 	, _relatedObject(readVarString(stream)) {
@@ -120,7 +120,7 @@ void InteractableObject::toggle(bool isEnabled) {
 
 const char *Door::typeName() const { return "Door"; }
 
-Door::Door(Room *room, ReadStream &stream)
+Door::Door(Room *room, SeekableReadStream &stream)
 	: InteractableObject(room, stream)
 	, _targetRoom(readVarString(stream))
 	, _targetObject(readVarString(stream))
@@ -162,7 +162,7 @@ void Door::trigger(const char *_) {
 
 const char *Character::typeName() const { return "Character"; }
 
-Character::Character(Room *room, ReadStream &stream)
+Character::Character(Room *room, SeekableReadStream &stream)
 	: ShapeObject(room, stream)
 	, ITriggerableObject(stream)
 	, _graphicNormal(stream)
@@ -498,7 +498,7 @@ Task *Character::lerpLodBias(Process &process, float targetLodBias, int32 durati
 
 const char *WalkingCharacter::typeName() const { return "WalkingCharacter"; }
 
-WalkingCharacter::WalkingCharacter(Room *room, ReadStream &stream)
+WalkingCharacter::WalkingCharacter(Room *room, SeekableReadStream &stream)
 	: Character(room, stream) {
 	for (int32 i = 0; i < kDirectionCount; i++) {
 		auto fileName = readVarString(stream);
@@ -803,7 +803,7 @@ Task *WalkingCharacter::waitForArrival(Process &process) {
 
 const char *MainCharacter::typeName() const { return "MainCharacter"; }
 
-MainCharacter::MainCharacter(Room *room, ReadStream &stream)
+MainCharacter::MainCharacter(Room *room, SeekableReadStream &stream)
 	: WalkingCharacter(room, stream)
 	, _semaphore(name().firstChar() == 'M' ? "mortadelo" : "filemon") {
 	stream.readByte(); // unused byte
@@ -1155,7 +1155,7 @@ Background::Background(Room *room, const String &animationFileName, int16 scale)
 
 const char *FloorColor::typeName() const { return "FloorColor"; }
 
-FloorColor::FloorColor(Room *room, ReadStream &stream)
+FloorColor::FloorColor(Room *room, SeekableReadStream &stream)
 	: ObjectBase(room, stream)
 	, _shape(stream) {}
 
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index 8ef4fe55f02..91252e99794 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -41,7 +41,7 @@ ObjectBase::ObjectBase(Room *room, const char *name)
 	assert(room != nullptr);
 }
 
-ObjectBase::ObjectBase(Room *room, ReadStream &stream)
+ObjectBase::ObjectBase(Room *room, SeekableReadStream &stream)
 	: _room(room) {
 	assert(room != nullptr);
 	_name = readVarString(stream);
@@ -76,14 +76,14 @@ Shape *ObjectBase::shape() {
 
 const char *PointObject::typeName() const { return "PointObject"; }
 
-PointObject::PointObject(Room *room, ReadStream &stream)
+PointObject::PointObject(Room *room, SeekableReadStream &stream)
 	: ObjectBase(room, stream) {
 	_pos = Shape(stream).firstPoint();
 }
 
 const char *GraphicObject::typeName() const { return "GraphicObject"; }
 
-GraphicObject::GraphicObject(Room *room, ReadStream &stream)
+GraphicObject::GraphicObject(Room *room, SeekableReadStream &stream)
 	: ObjectBase(room, stream)
 	, _graphic(stream)
 	, _type((GraphicObjectType)stream.readSint32LE())
@@ -203,7 +203,7 @@ Task *GraphicObject::animate(Process &process) {
 
 const char *SpecialEffectObject::typeName() const { return "SpecialEffectObject"; }
 
-SpecialEffectObject::SpecialEffectObject(Room *room, ReadStream &stream)
+SpecialEffectObject::SpecialEffectObject(Room *room, SeekableReadStream &stream)
 	: GraphicObject(room, stream) {
 	_topLeft = Shape(stream).firstPoint();
 	_bottomRight = Shape(stream).firstPoint();
@@ -231,7 +231,7 @@ void SpecialEffectObject::draw() {
 
 const char *ShapeObject::typeName() const { return "ShapeObject"; }
 
-ShapeObject::ShapeObject(Room *room, ReadStream &stream)
+ShapeObject::ShapeObject(Room *room, SeekableReadStream &stream)
 	: ObjectBase(room, stream)
 	, _shape(stream)
 	, _cursorType((CursorType)stream.readSint32LE()) {}
@@ -302,7 +302,7 @@ void ShapeObject::updateSelection() {
 
 const char *PhysicalObject::typeName() const { return "PhysicalObject"; }
 
-PhysicalObject::PhysicalObject(Room *room, ReadStream &stream)
+PhysicalObject::PhysicalObject(Room *room, SeekableReadStream &stream)
 	: ShapeObject(room, stream) {
 	_order = stream.readSByte();
 }
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 56092156584..1bc625caa14 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -37,7 +37,7 @@ class ObjectBase {
 public:
 	static constexpr const char *kClassName = "CObjetoBase";
 	ObjectBase(Room *room, const char *name);
-	ObjectBase(Room *room, Common::ReadStream &stream);
+	ObjectBase(Room *room, Common::SeekableReadStream &stream);
 	virtual ~ObjectBase() {}
 
 	inline const Common::String &name() const { return _name; }
@@ -65,7 +65,7 @@ private:
 class PointObject : public ObjectBase {
 public:
 	static constexpr const char *kClassName = "CObjetoPunto";
-	PointObject(Room *room, Common::ReadStream &stream);
+	PointObject(Room *room, Common::SeekableReadStream &stream);
 
 	inline Common::Point &position() { return _pos; }
 	inline Common::Point position() const { return _pos; }
@@ -85,7 +85,7 @@ enum class GraphicObjectType : byte
 class GraphicObject : public ObjectBase {
 public:
 	static constexpr const char *kClassName = "CObjetoGrafico";
-	GraphicObject(Room *room, Common::ReadStream &stream);
+	GraphicObject(Room *room, Common::SeekableReadStream &stream);
 	~GraphicObject() override {}
 
 	void draw() override;
@@ -109,7 +109,7 @@ protected:
 class SpecialEffectObject final : public GraphicObject {
 public:
 	static constexpr const char *kClassName = "CObjetoGraficoMuare";
-	SpecialEffectObject(Room *room, Common::ReadStream &stream);
+	SpecialEffectObject(Room *room, Common::SeekableReadStream &stream);
 
 	void draw() override;
 	const char *typeName() const override;
@@ -122,7 +122,7 @@ private:
 
 class ShapeObject : public ObjectBase {
 public:
-	ShapeObject(Room *room, Common::ReadStream &stream);
+	ShapeObject(Room *room, Common::SeekableReadStream &stream);
 	~ShapeObject() override {}
 
 	inline int8 order() const { return _order; }
@@ -154,14 +154,14 @@ private:
 
 class PhysicalObject : public ShapeObject {
 public:
-	PhysicalObject(Room *room, Common::ReadStream &stream);
+	PhysicalObject(Room *room, Common::SeekableReadStream &stream);
 	const char *typeName() const override;
 };
 
 class MenuButton : public PhysicalObject {
 public:
 	static constexpr const char *kClassName = "CBotonMenu";
-	MenuButton(Room *room, Common::ReadStream &stream);
+	MenuButton(Room *room, Common::SeekableReadStream &stream);
 	~MenuButton() override {}
 
 	inline int32 actionId() const { return _actionId; }
@@ -196,7 +196,7 @@ private:
 class InternetMenuButton final : public MenuButton {
 public:
 	static constexpr const char *kClassName = "CBotonMenuInternet";
-	InternetMenuButton(Room *room, Common::ReadStream &stream);
+	InternetMenuButton(Room *room, Common::SeekableReadStream &stream);
 
 	const char *typeName() const override;
 };
@@ -204,7 +204,7 @@ public:
 class OptionsMenuButton final : public MenuButton {
 public:
 	static constexpr const char *kClassName = "CBotonMenuOpciones";
-	OptionsMenuButton(Room *room, Common::ReadStream &stream);
+	OptionsMenuButton(Room *room, Common::SeekableReadStream &stream);
 
 	void update() override;
 	void trigger() override;
@@ -214,7 +214,7 @@ public:
 class MainMenuButton final : public MenuButton {
 public:
 	static constexpr const char *kClassName = "CBotonMenuPrincipal";
-	MainMenuButton(Room *room, Common::ReadStream &stream);
+	MainMenuButton(Room *room, Common::SeekableReadStream &stream);
 
 	void update() override;
 	void trigger() override;
@@ -224,7 +224,7 @@ public:
 class PushButton final : public PhysicalObject {
 public:
 	static constexpr const char *kClassName = "CPushButton";
-	PushButton(Room *room, Common::ReadStream &stream);
+	PushButton(Room *room, Common::SeekableReadStream &stream);
 
 	const char *typeName() const override;
 
@@ -237,7 +237,7 @@ private:
 class EditBox final : public PhysicalObject {
 public:
 	static constexpr const char *kClassName = "CEditBox";
-	EditBox(Room *room, Common::ReadStream &stream);
+	EditBox(Room *room, Common::SeekableReadStream &stream);
 
 	const char *typeName() const override;
 
@@ -253,7 +253,7 @@ private:
 class CheckBox : public PhysicalObject {
 public:
 	static constexpr const char *kClassName = "CCheckBox";
-	CheckBox(Room *room, Common::ReadStream &stream);
+	CheckBox(Room *room, Common::SeekableReadStream &stream);
 	~CheckBox() override {}
 
 	inline bool &isChecked() { return _isChecked; }
@@ -284,7 +284,7 @@ private:
 class SlideButton final : public ObjectBase {
 public:
 	static constexpr const char *kClassName = "CSlideButton";
-	SlideButton(Room *room, Common::ReadStream &stream);
+	SlideButton(Room *room, Common::SeekableReadStream &stream);
 	~SlideButton() override {}
 
 	inline float &value() { return _value; }
@@ -310,7 +310,7 @@ private:
 class CheckBoxAutoAdjustNoise final : public CheckBox {
 public:
 	static constexpr const char *kClassName = "CCheckBoxAutoAjustarRuido";
-	CheckBoxAutoAdjustNoise(Room *room, Common::ReadStream &stream);
+	CheckBoxAutoAdjustNoise(Room *room, Common::SeekableReadStream &stream);
 
 	const char *typeName() const override;
 };
@@ -318,7 +318,7 @@ public:
 class IRCWindow final : public ObjectBase {
 public:
 	static constexpr const char *kClassName = "CVentanaIRC";
-	IRCWindow(Room *room, Common::ReadStream &stream);
+	IRCWindow(Room *room, Common::SeekableReadStream &stream);
 
 	const char *typeName() const override;
 
@@ -329,7 +329,7 @@ private:
 class MessageBox final : public ObjectBase {
 public:
 	static constexpr const char *kClassName = "CMessageBox";
-	MessageBox(Room *room, Common::ReadStream &stream);
+	MessageBox(Room *room, Common::SeekableReadStream &stream);
 	~MessageBox() override {}
 
 	const char *typeName() const override;
@@ -346,7 +346,7 @@ private:
 class VoiceMeter final : public GraphicObject {
 public:
 	static constexpr const char *kClassName = "CVuMeter";
-	VoiceMeter(Room *room, Common::ReadStream &stream);
+	VoiceMeter(Room *room, Common::SeekableReadStream &stream);
 
 	const char *typeName() const override;
 };
@@ -354,7 +354,7 @@ public:
 class Item : public GraphicObject { //-V690
 public:
 	static constexpr const char *kClassName = "CObjetoInventario";
-	Item(Room *room, Common::ReadStream &stream);
+	Item(Room *room, Common::SeekableReadStream &stream);
 	Item(const Item &other);
 	// no copy-assign operator as it is non-sensical, the copy ctor is a special case for item-handling
 
@@ -365,7 +365,7 @@ public:
 
 class ITriggerableObject {
 public:
-	ITriggerableObject(Common::ReadStream &stream);
+	ITriggerableObject(Common::SeekableReadStream &stream);
 	virtual ~ITriggerableObject() {}
 
 	inline Direction interactionDirection() const { return _interactionDirection; }
@@ -383,7 +383,7 @@ protected:
 class InteractableObject : public PhysicalObject, public ITriggerableObject {
 public:
 	static constexpr const char *kClassName = "CObjetoTipico";
-	InteractableObject(Room *room, Common::ReadStream &stream);
+	InteractableObject(Room *room, Common::SeekableReadStream &stream);
 	~InteractableObject() override {}
 
 	void drawDebug() override;
@@ -399,7 +399,7 @@ private:
 class Door final : public InteractableObject {
 public:
 	static constexpr const char *kClassName = "CPuerta";
-	Door(Room *room, Common::ReadStream &stream);
+	Door(Room *room, Common::SeekableReadStream &stream);
 
 	inline const Common::String &targetRoom() const { return _targetRoom; }
 	inline const Common::String &targetObject() const { return _targetObject; }
@@ -419,7 +419,7 @@ private:
 class Character : public ShapeObject, public ITriggerableObject {
 public:
 	static constexpr const char *kClassName = "CPersonaje";
-	Character(Room *room, Common::ReadStream &stream);
+	Character(Room *room, Common::SeekableReadStream &stream);
 	~Character() override {}
 
 	void update() override;
@@ -461,7 +461,7 @@ protected:
 class WalkingCharacter : public Character {
 public:
 	static constexpr const char *kClassName = "CPersonajeAnda";
-	WalkingCharacter(Room *room, Common::ReadStream &stream);
+	WalkingCharacter(Room *room, Common::SeekableReadStream &stream);
 	~WalkingCharacter() override {}
 
 	inline bool isWalking() const { return _isWalking; }
@@ -528,7 +528,7 @@ struct DialogMenuLine {
 class MainCharacter final : public WalkingCharacter {
 public:
 	static constexpr const char *kClassName = "CPersonajePrincipal";
-	MainCharacter(Room *room, Common::ReadStream &stream);
+	MainCharacter(Room *room, Common::SeekableReadStream &stream);
 	~MainCharacter() override;
 
 	inline MainCharacterKind kind() const { return _kind; }
@@ -588,7 +588,7 @@ public:
 class FloorColor final : public ObjectBase {
 public:
 	static constexpr const char *kClassName = "CSueloColor";
-	FloorColor(Room *room, Common::ReadStream &stream);
+	FloorColor(Room *room, Common::SeekableReadStream &stream);
 	~FloorColor() override {}
 
 	void update() override;
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index f4090100bab..96c00505166 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -34,7 +34,7 @@ namespace Alcachofa {
 
 Room::Room(World *world, SeekableReadStream &stream) : Room(world, stream, false) {}
 
-static ObjectBase *readRoomObject(Room *room, const String &type, ReadStream &stream) {
+static ObjectBase *readRoomObject(Room *room, const String &type, SeekableReadStream &stream) {
 	if (type == ObjectBase::kClassName)
 		return new ObjectBase(room, stream);
 	else if (type == PointObject::kClassName)
diff --git a/engines/alcachofa/ui-objects.cpp b/engines/alcachofa/ui-objects.cpp
index e0e17424048..c3745935556 100644
--- a/engines/alcachofa/ui-objects.cpp
+++ b/engines/alcachofa/ui-objects.cpp
@@ -32,7 +32,7 @@ namespace Alcachofa {
 
 const char *MenuButton::typeName() const { return "MenuButton"; }
 
-MenuButton::MenuButton(Room *room, ReadStream &stream)
+MenuButton::MenuButton(Room *room, SeekableReadStream &stream)
 	: PhysicalObject(room, stream)
 	, _actionId(stream.readSint32LE())
 	, _graphicNormal(stream)
@@ -105,12 +105,12 @@ void MenuButton::trigger() {
 
 const char *InternetMenuButton::typeName() const { return "InternetMenuButton"; }
 
-InternetMenuButton::InternetMenuButton(Room *room, ReadStream &stream)
+InternetMenuButton::InternetMenuButton(Room *room, SeekableReadStream &stream)
 	: MenuButton(room, stream) {}
 
 const char *OptionsMenuButton::typeName() const { return "OptionsMenuButton"; }
 
-OptionsMenuButton::OptionsMenuButton(Room *room, ReadStream &stream)
+OptionsMenuButton::OptionsMenuButton(Room *room, SeekableReadStream &stream)
 	: MenuButton(room, stream) {}
 
 void OptionsMenuButton::update() {
@@ -126,7 +126,7 @@ void OptionsMenuButton::trigger() {
 
 const char *MainMenuButton::typeName() const { return "MainMenuButton"; }
 
-MainMenuButton::MainMenuButton(Room *room, ReadStream &stream)
+MainMenuButton::MainMenuButton(Room *room, SeekableReadStream &stream)
 	: MenuButton(room, stream) {}
 
 void MainMenuButton::update() {
@@ -143,7 +143,7 @@ void MainMenuButton::trigger() {
 
 const char *PushButton::typeName() const { return "PushButton"; }
 
-PushButton::PushButton(Room *room, ReadStream &stream)
+PushButton::PushButton(Room *room, SeekableReadStream &stream)
 	: PhysicalObject(room, stream)
 	, _alwaysVisible(readBool(stream))
 	, _graphic1(stream)
@@ -152,7 +152,7 @@ PushButton::PushButton(Room *room, ReadStream &stream)
 
 const char *EditBox::typeName() const { return "EditBox"; }
 
-EditBox::EditBox(Room *room, ReadStream &stream)
+EditBox::EditBox(Room *room, SeekableReadStream &stream)
 	: PhysicalObject(room, stream)
 	, i1(stream.readSint32LE())
 	, p1(Shape(stream).firstPoint())
@@ -169,7 +169,7 @@ EditBox::EditBox(Room *room, ReadStream &stream)
 
 const char *CheckBox::typeName() const { return "CheckBox"; }
 
-CheckBox::CheckBox(Room *room, ReadStream &stream)
+CheckBox::CheckBox(Room *room, SeekableReadStream &stream)
 	: PhysicalObject(room, stream)
 	, _isChecked(readBool(stream))
 	, _graphicUnchecked(stream)
@@ -234,14 +234,14 @@ void CheckBox::trigger() {
 
 const char *CheckBoxAutoAdjustNoise::typeName() const { return "CheckBoxAutoAdjustNoise"; }
 
-CheckBoxAutoAdjustNoise::CheckBoxAutoAdjustNoise(Room *room, ReadStream &stream)
+CheckBoxAutoAdjustNoise::CheckBoxAutoAdjustNoise(Room *room, SeekableReadStream &stream)
 	: CheckBox(room, stream) {
 	stream.readByte(); // unused and ignored byte
 }
 
 const char *SlideButton::typeName() const { return "SlideButton"; }
 
-SlideButton::SlideButton(Room *room, ReadStream &stream)
+SlideButton::SlideButton(Room *room, SeekableReadStream &stream)
 	: ObjectBase(room, stream)
 	, _valueId(stream.readSint32LE())
 	, _minPos(Shape(stream).firstPoint())
@@ -313,14 +313,14 @@ bool SlideButton::isMouseOver() const {
 
 const char *IRCWindow::typeName() const { return "IRCWindow"; }
 
-IRCWindow::IRCWindow(Room *room, ReadStream &stream)
+IRCWindow::IRCWindow(Room *room, SeekableReadStream &stream)
 	: ObjectBase(room, stream)
 	, _p1(Shape(stream).firstPoint())
 	, _p2(Shape(stream).firstPoint()) {}
 
 const char *MessageBox::typeName() const { return "MessageBox"; }
 
-MessageBox::MessageBox(Room *room, ReadStream &stream)
+MessageBox::MessageBox(Room *room, SeekableReadStream &stream)
 	: ObjectBase(room, stream)
 	, _graph1(stream)
 	, _graph2(stream)
@@ -336,7 +336,7 @@ MessageBox::MessageBox(Room *room, ReadStream &stream)
 
 const char *VoiceMeter::typeName() const { return "VoiceMeter"; }
 
-VoiceMeter::VoiceMeter(Room *room, ReadStream &stream)
+VoiceMeter::VoiceMeter(Room *room, SeekableReadStream &stream)
 	: GraphicObject(room, stream) {
 	stream.readByte(); // unused and ignored byte
 }


Commit: 6f74da160b15a687c88f29da53e8a39f46f79fee
    https://github.com/scummvm/scummvm/commit/6f74da160b15a687c88f29da53e8a39f46f79fee
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:25+01:00

Commit Message:
ALCACHOFA: Add support for V1 rooms

Changed paths:
    engines/alcachofa/objects.h
    engines/alcachofa/rooms.cpp
    engines/alcachofa/rooms.h


diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 1bc625caa14..db974ef6808 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -563,7 +563,7 @@ protected:
 	void onArrived() override;
 
 private:
-	friend class Inventory;
+	friend class InventoryV3;
 	friend struct DialogMenuTask;
 	Item *getItemByName(const Common::String &name) const;
 	void drawInner();
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 96c00505166..2c5e3d3304a 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -32,7 +32,9 @@ using namespace Common;
 
 namespace Alcachofa {
 
-Room::Room(World *world, SeekableReadStream &stream) : Room(world, stream, false) {}
+Room::Room(World *world) : _world(world) {}
+
+RoomV3::RoomV3(World *world, SeekableReadStream &stream) : RoomV3(world, stream, false) {}
 
 static ObjectBase *readRoomObject(Room *room, const String &type, SeekableReadStream &stream) {
 	if (type == ObjectBase::kClassName)
@@ -85,9 +87,10 @@ static ObjectBase *readRoomObject(Room *room, const String &type, SeekableReadSt
 		return nullptr; // handled in Room::Room
 }
 
-Room::Room(World *world, SeekableReadStream &stream, bool hasUselessByte)
-	: _world(world) {
+RoomV3::RoomV3(World *world, SeekableReadStream &stream, bool hasUselessByte)
+	: Room(world) {
 	_name = readVarString(stream);
+	_backgroundName = _name;
 	_musicId = (int)stream.readByte();
 	_characterAlphaTint = stream.readByte();
 	auto backgroundScale = stream.readSint16LE();
@@ -99,6 +102,10 @@ Room::Room(World *world, SeekableReadStream &stream, bool hasUselessByte)
 	if (hasUselessByte)
 		stream.readByte();
 
+	readObjectsAndBackground(stream, backgroundScale);
+}
+
+void Room::readObjectsAndBackground(SeekableReadStream &stream, int16 backgroundScale) {
 	uint32 objectEnd = stream.readUint32LE();
 	while (objectEnd > 0) {
 		const auto type = readVarString(stream);
@@ -117,12 +124,32 @@ Room::Room(World *world, SeekableReadStream &stream, bool hasUselessByte)
 		objectEnd = stream.readUint32LE();
 	}
 	if (g_engine->game().doesRoomHaveBackground(this))
-		_objects.push_back(new Background(this, _name, backgroundScale));
+		_objects.push_back(new Background(this, _backgroundName, backgroundScale));
 
 	if (!_floors[0].empty())
 		_activeFloorI = 0;
 }
 
+RoomV1::RoomV1(World *world, SeekableReadStream &stream, bool readObjects)
+	: Room(world) {
+	_name = readVarString(stream);
+	_backgroundName = readVarString(stream);
+	_musicId = (int)stream.readByte();
+	_characterAlphaTint = stream.readByte();
+	skipVarString(stream);
+
+	if (readObjects)
+		readObjectsAndBackground(stream, kBaseScale);
+}
+
+RoomV1WithFloor::RoomV1WithFloor(World *world, SeekableReadStream &stream)
+	: RoomV1(world, stream, false) {
+	_floors[0] = PathFindingShape(stream);
+	_floors[1] = PathFindingShape(stream);
+
+	readObjectsAndBackground(stream, kBaseScale);
+}
+
 Room::~Room() {
 	for (auto *object : _objects)
 		delete object;
@@ -322,7 +349,7 @@ ShapeObject *Room::getSelectedObject(ShapeObject *best) const {
 }
 
 OptionsMenu::OptionsMenu(World *world, SeekableReadStream &stream)
-	: Room(world, stream, true) {}
+	: RoomV3(world, stream, true) {}
 
 bool OptionsMenu::updateInput() {
 	if (!Room::updateInput())
@@ -356,19 +383,19 @@ void OptionsMenu::clearLastSelectedObject() {
 }
 
 ConnectMenu::ConnectMenu(World *world, SeekableReadStream &stream)
-	: Room(world, stream, true) {}
+	: RoomV3(world, stream, true) {}
 
 ListenMenu::ListenMenu(World *world, SeekableReadStream &stream)
-	: Room(world, stream, true) {}
+	: RoomV3(world, stream, true) {}
 
-Inventory::Inventory(World *world, SeekableReadStream &stream)
-	: Room(world, stream, true) {}
+InventoryV3::InventoryV3(World *world, SeekableReadStream &stream)
+	: RoomV3(world, stream, true) {}
 
-Inventory::~Inventory() {
+InventoryV3::~InventoryV3() {
 	// No need to delete items, they are room objects and thus deleted in Room::~Room
 }
 
-bool Inventory::updateInput() {
+bool InventoryV3::updateInput() {
 	auto &player = g_engine->player();
 	auto &input = g_engine->input();
 	auto *hoveredItem = getHoveredItem();
@@ -411,7 +438,7 @@ bool Inventory::updateInput() {
 	return player.currentRoom() == this;
 }
 
-Item *Inventory::getHoveredItem() {
+Item *InventoryV3::getHoveredItem() {
 	auto mousePos = g_engine->input().mousePos2D();
 	for (auto item : _items) {
 		if (!item->isEnabled())
@@ -431,7 +458,7 @@ Item *Inventory::getHoveredItem() {
 	return nullptr;
 }
 
-void Inventory::initItems() {
+void InventoryV3::initItems() {
 	auto &mortadelo = world().mortadelo();
 	auto &filemon = world().filemon();
 	for (auto object : _objects) {
@@ -444,14 +471,14 @@ void Inventory::initItems() {
 	}
 }
 
-void Inventory::updateItemsByActiveCharacter() {
+void InventoryV3::updateItemsByActiveCharacter() {
 	auto *character = g_engine->player().activeCharacter();
 	assert(character != nullptr);
 	for (auto *item : _items)
 		item->toggle(character->hasItem(item->name()));
 }
 
-void Inventory::drawAsOverlay(int32 scrollY) {
+void InventoryV3::drawAsOverlay(int32 scrollY) {
 	for (auto object : _objects) {
 		auto graphic = object->graphic();
 		if (graphic == nullptr)
@@ -469,17 +496,23 @@ void Inventory::drawAsOverlay(int32 scrollY) {
 	}
 }
 
-void Inventory::open() {
+void InventoryV3::open() {
 	g_engine->camera().backup(1);
 	g_engine->player().changeRoom(name(), true);
 	updateItemsByActiveCharacter();
 }
 
-void Inventory::close() {
+void InventoryV3::close() {
 	g_engine->camera().restore(1);
 	g_engine->globalUI().startClosingInventory();
 }
 
+InventoryV1::InventoryV1(World *world, SeekableReadStream &stream)
+	: RoomV1(world, stream, false) {
+	stream.skip(1); // denoted as "sinusar" but unused
+	readObjectsAndBackground(stream, kBaseScale);
+}
+
 void Room::debugPrint(bool withObjects) const {
 	auto &console = g_engine->console();
 	console.debugPrintf("  %s\n", _name.c_str());
@@ -512,7 +545,7 @@ World::World() {
 	_globalRoom = getRoomByName("GLOBAL");
 	if (_globalRoom == nullptr)
 		error("Could not find GLOBAL room");
-	_inventory = dynamic_cast<Inventory *>(getRoomByName("INVENTARIO"));
+	_inventory = dynamic_cast<InventoryV3 *>(getRoomByName("INVENTARIO"));
 	if (_inventory == nullptr)
 		error("Could not find INVENTARIO");
 	_filemon = dynamic_cast<MainCharacter *>(_globalRoom->getObjectByName("FILEMON"));
@@ -635,15 +668,29 @@ const char *World::getDialogLine(int32 dialogId) const {
 static Room *readRoomV3(World *world, SeekableReadStream &stream) {
 	const auto type = readVarString(stream);
 	if (type == Room::kClassName)
-		return new Room(world, stream);
+		return new RoomV3(world, stream);
 	else if (type == OptionsMenu::kClassName)
 		return new OptionsMenu(world, stream);
 	else if (type == ConnectMenu::kClassName)
 		return new ConnectMenu(world, stream);
 	else if (type == ListenMenu::kClassName)
 		return new ListenMenu(world, stream);
-	else if (type == Inventory::kClassName)
-		return new Inventory(world, stream);
+	else if (type == InventoryV3::kClassName)
+		return new InventoryV3(world, stream);
+	else {
+		g_engine->game().unknownRoomType(type);
+		return nullptr;
+	}
+}
+
+static Room *readRoomV1(World *world, SeekableReadStream &stream) {
+	const auto type = readVarString(stream);
+	if (type == Room::kClassName)
+		return new RoomV1(world, stream);
+	else if (type == RoomV1WithFloor::kClassName)
+		return new RoomV1WithFloor(world, stream);
+	else if (type == InventoryV1::kClassName)
+		return new InventoryV1(world, stream);
 	else {
 		g_engine->game().unknownRoomType(type);
 		return nullptr;
@@ -724,7 +771,7 @@ bool World::loadWorldFileV1(const char *path) {
 	readGlobalAnim(GlobalAnimationKind::FilemonIcon, GlobalAnimationKind::FilemonDisabledIcon);
 	readGlobalAnim(GlobalAnimationKind::InventoryIcon, GlobalAnimationKind::InventoryDisabledIcon);
 
-	readRooms(readRoomV3, *file);
+	readRooms(readRoomV1, *file);
 	_files.emplace_back(move(file));
 	return true;
 }
@@ -742,6 +789,7 @@ void World::readRooms(World::ReadRoomFunc readRoom, File &file) {
 			error("Read past the room data for world %s", file.getName());
 		roomEnd = file.readUint32LE();
 	}
+	scumm_assert(!file.err());
 }
 
 /**
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index df73b4f47fc..dab0da4641b 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -31,7 +31,6 @@ class World;
 class Room {
 public:
 	static constexpr const char *kClassName = "CHabitacion";
-	Room(World *world, Common::SeekableReadStream &stream);
 	virtual ~Room();
 
 	inline World &world() { return *_world; }
@@ -65,7 +64,8 @@ public:
 	void debugPrint(bool withObjects) const;
 
 protected:
-	Room(World *world, Common::SeekableReadStream &stream, bool hasUselessByte);
+	Room(World *world);
+	void readObjectsAndBackground(Common::SeekableReadStream &stream, int16 backgroundScale);
 	void updateScripts();
 	void updateRoomBounds();
 	void updateInteraction();
@@ -75,19 +75,38 @@ protected:
 	ShapeObject *getSelectedObject(ShapeObject *best = nullptr) const;
 
 	World *_world;
-	Common::String _name;
+	Common::String _name, _backgroundName;
 	PathFindingShape _floors[2];
 	bool _fixedCameraOnEntering;
 	int8 _activeFloorI = -1;
 	int _musicId = -1;
 	uint8
-		_characterAlphaTint,
-		_characterAlphaPremultiplier; ///< for some reason in percent instead of 0-255
+		_characterAlphaTint = 0,
+		_characterAlphaPremultiplier = 100; ///< for some reason in percent instead of 0-255
 
 	Common::Array<ObjectBase *> _objects;
 };
 
-class OptionsMenu final : public Room {
+class RoomV3 : public Room {
+public:
+	RoomV3(World *world, Common::SeekableReadStream &stream);
+
+protected:
+	RoomV3(World *world, Common::SeekableReadStream &stream, bool hasUselessByte);
+};
+
+class RoomV1 : public Room {
+public:
+	RoomV1(World *world, Common::SeekableReadStream &stream, bool readObjects = true);
+};
+
+class RoomV1WithFloor final : public RoomV1 {
+public:
+	static constexpr const char *kClassName = "CHabitacionConSuelo";
+	RoomV1WithFloor(World *world, Common::SeekableReadStream &stream);
+};
+
+class OptionsMenu final : public RoomV3 {
 public:
 	static constexpr const char *kClassName = "CHabitacionMenuOpciones";
 	OptionsMenu(World *world, Common::SeekableReadStream &stream);
@@ -104,23 +123,23 @@ private:
 	SlideButton *_currentSlideButton = nullptr;
 };
 
-class ConnectMenu final : public Room {
+class ConnectMenu final : public RoomV3 {
 public:
 	static constexpr const char *kClassName = "CHabitacionConectar";
 	ConnectMenu(World *world, Common::SeekableReadStream &stream);
 };
 
-class ListenMenu final : public Room {
+class ListenMenu final : public RoomV3 {
 public:
 	static constexpr const char *kClassName = "CHabitacionEsperar";
 	ListenMenu(World *world, Common::SeekableReadStream &stream);
 };
 
-class Inventory final : public Room {
+class InventoryV3 final : public RoomV3 {
 public:
 	static constexpr const char *kClassName = "CInventario";
-	Inventory(World *world, Common::SeekableReadStream &stream);
-	~Inventory() override;
+	InventoryV3(World *world, Common::SeekableReadStream &stream);
+	~InventoryV3() override;
 
 	bool updateInput() override;
 
@@ -136,6 +155,12 @@ private:
 	Common::Array<Item *> _items;
 };
 
+class InventoryV1 final : public RoomV1 {
+public:
+	static constexpr const char *kClassName = "CInventario";
+	InventoryV1(World *world, Common::SeekableReadStream &stream);
+};
+
 enum class GlobalAnimationKind {
 	GeneralFont = 0,
 	DialogFont,
@@ -161,7 +186,7 @@ public:
 	inline RoomIterator beginRooms() const { return _rooms.begin(); }
 	inline RoomIterator endRooms() const { return _rooms.end(); }
 	inline Room &globalRoom() const { assert(_globalRoom != nullptr); return *_globalRoom; }
-	inline Inventory &inventory() const { assert(_inventory != nullptr); return *_inventory; }
+	inline InventoryV3 &inventory() const { assert(_inventory != nullptr); return *_inventory; }
 	inline MainCharacter &filemon() const { assert(_filemon != nullptr); return *_filemon; }
 	inline MainCharacter &mortadelo() const { assert(_mortadelo != nullptr);  return *_mortadelo; }
 	inline GameFileReference scriptFileRef() const { return _scriptFileRef; }
@@ -208,7 +233,7 @@ private:
 	Common::String _initScriptName;
 	GameFileReference _scriptFileRef;
 	Room *_globalRoom;
-	Inventory *_inventory;
+	InventoryV3 *_inventory;
 	MainCharacter *_filemon, *_mortadelo;
 	uint8 _loadedMapCount = 0;
 	Common::HashMap<const char *, const char *,


Commit: 81ddc5c1e0e1043aab52884031ab8cb42a5c26f5
    https://github.com/scummvm/scummvm/commit/81ddc5c1e0e1043aab52884031ab8cb42a5c26f5
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:25+01:00

Commit Message:
ALCACHOFA: Support reading V1 objects

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.h
    engines/alcachofa/common.cpp
    engines/alcachofa/common.h
    engines/alcachofa/game-movie-adventure-special.cpp
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/general-objects.cpp
    engines/alcachofa/global-ui.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/graphics.h
    engines/alcachofa/objects.h
    engines/alcachofa/player.cpp
    engines/alcachofa/rooms.cpp
    engines/alcachofa/rooms.h
    engines/alcachofa/script.cpp
    engines/alcachofa/ui-objects.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 17b43f1c126..016804dce9a 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -79,9 +79,9 @@ Common::Error AlcachofaEngine::run() {
 	g_system->showMouse(false);
 	setDebugger(_console);
 	_game.reset(Game::create());
+	_world.load();
 	_renderer.reset(IRenderer::createOpenGLRenderer(game().getResolution()));
 	_drawQueue.reset(new DrawQueue(_renderer.get()));
-	_world.reset(new World());
 	_script.reset(new Script());
 	_player.reset(new Player());
 	_globalUI.reset(new GlobalUI());
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 05123797ebe..04386c5e1af 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -127,7 +127,7 @@ public:
 	inline Input &input() { return _input; }
 	inline Sounds &sounds() { return _sounds; }
 	inline Player &player() { return *_player; }
-	inline World &world() { return *_world; }
+	inline World &world() { return _world; }
 	inline Script &script() { return *_script; }
 	inline GlobalUI &globalUI() { return *_globalUI; }
 	inline Menu &menu() { return *_menu; }
@@ -182,12 +182,12 @@ private:
 	Common::ScopedPtr<IDebugHandler> _debugHandler;
 	Common::ScopedPtr<IRenderer> _renderer;
 	Common::ScopedPtr<DrawQueue> _drawQueue;
-	Common::ScopedPtr<World> _world;
 	Common::ScopedPtr<Script> _script;
 	Common::ScopedPtr<Player> _player;
 	Common::ScopedPtr<GlobalUI> _globalUI;
 	Common::ScopedPtr<Menu> _menu;
 	Common::ScopedPtr<Game> _game;
+	World _world;
 	Camera _camera;
 	Input _input;
 	Sounds _sounds;
diff --git a/engines/alcachofa/common.cpp b/engines/alcachofa/common.cpp
index 771dd1605ea..c37ff0ca330 100644
--- a/engines/alcachofa/common.cpp
+++ b/engines/alcachofa/common.cpp
@@ -19,8 +19,7 @@
  *
  */
 
-#include "alcachofa/common.h"
-#include "alcachofa/detection.h"
+#include "alcachofa/alcachofa.h"
 
 using namespace Common;
 using namespace Math;
@@ -200,4 +199,30 @@ void syncPoint(Serializer &serializer, Point &point) {
 	serializer.syncAsSint32LE(point.y);
 }
 
+static constexpr Direction kV1Directions[] = {
+	Direction::Up,
+	Direction::UpRight,
+	Direction::Right,
+	Direction::DownRight,
+	Direction::Down,
+	Direction::DownLeft,
+	Direction::Left,
+	Direction::UpLeft
+};
+
+Direction intToDirection(int32 rawValue) {
+	if (g_engine->isV1()) {
+		scumm_assert(rawValue >= 0 && rawValue < kFullDirectionCount);
+		return kV1Directions[rawValue];
+	} else {
+		scumm_assert(rawValue >= 0 && rawValue < kDirectionCount);
+		return (Direction)rawValue;
+	}
+}
+
+Direction readDirection(Common::ReadStream &stream) {
+	int32 rawValue = stream.readSint32LE();
+	return intToDirection(rawValue);
+}
+
 }
diff --git a/engines/alcachofa/common.h b/engines/alcachofa/common.h
index a274df1c0d9..cbef429d523 100644
--- a/engines/alcachofa/common.h
+++ b/engines/alcachofa/common.h
@@ -47,6 +47,13 @@ enum class Direction {
 	Down,
 	Left,
 
+	// V1 has some fields with these additional directions
+	// but they are largely irrelevant so we map them to the main four
+	UpRight,
+	DownRight,
+	DownLeft,
+	UpLeft,
+
 	Invalid = -1
 };
 
@@ -64,6 +71,7 @@ enum class EasingType {
 };
 
 constexpr const int32 kDirectionCount = 4;
+constexpr const int32 kFullDirectionCount = 8; ///< only to be used for IO or mapping V1 values
 constexpr const int8 kOrderCount = 70;
 constexpr const int8 kForegroundOrderCount = 10;
 
@@ -133,6 +141,8 @@ Common::Point readPoint(Common::ReadStream &stream);
 Common::String readVarString(Common::ReadStream &stream);
 void skipVarString(Common::SeekableReadStream &stream);
 void syncPoint(Common::Serializer &serializer, Common::Point &point);
+Direction intToDirection(int32 value);
+Direction readDirection(Common::ReadStream &stream);
 
 template<typename T>
 inline void syncArray(Common::Serializer &serializer, Common::Array<T> &array, void (*serializeFunction)(Common::Serializer &, T &)) {
@@ -185,7 +195,7 @@ struct GameFileReference {
 
 	GameFileReference() {}
 
-	GameFileReference(const Common::String &path)
+	explicit GameFileReference(const Common::String &path)
 		: _path(path) {}
 
 	// in this case, path is only for debugging purposes
@@ -196,7 +206,11 @@ struct GameFileReference {
 		, _size(size) {}
 
 	inline bool isValid() const {
-		return !_path.empty() || _position >= 0;
+		return !_path.empty() || _fileIndex != UINT32_MAX;
+	}
+
+	inline bool isEmbedded() const {
+		return _fileIndex != UINT32_MAX;
 	}
 };
 
diff --git a/engines/alcachofa/game-movie-adventure-special.cpp b/engines/alcachofa/game-movie-adventure-special.cpp
index 51fb99d3d52..02b54199a59 100644
--- a/engines/alcachofa/game-movie-adventure-special.cpp
+++ b/engines/alcachofa/game-movie-adventure-special.cpp
@@ -209,7 +209,7 @@ public:
 	}
 
 	GameFileReference getScriptFileRef() {
-		return { "Script/SCRIPT.COD" };
+		return GameFileReference("Script/SCRIPT.COD");
 	}
 
 	Span<const ScriptOp> getScriptOpMap() override {
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 8883704b68e..cd2e9f08160 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -34,7 +34,11 @@ const char *Item::typeName() const { return "Item"; }
 
 Item::Item(Room *room, SeekableReadStream &stream)
 	: GraphicObject(room, stream) {
-	stream.readByte(); // unused and ignored byte
+	// unused data in any case
+	if (g_engine->isV1())
+		skipVarString(stream);
+	else
+		stream.skip(1);
 }
 
 Item::Item(const Item &other)
@@ -70,9 +74,14 @@ void Item::trigger() {
 		player.triggerObject(heldItem, name().c_str());
 }
 
-ITriggerableObject::ITriggerableObject(SeekableReadStream &stream)
-	: _interactionPoint(Shape(stream).firstPoint())
-	, _interactionDirection((Direction)stream.readSint32LE()) {}
+ITriggerableObject::ITriggerableObject(SeekableReadStream &stream) {
+	read(stream);
+}
+
+void ITriggerableObject::read(SeekableReadStream &stream) {
+	_interactionPoint = Shape(stream).firstPoint();
+	_interactionDirection = readDirection(stream);
+}
 
 void ITriggerableObject::onClick() {
 	auto heldItem = g_engine->player().heldItem();
@@ -91,6 +100,9 @@ InteractableObject::InteractableObject(Room *room, SeekableReadStream &stream)
 	, ITriggerableObject(stream)
 	, _relatedObject(readVarString(stream)) {
 	_relatedObject.toUppercase();
+
+	if (g_engine->isV1())
+		skipVarString(stream); // unused script field
 }
 
 void InteractableObject::drawDebug() {
@@ -124,7 +136,7 @@ Door::Door(Room *room, SeekableReadStream &stream)
 	: InteractableObject(room, stream)
 	, _targetRoom(readVarString(stream))
 	, _targetObject(readVarString(stream))
-	, _characterDirection((Direction)stream.readSint32LE()) {
+	, _characterDirection(readDirection(stream)) {
 	_targetRoom.replace(' ', '_');
 }
 
@@ -163,13 +175,28 @@ void Door::trigger(const char *_) {
 const char *Character::typeName() const { return "Character"; }
 
 Character::Character(Room *room, SeekableReadStream &stream)
-	: ShapeObject(room, stream)
-	, ITriggerableObject(stream)
-	, _graphicNormal(stream)
-	, _graphicTalking(stream) {
+	: ShapeObject(room, stream) {
+	if (g_engine->isV1()) {
+		// the structure in V1 is quite redundant
+		toggle(readBool(stream));
+		_order = stream.readByte();
+		ITriggerableObject::read(stream);
+		skipVarString(stream); // unused "relatedGraphicObject" field
+		skipVarString(stream); // unused "script" field
+		toggle(readBool(stream));
+		_graphicNormal = Graphic(stream);
+		auto talkingFileRef = g_engine->world().readFileRef(stream);
+		_graphicTalking = _graphicNormal;
+		_graphicTalking.setAnimation(talkingFileRef, AnimationFolder::Animations);
+	} else {
+		ITriggerableObject::read(stream);
+		_graphicNormal = Graphic(stream);
+		_graphicTalking = Graphic(stream);
+		_order = _graphicNormal.order();
+	}
+
 	_graphicNormal.start(true);
 	_graphicNormal.frameI() = _graphicTalking.frameI() = 0;
-	_order = _graphicNormal.order();
 }
 
 static Graphic *graphicOf(ObjectBase *object, Graphic *fallback = nullptr) {
@@ -500,13 +527,18 @@ const char *WalkingCharacter::typeName() const { return "WalkingCharacter"; }
 
 WalkingCharacter::WalkingCharacter(Room *room, SeekableReadStream &stream)
 	: Character(room, stream) {
+	// every other animation is for an unused direction
+
+	const auto &world = g_engine->world();
 	for (int32 i = 0; i < kDirectionCount; i++) {
-		auto fileName = readVarString(stream);
-		_walkingAnimations[i].reset(new Animation(Common::move(fileName)));
+		auto fileRef = world.readFileRef(stream);
+		_walkingAnimations[i].reset(new Animation(move(fileRef)));
+		world.readFileRef(stream);
 	}
 	for (int32 i = 0; i < kDirectionCount; i++) {
-		auto fileName = readVarString(stream);
-		_talkingAnimations[i].reset(new Animation(Common::move(fileName)));
+		auto fileRef = world.readFileRef(stream);
+		_talkingAnimations[i].reset(new Animation(move(fileRef)));
+		world.readFileRef(stream);
 	}
 }
 
@@ -1148,7 +1180,7 @@ const char *Background::typeName() const { return "Background"; }
 Background::Background(Room *room, const String &animationFileName, int16 scale)
 	: GraphicObject(room, "BACKGROUND") {
 	toggle(true);
-	_graphic.setAnimation(animationFileName, AnimationFolder::Backgrounds);
+	_graphic.setAnimation(GameFileReference(animationFileName), AnimationFolder::Backgrounds);
 	_graphic.scale() = scale;
 	_graphic.order() = 59;
 }
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index 91252e99794..4ba9981d7fa 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -45,7 +45,7 @@ ObjectBase::ObjectBase(Room *room, SeekableReadStream &stream)
 	: _room(room) {
 	assert(room != nullptr);
 	_name = readVarString(stream);
-	_isEnabled = readBool(stream);
+	_isEnabled = g_engine->isV1() ? true : readBool(stream);
 }
 
 void ObjectBase::toggle(bool isEnabled) {
@@ -84,10 +84,16 @@ PointObject::PointObject(Room *room, SeekableReadStream &stream)
 const char *GraphicObject::typeName() const { return "GraphicObject"; }
 
 GraphicObject::GraphicObject(Room *room, SeekableReadStream &stream)
-	: ObjectBase(room, stream)
-	, _graphic(stream)
-	, _type((GraphicObjectType)stream.readSint32LE())
-	, _posterizeAlpha(100 - stream.readSint32LE()) {
+	: ObjectBase(room, stream) {
+	if (g_engine->isV1()) {
+		toggle(readBool(stream));
+		_graphic = Graphic(stream);
+	} else {
+		_graphic = Graphic(stream);
+		_type = (GraphicObjectType)stream.readSint32LE();
+		_posterizeAlpha = 100 - stream.readSint32LE();
+	}
+
 	_graphic.start(true);
 }
 
@@ -233,8 +239,10 @@ const char *ShapeObject::typeName() const { return "ShapeObject"; }
 
 ShapeObject::ShapeObject(Room *room, SeekableReadStream &stream)
 	: ObjectBase(room, stream)
-	, _shape(stream)
-	, _cursorType((CursorType)stream.readSint32LE()) {}
+	, _shape(stream) {
+	if (g_engine->isV2() || g_engine->isV3())
+		_cursorType = (CursorType)stream.readSint32LE();
+}
 
 void ShapeObject::update() {
 	if (isEnabled())
@@ -304,6 +312,8 @@ const char *PhysicalObject::typeName() const { return "PhysicalObject"; }
 
 PhysicalObject::PhysicalObject(Room *room, SeekableReadStream &stream)
 	: ShapeObject(room, stream) {
+	if (g_engine->isV1())
+		toggle(readBool(stream));
 	_order = stream.readSByte();
 }
 
diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index 7f7ab5740cc..4164dec0fc0 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -46,11 +46,11 @@ Rect closeInventoryTriggerBounds() {
 
 GlobalUI::GlobalUI() {
 	auto &world = g_engine->world();
-	_generalFont.reset(new Font(world.getGlobalAnimationName(GlobalAnimationKind::GeneralFont)));
-	_dialogFont.reset(new Font(world.getGlobalAnimationName(GlobalAnimationKind::DialogFont)));
-	_iconMortadelo.reset(new Animation(world.getGlobalAnimationName(GlobalAnimationKind::MortadeloIcon)));
-	_iconFilemon.reset(new Animation(world.getGlobalAnimationName(GlobalAnimationKind::FilemonIcon)));
-	_iconInventory.reset(new Animation(world.getGlobalAnimationName(GlobalAnimationKind::InventoryIcon)));
+	_generalFont.reset(new Font(world.getGlobalAnimation(GlobalAnimationKind::GeneralFont)));
+	_dialogFont.reset(new Font(world.getGlobalAnimation(GlobalAnimationKind::DialogFont)));
+	_iconMortadelo.reset(new Animation(world.getGlobalAnimation(GlobalAnimationKind::MortadeloIcon)));
+	_iconFilemon.reset(new Animation(world.getGlobalAnimation(GlobalAnimationKind::FilemonIcon)));
+	_iconInventory.reset(new Animation(world.getGlobalAnimation(GlobalAnimationKind::InventoryIcon)));
 
 	_generalFont->load();
 	_dialogFont->load();
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 45e2d3ce8b1..42fe7a9eecf 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -59,8 +59,8 @@ void IDebugRenderer::debugShape(const Shape &shape, Color color) {
 	}
 }
 
-AnimationBase::AnimationBase(String fileName, AnimationFolder folder)
-	: _fileName(reencode(fileName))
+AnimationBase::AnimationBase(GameFileReference fileRef, AnimationFolder folder)
+	: _fileRef(move(fileRef))
 	, _folder(folder) {}
 
 AnimationBase::~AnimationBase() {
@@ -71,6 +71,8 @@ void AnimationBase::load() {
 	if (_isLoaded)
 		return;
 
+	assert(!_fileRef.isEmbedded());
+
 	String fullPath;
 	switch (_folder) {
 	case AnimationFolder::Animations:
@@ -86,14 +88,14 @@ void AnimationBase::load() {
 		assert(false && "Invalid AnimationFolder");
 		break;
 	}
-	if (_fileName.size() < 4 || scumm_strnicmp(_fileName.end() - 4, ".AN0", 4) != 0)
-		_fileName += ".AN0";
-	fullPath += _fileName;
+	if (_fileRef._path.size() < 4 || scumm_strnicmp(_fileRef._path.end() - 4, ".AN0", 4) != 0)
+		_fileRef._path += ".AN0";
+	fullPath += _fileRef._path;
 
 	File file;
 	if (!file.open(fullPath.c_str())) {
 		// original fallback
-		fullPath = "Mascaras/" + _fileName;
+		fullPath = "Mascaras/" + _fileRef._path;
 		if (!file.open(fullPath.c_str())) {
 			loadMissingAnimation();
 			return;
@@ -170,7 +172,7 @@ ManagedSurface *AnimationBase::readImage(SeekableReadStream &stream) const {
 	SeekableSubReadStream subStream(&stream, stream.pos(), stream.size());
 	TGADecoder decoder;
 	if (!decoder.loadStream(subStream))
-		error("Failed to load TGA from animation %s", _fileName.c_str());
+		error("Failed to load TGA from animation %s", _fileRef._path.c_str());
 
 	// The length of the image is unknown but TGADecoder does not read
 	// the end marker, so let's search for it.
@@ -183,7 +185,7 @@ ManagedSurface *AnimationBase::readImage(SeekableReadStream &stream) const {
 		if (potentialStart < buffer + kMarkerLength)
 			memmove(buffer, potentialStart, kMarkerLength - nextRead);
 		if (stream.read(buffer + kMarkerLength - nextRead, nextRead) != nextRead)
-			error("Unexpected end-of-file in animation %s", _fileName.c_str());
+			error("Unexpected end-of-file in animation %s", _fileRef._path.c_str());
 		potentialStart = find(buffer + 1, buffer + kMarkerLength, kExpectedMarker[0]);
 	} while (strncmp(buffer, kExpectedMarker, kMarkerLength) != 0);
 
@@ -202,7 +204,7 @@ ManagedSurface *AnimationBase::readImage(SeekableReadStream &stream) const {
 
 void AnimationBase::loadMissingAnimation() {
 	// only allow missing animations we know are faulty in the original game
-	g_engine->game().missingAnimation(_fileName);
+	g_engine->game().missingAnimation(_fileRef._path);
 
 	// otherwise setup a functioning but empty animation
 	_isLoaded = true;
@@ -248,8 +250,8 @@ Point AnimationBase::imageSize(int32 imageI) const {
 	return image == nullptr ? Point() : Point(image->w, image->h);
 }
 
-Animation::Animation(String fileName, AnimationFolder folder)
-	: AnimationBase(fileName, folder) {}
+Animation::Animation(GameFileReference fileRef, AnimationFolder folder)
+	: AnimationBase(move(fileRef), folder) {}
 
 void Animation::load() {
 	if (_isLoaded)
@@ -447,7 +449,7 @@ void Animation::drawEffect(int32 frameI, Vector3d topLeft, Vector2d size, Vector
 	renderer.quad(as2D(topLeft), size, kWhite, rotation, texMin + texOffset, texMax + texOffset);
 }
 
-Font::Font(String fileName) : AnimationBase(fileName) {}
+Font::Font(GameFileReference fileRef) : AnimationBase(move(fileRef)) {}
 
 void Font::load() {
 	if (_isLoaded)
@@ -487,7 +489,7 @@ void Font::load() {
 	}
 	_texture = g_engine->renderer().createTexture(atlasSurface.w, atlasSurface.h, false);
 	_texture->update(atlasSurface);
-	debugCN(1, kDebugGraphics, "Rendered font atlas %s at %dx%d", _fileName.c_str(), atlasSurface.w, atlasSurface.h);
+	debugCN(1, kDebugGraphics, "Rendered font atlas %s at %dx%d", _fileRef._path.c_str(), atlasSurface.w, atlasSurface.h);
 }
 
 void Font::freeImages() {
@@ -512,29 +514,27 @@ void Font::drawCharacter(int32 imageI, Point centerPoint, Color color) {
 
 Graphic::Graphic() {}
 
-Graphic::Graphic(ReadStream &stream) {
-	_topLeft.x = stream.readSint16LE();
-	_topLeft.y = stream.readSint16LE();
-	_scale = stream.readSint16LE();
-	_order = stream.readSByte();
-	auto animationName = readVarString(stream);
-	if (!animationName.empty())
-		setAnimation(animationName, AnimationFolder::Animations);
-}
-
-Graphic::Graphic(const Graphic &other)
-	: _animation(other._animation)
-	, _topLeft(other._topLeft)
-	, _scale(other._scale)
-	, _order(other._order)
-	, _color(other._color)
-	, _isPaused(other._isPaused)
-	, _isLooping(other._isLooping)
-	, _lastTime(other._lastTime)
-	, _frameI(other._frameI)
-	, _depthScale(other._depthScale) {}
-
-Graphic &Graphic::operator= (const Graphic &other) {
+Graphic::Graphic(SeekableReadStream &stream) {
+	if (g_engine->isV1()) {
+		_topLeft = readPoint(stream);
+		_scale = stream.readSint16LE();
+		_order = (int8)stream.readSint16LE();
+	} else {
+		_topLeft.x = stream.readSint16LE();
+		_topLeft.y = stream.readSint16LE();
+		_scale = stream.readSint16LE();
+		_order = stream.readSByte();
+	}
+	auto animationRef = g_engine->world().readFileRef(stream);
+	if (animationRef.isValid())
+		setAnimation(animationRef, AnimationFolder::Animations);
+}
+
+Graphic::Graphic(const Graphic &other) {
+	*this = other;
+}
+
+Graphic &Graphic::operator=(const Graphic &other) {
 	_ownedAnimation.reset();
 	_animation = other._animation;
 	_topLeft = other._topLeft;
@@ -602,8 +602,8 @@ void Graphic::reset() {
 	_lastTime = _isPaused ? 0 : g_engine->getMillis();
 }
 
-void Graphic::setAnimation(const Common::String &fileName, AnimationFolder folder) {
-	_ownedAnimation.reset(new Animation(fileName, folder));
+void Graphic::setAnimation(const GameFileReference &fileRef, AnimationFolder folder) {
+	_ownedAnimation.reset(new Animation(fileRef, folder));
 	_animation = _ownedAnimation.get();
 }
 
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 7a751491da4..1cb5da94f8f 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -148,7 +148,7 @@ struct AnimationFrame {
  */
 class AnimationBase {
 protected:
-	AnimationBase(Common::String fileName, AnimationFolder folder = AnimationFolder::Animations);
+	AnimationBase(GameFileReference fileRef, AnimationFolder folder = AnimationFolder::Animations);
 	~AnimationBase();
 
 	void load();
@@ -165,7 +165,7 @@ protected:
 		int offsetY);
 
 	static constexpr const uint kMaxSpriteIDs = 256;
-	Common::String _fileName;
+	GameFileReference _fileRef;
 	AnimationFolder _folder;
 	bool _isLoaded = false;
 	uint32 _totalDuration = 0;
@@ -185,7 +185,7 @@ protected:
  */
 class Animation : private AnimationBase {
 public:
-	Animation(Common::String fileName, AnimationFolder folder = AnimationFolder::Animations);
+	Animation(GameFileReference fileRef, AnimationFolder folder = AnimationFolder::Animations);
 
 	void load();
 	void freeImages();
@@ -241,7 +241,7 @@ private:
 
 class Font : private AnimationBase {
 public:
-	Font(Common::String fileName);
+	Font(GameFileReference fileRef);
 
 	void load();
 	void freeImages();
@@ -259,9 +259,11 @@ private:
 class Graphic {
 public:
 	Graphic();
-	Graphic(Common::ReadStream &stream);
+	Graphic(Common::SeekableReadStream &stream);
 	Graphic(const Graphic &other); // animation reference is taken, so keep other alive
-	Graphic &operator= (const Graphic &other);
+	Graphic(Graphic &&other) = default;
+	Graphic &operator=(const Graphic &other);
+	Graphic &operator=(Graphic &&other) = default;
 
 	inline Common::Point &topLeft() { return _topLeft; }
 	inline int8 &order() { return _order; }
@@ -287,7 +289,7 @@ public:
 	void start(bool looping);
 	void pause();
 	void reset();
-	void setAnimation(const Common::String &fileName, AnimationFolder folder);
+	void setAnimation(const GameFileReference &fileRef, AnimationFolder folder);
 	void setAnimation(Animation *animation); ///< no memory ownership is given, but for prerendering it has to be mutable
 	void syncGame(Common::Serializer &serializer);
 
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index db974ef6808..0e870ea333c 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -102,8 +102,8 @@ protected:
 	GraphicObject(Room *room, const char *name);
 
 	Graphic _graphic;
-	GraphicObjectType _type;
-	int32 _posterizeAlpha;
+	GraphicObjectType _type = GraphicObjectType::Normal;
+	int32 _posterizeAlpha = 100;
 };
 
 class SpecialEffectObject final : public GraphicObject {
@@ -147,17 +147,33 @@ protected:
 	int8 _order = 0;
 private:
 	Shape _shape;
-	CursorType _cursorType;
+	CursorType _cursorType = CursorType::Point;
 	bool _isNewlySelected = false,
 		_wasSelected = false;
 };
 
 class PhysicalObject : public ShapeObject {
 public:
+	static constexpr const char *kClassName = "CObjetoFisico";
 	PhysicalObject(Room *room, Common::SeekableReadStream &stream);
 	const char *typeName() const override;
 };
 
+class ButtonV1 : public PhysicalObject {
+public:
+	static constexpr const char *kClassName = "CBoton";
+	ButtonV1(Room *room, Common::SeekableReadStream &stream);
+
+	inline byte actionId() const { return _actionId; }
+	inline const Common::String &actionName() const { return _actionName; }
+
+	const char *typeName() const override;
+
+private:
+	byte _actionId;
+	Common::String _actionName;
+};
+
 class MenuButton : public PhysicalObject {
 public:
 	static constexpr const char *kClassName = "CBotonMenu";
@@ -374,6 +390,8 @@ public:
 	virtual void trigger(const char *action) = 0;
 
 protected:
+	ITriggerableObject() = default;
+	void read(Common::SeekableReadStream &stream);
 	void onClick();
 
 	Common::Point _interactionPoint;
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 2daf48b5fa6..fe0e4651abe 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -31,8 +31,8 @@ namespace Alcachofa {
 Player::Player()
 	: _activeCharacter(&g_engine->world().mortadelo())
 	, _semaphore("player") {
-	const auto &cursorPath = g_engine->world().getGlobalAnimationName(GlobalAnimationKind::Cursor);
-	_cursorAnimation.reset(new Animation(cursorPath));
+	const auto &cursorRef = g_engine->world().getGlobalAnimation(GlobalAnimationKind::Cursor);
+	_cursorAnimation.reset(new Animation(cursorRef));
 	_cursorAnimation->load();
 }
 
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 2c5e3d3304a..e3063ad9292 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -83,6 +83,8 @@ static ObjectBase *readRoomObject(Room *room, const String &type, SeekableReadSt
 		return new MainCharacter(room, stream);
 	else if (type == FloorColor::kClassName)
 		return new FloorColor(room, stream);
+	else if (type == ButtonV1::kClassName)
+		return new ButtonV1(room, stream);
 	else
 		return nullptr; // handled in Room::Room
 }
@@ -117,7 +119,7 @@ void Room::readObjectsAndBackground(SeekableReadStream &stream, int16 background
 			g_engine->game().notEnoughObjectDataRead(_name.c_str(), stream.pos(), objectEnd);
 			stream.seek(objectEnd, SEEK_SET);
 		} else if (stream.pos() > objectEnd) // this is probably not recoverable
-			error("Read past the object data (%u > %lld) in room %s", objectEnd, (long long int)stream.pos(), _name.c_str());
+			error("Read past the object data (%u < %lld) in room %s", objectEnd, (long long int)stream.pos(), _name.c_str());
 
 		if (object != nullptr)
 			_objects.push_back(object);
@@ -527,7 +529,7 @@ void Room::debugPrint(bool withObjects) const {
 	}
 }
 
-World::World() {
+void World::load() {
 	_scriptFileRef = g_engine->game().getScriptFileRef();
 
 	auto loadWorldFile =
@@ -645,11 +647,10 @@ void World::toggleObject(MainCharacterKind character, const char *objName, bool
 		object->toggle(isEnabled);
 }
 
-const Common::String &World::getGlobalAnimationName(GlobalAnimationKind kind) const {
+const GameFileReference &World::getGlobalAnimation(GlobalAnimationKind kind) const {
 	int kindI = (int)kind;
 	assert(kindI >= 0 && kindI < (int)GlobalAnimationKind::Count);
-	error("You broke this, remember?");
-	//return _globalAnimationNames[kindI];
+	return _globalAnimations[kindI];
 }
 
 const char *World::getLocalizedName(const String &name) const {
@@ -904,10 +905,11 @@ GameFileReference World::readFileRef(SeekableReadStream &stream) const {
 	auto name = readVarString(stream);
 	if (g_engine->isV1()) {
 		uint32 size = stream.readUint32LE();
+		uint32 offset = (uint32)stream.pos();
 		stream.skip(size);
-		return { name, (uint32)_files.size(), (uint32)stream.pos(), size };
+		return { name, (uint32)_files.size(), offset, size };
 	} else
-		return { name };
+		return GameFileReference(reencode(name));
 }
 
 ScopedPtr<SeekableReadStream> World::openFileRef(const GameFileReference &ref) const {
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index dab0da4641b..f53042d256c 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -177,8 +177,8 @@ enum class GlobalAnimationKind {
 
 class World final {
 public:
-	World();
 	~World();
+	void load(); ///< unfortunately has to be split from ctor, so g_engine->world() is already set during load
 
 	// reference-returning queries will error if the object does not exist
 
@@ -204,7 +204,7 @@ public:
 	ObjectBase *getObjectByName(const char *name) const;
 	ObjectBase *getObjectByName(MainCharacterKind character, const char *name) const;
 	ObjectBase *getObjectByNameFromAnyRoom(const char *name) const;
-	const Common::String &getGlobalAnimationName(GlobalAnimationKind kind) const;
+	const GameFileReference &getGlobalAnimation(GlobalAnimationKind kind) const;
 	const char *getLocalizedName(const Common::String &name) const;
 	const char *getDialogLine(int32 dialogId) const;
 
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index c7665435cef..f6b277c84ed 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -677,11 +677,11 @@ private:
 			if (character == nullptr)
 				g_engine->game().unknownScriptCharacter("stop-and-turn", getStringArg(0));
 			else
-				character->stopWalking((Direction)getNumberArg(1));
+				character->stopWalking(intToDirection(getNumberArg(1)));
 			return TaskReturn::finish(1);
 		}
 		case ScriptKernelTask::StopAndTurnMe: {
-			relatedCharacter().stopWalking((Direction)getNumberArg(0));
+			relatedCharacter().stopWalking(intToDirection(getNumberArg(0)));
 			return TaskReturn::finish(1);
 		}
 		case ScriptKernelTask::Go: {
diff --git a/engines/alcachofa/ui-objects.cpp b/engines/alcachofa/ui-objects.cpp
index c3745935556..79b35bba11c 100644
--- a/engines/alcachofa/ui-objects.cpp
+++ b/engines/alcachofa/ui-objects.cpp
@@ -30,6 +30,13 @@ using namespace Common;
 
 namespace Alcachofa {
 
+const char *ButtonV1::typeName() const { return "ButtonV1"; }
+
+ButtonV1::ButtonV1(Room *room, Common::SeekableReadStream &stream)
+	: PhysicalObject(room, stream)
+	, _actionId(stream.readByte())
+	, _actionName(readVarString(stream)) {}
+
 const char *MenuButton::typeName() const { return "MenuButton"; }
 
 MenuButton::MenuButton(Room *room, SeekableReadStream &stream)


Commit: 8e3a73a757d1de3499edd812fa04f91d3f6329be
    https://github.com/scummvm/scummvm/commit/8e3a73a757d1de3499edd812fa04f91d3f6329be
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:25+01:00

Commit Message:
ALCACHOFA: Support V1 text files

Changed paths:
    engines/alcachofa/game-movie-adventure-original.cpp
    engines/alcachofa/game-movie-adventure-special.cpp
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/game.h
    engines/alcachofa/rooms.cpp
    engines/alcachofa/rooms.h


diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index b51490f38e7..eef4837838e 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -40,7 +40,7 @@ public:
 		}
 	}
 
-	Point getResolution() {
+	Point getResolution() override {
 		return Point(800, 600);
 	}
 
@@ -50,14 +50,26 @@ public:
 		"global.emc",
 		nullptr
 	};
-	const char *const *getMapFiles() {
+	const char *const *getMapFiles() override {
 		return kMapFiles;
 	}
 
-	GameFileReference getScriptFileRef() {
+	GameFileReference getScriptFileRef() override {
 		// V1 embeds the script into global.emc, it is overridden during world load
 		return {};
 	}
+
+	const char *getDialogFileName() override {
+		return "TEXTOS.TXT";
+	}
+
+	const char *getObjectFileName() override {
+		return "OBJETOS.TXT";
+	}
+
+	char getTextFileKey() override {
+		return kNoXORKey;
+	}
 };
 
 Game *Game::createForMovieAdventureOriginal() {
diff --git a/engines/alcachofa/game-movie-adventure-special.cpp b/engines/alcachofa/game-movie-adventure-special.cpp
index 02b54199a59..66a5f6f8e5b 100644
--- a/engines/alcachofa/game-movie-adventure-special.cpp
+++ b/engines/alcachofa/game-movie-adventure-special.cpp
@@ -208,7 +208,7 @@ public:
 		return kMapFiles;
 	}
 
-	GameFileReference getScriptFileRef() {
+	GameFileReference getScriptFileRef() override {
 		return GameFileReference("Script/SCRIPT.COD");
 	}
 
@@ -216,6 +216,18 @@ public:
 		return { kScriptOpMap, ARRAYSIZE(kScriptOpMap) };
 	}
 
+	const char *getDialogFileName() override {
+		return "Textos/DIALOGOS.nkr";
+	}
+
+	const char *getObjectFileName() override {
+		return "Textos/OBJETOS.nkr";
+	}
+
+	char getTextFileKey() override {
+		return kEmbeddedXORKey;
+	}
+
 	void updateScriptVariables() override {
 		Script &script = g_engine->script();
 		if (g_engine->input().wasAnyMousePressed()) // yes, this variable is never reset by the engine (only by script)
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index cd2e9f08160..8cfc74ce801 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -533,12 +533,14 @@ WalkingCharacter::WalkingCharacter(Room *room, SeekableReadStream &stream)
 	for (int32 i = 0; i < kDirectionCount; i++) {
 		auto fileRef = world.readFileRef(stream);
 		_walkingAnimations[i].reset(new Animation(move(fileRef)));
-		world.readFileRef(stream);
+		if (g_engine->isV1())
+			world.readFileRef(stream);
 	}
 	for (int32 i = 0; i < kDirectionCount; i++) {
 		auto fileRef = world.readFileRef(stream);
 		_talkingAnimations[i].reset(new Animation(move(fileRef)));
-		world.readFileRef(stream);
+		if (g_engine->isV1())
+			world.readFileRef(stream);
 	}
 }
 
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index fa64a1dea70..d559c3b1336 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -58,6 +58,9 @@ public:
 	virtual void updateScriptVariables() = 0;
 	virtual bool shouldClipCamera() = 0;
 	virtual void drawScreenStates();
+	virtual const char *getDialogFileName() = 0;
+	virtual const char *getObjectFileName() = 0;
+	virtual char getTextFileKey() = 0;
 
 	virtual bool doesRoomHaveBackground(const Room *room);
 	virtual void unknownRoomObject(const Common::String &type);
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index e3063ad9292..003fcef4203 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -794,26 +794,39 @@ void World::readRooms(World::ReadRoomFunc readRoom, File &file) {
 }
 
 /**
- * @brief Behold the incredible encryption of text files:
+ * @brief Behold the incredible encryption of text files (for V3):
  *   - first 32 bytes are cipher
  *   - next byte is the XOR key
  *   - next 4 bytes are garbage
  *   - rest of the file is cipher
+ *
+ * V1 is unencrypted
+ * V2 uses a constant key
  */
-static void loadEncryptedFile(const char *path, Array<char> &output) {
+static void loadEncryptedFile(const char *path, char key, Array<char> &output) {
 	constexpr uint kHeaderSize = 32;
 	File file;
 	if (!file.open(path))
 		error("Could not open text file %s", path);
-	output.resize(file.size() - 4 - 1 + 1); // garbage bytes, key and we add a zero terminator for safety
-	if (file.read(output.data(), kHeaderSize) != kHeaderSize)
-		error("Could not read text file header");
-	char key = file.readSByte();
-	uint remainingSize = output.size() - kHeaderSize - 1;
-	if (!file.skip(4) || file.read(output.data() + kHeaderSize, remainingSize) != remainingSize)
-		error("Could not read text file body");
-	for (auto &ch : output)
-		ch ^= key;
+
+	if (key == kEmbeddedXORKey) {
+		output.resize(file.size() - 4 - 1 + 1); // garbage bytes, key and we add a zero terminator for safety
+		if (file.read(output.data(), kHeaderSize) != kHeaderSize)
+			error("Could not read text file header");
+		key = file.readSByte();
+		uint remainingSize = output.size() - kHeaderSize - 1;
+		if (!file.skip(4) || file.read(output.data() + kHeaderSize, remainingSize) != remainingSize)
+			error("Could not read text file body");
+	} else {
+		output.resize(file.size() + 1);
+		if (file.read(output.data(), output.size() - 1) != output.size() - 1)
+			error("Could not read text file");
+	}
+
+	if (key != kNoXORKey) {
+		for (auto &ch : output)
+			ch ^= key;
+	}
 	output.back() = '\0'; // one for good measure and a zero-terminator
 }
 
@@ -836,24 +849,50 @@ static char *trimTrailing(char *start, char *end) {
 }
 
 void World::loadLocalizedNames() {
-	loadEncryptedFile("Textos/OBJETOS.nkr", _namesChunk);
+	loadEncryptedFile(
+		g_engine->game().getObjectFileName(),
+		g_engine->game().getTextFileKey(),
+		_namesChunk);
 	char *lineStart = _namesChunk.begin(), *fileEnd = _namesChunk.end() - 1;
-	while (lineStart < fileEnd) {
-		char *lineEnd = find(lineStart, fileEnd, '\n');
-		char *keyEnd = find(lineStart, lineEnd, '#');
-		if (keyEnd == lineStart || keyEnd == lineEnd || keyEnd + 1 == lineEnd)
-			error("Invalid localized name line separator");
-		char *valueEnd = trimTrailing(keyEnd + 1, lineEnd);
-
-		*keyEnd = 0;
-		*valueEnd = 0;
-		if (valueEnd == keyEnd + 1) {
-			// happens in the english version of Movie Adventure
-			warning("Empty localized name for %s", lineStart);
+
+	if (*lineStart == '\"') {
+		// "key" "value"
+		while (lineStart < fileEnd) {
+			char *lineEnd = find(lineStart, fileEnd, '\n');
+			char *keyStart = find(lineStart, lineEnd, '\"');
+			char *keyEnd = find(MIN(keyStart + 1, lineEnd), lineEnd, '\"');
+			char *valueStart = find(MIN(keyEnd + 1, lineEnd), lineEnd, '\"');
+			char *valueEnd = find(MIN(valueStart + 1, lineEnd), lineEnd, '\"');
+			if (keyStart == lineEnd || keyEnd == lineEnd || keyStart + 1 == keyEnd ||
+				valueStart == lineEnd || valueEnd == lineEnd)
+				error("Invalid localized name line");
+
+			keyStart++;
+			*keyEnd = 0;
+			valueStart++;
+			*valueEnd = 0;
+			_localizedNames[keyStart] = valueStart;
+			lineStart = lineEnd + 1;
 		}
+	} else {
+		// key#value
+		while (lineStart < fileEnd) {
+			char *lineEnd = find(lineStart, fileEnd, '\n');
+			char *keyEnd = find(lineStart, lineEnd, '#');
+			if (keyEnd == lineStart || keyEnd == lineEnd || keyEnd + 1 == lineEnd)
+				error("Invalid localized name line separator");
+			char *valueEnd = trimTrailing(keyEnd + 1, lineEnd);
+
+			*keyEnd = 0;
+			*valueEnd = 0;
+			if (valueEnd == keyEnd + 1) {
+				// happens in the english version of Movie Adventure
+				warning("Empty localized name for %s", lineStart);
+			}
 
-		_localizedNames[lineStart] = keyEnd + 1;
-		lineStart = lineEnd + 1;
+			_localizedNames[lineStart] = keyEnd + 1;
+			lineStart = lineEnd + 1;
+		}
 	}
 }
 
@@ -866,7 +905,10 @@ void World::loadDialogLines() {
 	 * - The ID does not have to be correct, it is ignored by the original engine.
 	 * - We only need the dialog line and insert null-terminators where appropriate.
 	 */
-	loadEncryptedFile("Textos/DIALOGOS.nkr", _dialogChunk);
+	loadEncryptedFile(
+		g_engine->game().getDialogFileName(),
+		g_engine->game().getTextFileKey(),
+		_dialogChunk);
 	char *lineStart = _dialogChunk.begin(), *fileEnd = _dialogChunk.end() - 1;
 	while (lineStart < fileEnd) {
 		char *lineEnd = find(lineStart, fileEnd, '\n');
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index f53042d256c..8ad415d0e33 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -77,7 +77,7 @@ protected:
 	World *_world;
 	Common::String _name, _backgroundName;
 	PathFindingShape _floors[2];
-	bool _fixedCameraOnEntering;
+	bool _fixedCameraOnEntering = false;
 	int8 _activeFloorI = -1;
 	int _musicId = -1;
 	uint8
@@ -175,6 +175,9 @@ enum class GlobalAnimationKind {
 	Count
 };
 
+constexpr char kNoXORKey = 0;
+constexpr char kEmbeddedXORKey = -128;
+
 class World final {
 public:
 	~World();


Commit: a16216b86af4be99cc18274075ecbe514eec5be0
    https://github.com/scummvm/scummvm/commit/a16216b86af4be99cc18274075ecbe514eec5be0
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:25+01:00

Commit Message:
ALCACHOFA: Simplify room reading with V1 vs V3

Changed paths:
    engines/alcachofa/objects.h
    engines/alcachofa/rooms.cpp
    engines/alcachofa/rooms.h


diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 0e870ea333c..ae5125b352d 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -581,7 +581,7 @@ protected:
 	void onArrived() override;
 
 private:
-	friend class InventoryV3;
+	friend class Inventory;
 	friend struct DialogMenuTask;
 	Item *getItemByName(const Common::String &name) const;
 	void drawInner();
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 003fcef4203..2e61097e8d5 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -34,7 +34,12 @@ namespace Alcachofa {
 
 Room::Room(World *world) : _world(world) {}
 
-RoomV3::RoomV3(World *world, SeekableReadStream &stream) : RoomV3(world, stream, false) {}
+Room::Room(World *world, SeekableReadStream &stream) : _world(world) {
+	if (g_engine->isV1())
+		readRoomV1(stream, true);
+	else
+		readRoomV3(stream, false);
+}
 
 static ObjectBase *readRoomObject(Room *room, const String &type, SeekableReadStream &stream) {
 	if (type == ObjectBase::kClassName)
@@ -89,8 +94,18 @@ static ObjectBase *readRoomObject(Room *room, const String &type, SeekableReadSt
 		return nullptr; // handled in Room::Room
 }
 
-RoomV3::RoomV3(World *world, SeekableReadStream &stream, bool hasUselessByte)
-	: Room(world) {
+void Room::readRoomV1(SeekableReadStream &stream, bool readObjects) {
+	_name = readVarString(stream);
+	_backgroundName = readVarString(stream);
+	_musicId = (int)stream.readByte();
+	_characterAlphaTint = stream.readByte();
+	skipVarString(stream);
+
+	if (readObjects)
+		readObjectsAndBackground(stream, kBaseScale);
+}
+
+void Room::readRoomV3(SeekableReadStream &stream, bool hasUselessByte) {
 	_name = readVarString(stream);
 	_backgroundName = _name;
 	_musicId = (int)stream.readByte();
@@ -132,23 +147,10 @@ void Room::readObjectsAndBackground(SeekableReadStream &stream, int16 background
 		_activeFloorI = 0;
 }
 
-RoomV1::RoomV1(World *world, SeekableReadStream &stream, bool readObjects)
-	: Room(world) {
-	_name = readVarString(stream);
-	_backgroundName = readVarString(stream);
-	_musicId = (int)stream.readByte();
-	_characterAlphaTint = stream.readByte();
-	skipVarString(stream);
-
-	if (readObjects)
-		readObjectsAndBackground(stream, kBaseScale);
-}
-
-RoomV1WithFloor::RoomV1WithFloor(World *world, SeekableReadStream &stream)
-	: RoomV1(world, stream, false) {
+RoomWithFloor::RoomWithFloor(World *world, SeekableReadStream &stream) : Room(world) {
+	readRoomV1(stream, false);
 	_floors[0] = PathFindingShape(stream);
 	_floors[1] = PathFindingShape(stream);
-
 	readObjectsAndBackground(stream, kBaseScale);
 }
 
@@ -350,8 +352,9 @@ ShapeObject *Room::getSelectedObject(ShapeObject *best) const {
 	return best;
 }
 
-OptionsMenu::OptionsMenu(World *world, SeekableReadStream &stream)
-	: RoomV3(world, stream, true) {}
+OptionsMenu::OptionsMenu(World *world, SeekableReadStream &stream) : Room(world) {
+	readRoomV3(stream, true);
+}
 
 bool OptionsMenu::updateInput() {
 	if (!Room::updateInput())
@@ -384,20 +387,28 @@ void OptionsMenu::clearLastSelectedObject() {
 	_lastSelectedObject = nullptr;
 }
 
-ConnectMenu::ConnectMenu(World *world, SeekableReadStream &stream)
-	: RoomV3(world, stream, true) {}
+ConnectMenu::ConnectMenu(World *world, SeekableReadStream &stream) : Room(world) {
+	readRoomV3(stream, true);
+}
 
-ListenMenu::ListenMenu(World *world, SeekableReadStream &stream)
-	: RoomV3(world, stream, true) {}
+ListenMenu::ListenMenu(World *world, SeekableReadStream &stream) : Room(world) {
+	readRoomV3(stream, true);
+}
 
-InventoryV3::InventoryV3(World *world, SeekableReadStream &stream)
-	: RoomV3(world, stream, true) {}
+Inventory::Inventory(World *world, SeekableReadStream &stream) : Room(world) {
+	if (g_engine->isV1()) {
+		readRoomV1(stream, false);
+		stream.skip(1); // denoted as "sinusar" but unused
+		readObjectsAndBackground(stream, kBaseScale);
+	} else
+		readRoomV3(stream, true);
+}
 
-InventoryV3::~InventoryV3() {
+Inventory::~Inventory() {
 	// No need to delete items, they are room objects and thus deleted in Room::~Room
 }
 
-bool InventoryV3::updateInput() {
+bool Inventory::updateInput() {
 	auto &player = g_engine->player();
 	auto &input = g_engine->input();
 	auto *hoveredItem = getHoveredItem();
@@ -440,7 +451,7 @@ bool InventoryV3::updateInput() {
 	return player.currentRoom() == this;
 }
 
-Item *InventoryV3::getHoveredItem() {
+Item *Inventory::getHoveredItem() {
 	auto mousePos = g_engine->input().mousePos2D();
 	for (auto item : _items) {
 		if (!item->isEnabled())
@@ -460,7 +471,7 @@ Item *InventoryV3::getHoveredItem() {
 	return nullptr;
 }
 
-void InventoryV3::initItems() {
+void Inventory::initItems() {
 	auto &mortadelo = world().mortadelo();
 	auto &filemon = world().filemon();
 	for (auto object : _objects) {
@@ -473,14 +484,14 @@ void InventoryV3::initItems() {
 	}
 }
 
-void InventoryV3::updateItemsByActiveCharacter() {
+void Inventory::updateItemsByActiveCharacter() {
 	auto *character = g_engine->player().activeCharacter();
 	assert(character != nullptr);
 	for (auto *item : _items)
 		item->toggle(character->hasItem(item->name()));
 }
 
-void InventoryV3::drawAsOverlay(int32 scrollY) {
+void Inventory::drawAsOverlay(int32 scrollY) {
 	for (auto object : _objects) {
 		auto graphic = object->graphic();
 		if (graphic == nullptr)
@@ -498,7 +509,7 @@ void InventoryV3::drawAsOverlay(int32 scrollY) {
 	}
 }
 
-void InventoryV3::open() {
+void Inventory::open() {
 	g_engine->camera().backup(1);
 	g_engine->player().changeRoom(name(), true);
 	updateItemsByActiveCharacter();
@@ -509,12 +520,6 @@ void InventoryV3::close() {
 	g_engine->globalUI().startClosingInventory();
 }
 
-InventoryV1::InventoryV1(World *world, SeekableReadStream &stream)
-	: RoomV1(world, stream, false) {
-	stream.skip(1); // denoted as "sinusar" but unused
-	readObjectsAndBackground(stream, kBaseScale);
-}
-
 void Room::debugPrint(bool withObjects) const {
 	auto &console = g_engine->console();
 	console.debugPrintf("  %s\n", _name.c_str());
@@ -547,7 +552,7 @@ void World::load() {
 	_globalRoom = getRoomByName("GLOBAL");
 	if (_globalRoom == nullptr)
 		error("Could not find GLOBAL room");
-	_inventory = dynamic_cast<InventoryV3 *>(getRoomByName("INVENTARIO"));
+	_inventory = dynamic_cast<Inventory *>(getRoomByName("INVENTARIO"));
 	if (_inventory == nullptr)
 		error("Could not find INVENTARIO");
 	_filemon = dynamic_cast<MainCharacter *>(_globalRoom->getObjectByName("FILEMON"));
@@ -666,32 +671,20 @@ const char *World::getDialogLine(int32 dialogId) const {
 	return _dialogLines[dialogId];
 }
 
-static Room *readRoomV3(World *world, SeekableReadStream &stream) {
+static Room *readRoom(World *world, SeekableReadStream &stream) {
 	const auto type = readVarString(stream);
 	if (type == Room::kClassName)
-		return new RoomV3(world, stream);
+		return new Room(world, stream);
+	else if (type == RoomWithFloor::kClassName)
+		return new RoomWithFloor(world, stream);
 	else if (type == OptionsMenu::kClassName)
 		return new OptionsMenu(world, stream);
 	else if (type == ConnectMenu::kClassName)
 		return new ConnectMenu(world, stream);
 	else if (type == ListenMenu::kClassName)
 		return new ListenMenu(world, stream);
-	else if (type == InventoryV3::kClassName)
-		return new InventoryV3(world, stream);
-	else {
-		g_engine->game().unknownRoomType(type);
-		return nullptr;
-	}
-}
-
-static Room *readRoomV1(World *world, SeekableReadStream &stream) {
-	const auto type = readVarString(stream);
-	if (type == Room::kClassName)
-		return new RoomV1(world, stream);
-	else if (type == RoomV1WithFloor::kClassName)
-		return new RoomV1WithFloor(world, stream);
-	else if (type == InventoryV1::kClassName)
-		return new InventoryV1(world, stream);
+	else if (type == Inventory::kClassName)
+		return new Inventory(world, stream);
 	else {
 		g_engine->game().unknownRoomType(type);
 		return nullptr;
@@ -727,7 +720,7 @@ bool World::loadWorldFileV3(const char *path) {
 	for (int i = 0; i < (int)GlobalAnimationKind::Count; i++)
 		_globalAnimations[i] = readFileRef(file);
 
-	readRooms(readRoomV3, file);
+	readRooms(file);
 
 	return true;
 }
@@ -772,12 +765,12 @@ bool World::loadWorldFileV1(const char *path) {
 	readGlobalAnim(GlobalAnimationKind::FilemonIcon, GlobalAnimationKind::FilemonDisabledIcon);
 	readGlobalAnim(GlobalAnimationKind::InventoryIcon, GlobalAnimationKind::InventoryDisabledIcon);
 
-	readRooms(readRoomV1, *file);
+	readRooms(*file);
 	_files.emplace_back(move(file));
 	return true;
 }
 
-void World::readRooms(World::ReadRoomFunc readRoom, File &file) {
+void World::readRooms(File &file) {
 	uint32 roomEnd = file.readUint32LE();
 	while (roomEnd > 0) {
 		Room *room = readRoom(this, file);
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index 8ad415d0e33..442f21c75f3 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -31,6 +31,7 @@ class World;
 class Room {
 public:
 	static constexpr const char *kClassName = "CHabitacion";
+	Room(World *world, Common::SeekableReadStream &stream);
 	virtual ~Room();
 
 	inline World &world() { return *_world; }
@@ -65,6 +66,8 @@ public:
 
 protected:
 	Room(World *world);
+	void readRoomV1(Common::SeekableReadStream &stream, bool readObjects);
+	void readRoomV3(Common::SeekableReadStream &stream, bool hasUselessByte);
 	void readObjectsAndBackground(Common::SeekableReadStream &stream, int16 backgroundScale);
 	void updateScripts();
 	void updateRoomBounds();
@@ -87,26 +90,14 @@ protected:
 	Common::Array<ObjectBase *> _objects;
 };
 
-class RoomV3 : public Room {
-public:
-	RoomV3(World *world, Common::SeekableReadStream &stream);
-
-protected:
-	RoomV3(World *world, Common::SeekableReadStream &stream, bool hasUselessByte);
-};
-
-class RoomV1 : public Room {
-public:
-	RoomV1(World *world, Common::SeekableReadStream &stream, bool readObjects = true);
-};
-
-class RoomV1WithFloor final : public RoomV1 {
+// only used for V1 where Rooms by default have no floor
+class RoomWithFloor final : public Room {
 public:
 	static constexpr const char *kClassName = "CHabitacionConSuelo";
-	RoomV1WithFloor(World *world, Common::SeekableReadStream &stream);
+	RoomWithFloor(World *world, Common::SeekableReadStream &stream);
 };
 
-class OptionsMenu final : public RoomV3 {
+class OptionsMenu final : public Room {
 public:
 	static constexpr const char *kClassName = "CHabitacionMenuOpciones";
 	OptionsMenu(World *world, Common::SeekableReadStream &stream);
@@ -123,23 +114,23 @@ private:
 	SlideButton *_currentSlideButton = nullptr;
 };
 
-class ConnectMenu final : public RoomV3 {
+class ConnectMenu final : public Room {
 public:
 	static constexpr const char *kClassName = "CHabitacionConectar";
 	ConnectMenu(World *world, Common::SeekableReadStream &stream);
 };
 
-class ListenMenu final : public RoomV3 {
+class ListenMenu final : public Room {
 public:
 	static constexpr const char *kClassName = "CHabitacionEsperar";
 	ListenMenu(World *world, Common::SeekableReadStream &stream);
 };
 
-class InventoryV3 final : public RoomV3 {
+class Inventory final : public Room {
 public:
 	static constexpr const char *kClassName = "CInventario";
-	InventoryV3(World *world, Common::SeekableReadStream &stream);
-	~InventoryV3() override;
+	Inventory(World *world, Common::SeekableReadStream &stream);
+	~Inventory() override;
 
 	bool updateInput() override;
 
@@ -155,12 +146,6 @@ private:
 	Common::Array<Item *> _items;
 };
 
-class InventoryV1 final : public RoomV1 {
-public:
-	static constexpr const char *kClassName = "CInventario";
-	InventoryV1(World *world, Common::SeekableReadStream &stream);
-};
-
 enum class GlobalAnimationKind {
 	GeneralFont = 0,
 	DialogFont,
@@ -189,7 +174,7 @@ public:
 	inline RoomIterator beginRooms() const { return _rooms.begin(); }
 	inline RoomIterator endRooms() const { return _rooms.end(); }
 	inline Room &globalRoom() const { assert(_globalRoom != nullptr); return *_globalRoom; }
-	inline InventoryV3 &inventory() const { assert(_inventory != nullptr); return *_inventory; }
+	inline Inventory &inventory() const { assert(_inventory != nullptr); return *_inventory; }
 	inline MainCharacter &filemon() const { assert(_filemon != nullptr); return *_filemon; }
 	inline MainCharacter &mortadelo() const { assert(_mortadelo != nullptr);  return *_mortadelo; }
 	inline GameFileReference scriptFileRef() const { return _scriptFileRef; }
@@ -218,10 +203,9 @@ public:
 	Common::ScopedPtr<Common::SeekableReadStream> openFileRef(const GameFileReference &ref) const;
 
 private:
-	using ReadRoomFunc = Room *(*)(World *, Common::SeekableReadStream &);
 	bool loadWorldFileV3(const char *path);
 	bool loadWorldFileV1(const char *path);
-	void readRooms(ReadRoomFunc readRoom, Common::File &file);
+	void readRooms(Common::File &file);
 	void loadLocalizedNames();
 	void loadDialogLines();
 
@@ -236,7 +220,7 @@ private:
 	Common::String _initScriptName;
 	GameFileReference _scriptFileRef;
 	Room *_globalRoom;
-	InventoryV3 *_inventory;
+	Inventory *_inventory;
 	MainCharacter *_filemon, *_mortadelo;
 	uint8 _loadedMapCount = 0;
 	Common::HashMap<const char *, const char *,


Commit: 7000df5f76dc469843a153a131602ae8dff27414
    https://github.com/scummvm/scummvm/commit/7000df5f76dc469843a153a131602ae8dff27414
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:26+01:00

Commit Message:
ALCACHOFA: Support V1 script files

Changed paths:
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index f6b277c84ed..5d777730431 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -44,57 +44,62 @@ ScriptInstruction::ScriptInstruction(ReadStream &stream)
 	, _arg(stream.readSint32LE()) {}
 
 Script::Script() {
-	File file;
-	if (!file.open("script/SCRIPT.COD"))
+	auto file = g_engine->world().openFileRef(g_engine->world().scriptFileRef());
+	if (!file)
 		error("Could not open script");
 
-	uint32 stringBlobSize = file.readUint32LE();
-	uint32 memorySize = file.readUint32LE();
+	uint32 stringBlobSize = file->readUint32LE();
+	uint32 memorySize = g_engine->isV1() ? 0 : file->readUint32LE();
 	_strings = SpanOwner<Span<char>>({ new char[stringBlobSize], stringBlobSize });
-	if (file.read(&_strings[0], stringBlobSize) != stringBlobSize)
+	if (file->read(&_strings[0], stringBlobSize) != stringBlobSize)
 		error("Could not read script string blob");
 	if (_strings[stringBlobSize - 1] != 0)
 		error("String blob does not end with null terminator");
 
-	if (memorySize % sizeof(int32) != 0)
-		error("Unexpected size of script memory");
-	_variables.resize(memorySize / sizeof(int32), 0);
-
-	uint32 variableCount = file.readUint32LE();
+	uint32 variableCount = file->readUint32LE();
 	for (uint32 i = 0; i < variableCount; i++) {
-		String name = readVarString(file);
-		uint32 offset = file.readUint32LE();
+		String name = readVarString(*file);
+		uint32 offset = file->readUint32LE();
 		if (offset % sizeof(int32) != 0)
 			error("Unaligned variable offset");
 		_variableNames[name] = offset / 4;
 	}
 
-	uint32 procedureCount = file.readUint32LE();
+	if (memorySize == 0) {
+		// in V1 the memory size is implicitly stored in the variables
+		for (const auto &variable : _variableNames)
+			memorySize = MAX(memorySize, variable._value + (uint32)sizeof(int32));
+	}
+	if (memorySize % sizeof(int32) != 0)
+		error("Unexpected size of script memory");
+	_variables.resize(memorySize / sizeof(int32), 0);
+
+	uint32 procedureCount = file->readUint32LE();
 	for (uint32 i = 0; i < procedureCount; i++) {
-		String name = readVarString(file);
-		uint32 offset = file.readUint32LE();
-		file.skip(sizeof(uint32));
+		String name = readVarString(*file);
+		uint32 offset = file->readUint32LE();
+		file->skip(sizeof(uint32));
 		_procedures[name] = offset - 1; // originally one-based, but let's not.
 	}
 
-	uint32 behaviorCount = file.readUint32LE();
+	uint32 behaviorCount = file->readUint32LE();
 	for (uint32 i = 0; i < behaviorCount; i++) {
-		String behaviorName = readVarString(file) + '/';
-		variableCount = file.readUint32LE(); // not used by the original game
+		String behaviorName = readVarString(*file) + '/';
+		variableCount = file->readUint32LE(); // not used by the original game
 		assert(variableCount == 0);
-		procedureCount = file.readUint32LE();
+		procedureCount = file->readUint32LE();
 		for (uint32 j = 0; j < procedureCount; j++) {
-			String name = behaviorName + readVarString(file);
-			uint32 offset = file.readUint32LE();
-			file.skip(sizeof(uint32));
+			String name = behaviorName + readVarString(*file);
+			uint32 offset = file->readUint32LE();
+			file->skip(sizeof(uint32));
 			_procedures[name] = offset - 1;
 		}
 	}
 
-	uint32 instructionCount = file.readUint32LE();
+	uint32 instructionCount = file->readUint32LE();
 	_instructions.reserve(instructionCount);
 	for (uint32 i = 0; i < instructionCount; i++)
-		_instructions.push_back(ScriptInstruction(file));
+		_instructions.push_back(ScriptInstruction(*file));
 }
 
 static void syncAsSint32LE(Serializer &s, int32 &value) {


Commit: ed18828503109501514135c65b93a61379b203ed
    https://github.com/scummvm/scummvm/commit/ed18828503109501514135c65b93a61379b203ed
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:26+01:00

Commit Message:
ALCACHOFA: Add script translation maps

Changed paths:
    engines/alcachofa/game-movie-adventure-original.cpp
    engines/alcachofa/game.h


diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index eef4837838e..d249c3bce22 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -29,6 +29,78 @@ using namespace Common;
 
 namespace Alcachofa {
 
+static constexpr const ScriptOp kScriptOpMap[] = {
+	ScriptOp::Nop,
+	ScriptOp::Dup,
+	ScriptOp::PushAddr,
+	ScriptOp::PushValue,
+	ScriptOp::Deref,
+	ScriptOp::Nop,
+	ScriptOp::Pop1,
+	ScriptOp::Store,
+	ScriptOp::Nop,
+	ScriptOp::Nop,
+	ScriptOp::PushDynAddr,
+	ScriptOp::Nop,
+	ScriptOp::ScriptCall,
+	ScriptOp::KernelCall,
+	ScriptOp::JumpIfFalse,
+	ScriptOp::JumpIfTrue,
+	ScriptOp::Jump,
+	ScriptOp::Nop,
+	ScriptOp::Nop,
+	ScriptOp::Nop,
+	ScriptOp::Nop,
+	ScriptOp::Nop,
+	ScriptOp::Add,
+	ScriptOp::Nop,
+	ScriptOp::Nop,
+	ScriptOp::Nop,
+	ScriptOp::Nop,
+	ScriptOp::Nop,
+	ScriptOp::Equals,
+	ScriptOp::NotEquals,
+	ScriptOp::BitAnd,
+	ScriptOp::BitOr,
+	ScriptOp::Nop,
+	ScriptOp::Nop,
+	ScriptOp::Nop,
+	ScriptOp::Nop,
+	ScriptOp::ReturnVoid
+};
+
+static constexpr const ScriptKernelTask kScriptKernelTaskMap[] = {
+	ScriptKernelTask::Nop,
+	ScriptKernelTask::SayText,
+	ScriptKernelTask::Go,
+	ScriptKernelTask::Delay,
+	ScriptKernelTask::PlaySound,
+	ScriptKernelTask::FadeIn,
+	ScriptKernelTask::FadeOut,
+	ScriptKernelTask::Put,
+	ScriptKernelTask::ChangeRoom,
+	ScriptKernelTask::PlayVideo,
+	ScriptKernelTask::StopAndTurn,
+	ScriptKernelTask::StopAndTurnMe,
+	ScriptKernelTask::On,
+	ScriptKernelTask::Off,
+	ScriptKernelTask::Pickup,
+	ScriptKernelTask::Animate,
+	ScriptKernelTask::SheriffTakesCharacter,
+	ScriptKernelTask::ChangeCharacter,
+	ScriptKernelTask::LerpCamToObjectKeepingZ,
+	ScriptKernelTask::Drop,
+	ScriptKernelTask::ChangeDoor,
+	ScriptKernelTask::Disguise,
+	ScriptKernelTask::ToggleRoomFloor,
+	ScriptKernelTask::SetDialogLineReturn,
+	ScriptKernelTask::DialogMenu,
+	ScriptKernelTask::ChangeCharacterRoom,
+	ScriptKernelTask::PlayMusic,
+	ScriptKernelTask::StopMusic,
+	ScriptKernelTask::WaitForMusicToEnd
+};
+
 class GameMovieAdventureOriginal : public Game {
 public:
 	GameMovieAdventureOriginal() {
@@ -70,6 +142,14 @@ public:
 	char getTextFileKey() override {
 		return kNoXORKey;
 	}
+
+	Span<const ScriptOp> getScriptOpMap() override {
+		return { kScriptOpMap, ARRAYSIZE(kScriptOpMap) };
+	}
+
+	Span<const ScriptKernelTask> getScriptKernelTaskMap() override {
+		return { kScriptKernelTaskMap, ARRAYSIZE(kScriptKernelTaskMap) };
+	}
 };
 
 Game *Game::createForMovieAdventureOriginal() {
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index d559c3b1336..bd562331bf5 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -61,6 +61,8 @@ public:
 	virtual const char *getDialogFileName() = 0;
 	virtual const char *getObjectFileName() = 0;
 	virtual char getTextFileKey() = 0;
+	virtual Common::Span<const ScriptOp> getScriptOpMap() = 0;
+	virtual Common::Span<const ScriptKernelTask> getScriptKernelTaskMap() = 0;
 
 	virtual bool doesRoomHaveBackground(const Room *room);
 	virtual void unknownRoomObject(const Common::String &type);


Commit: c6c0723605ac3a07a768d84535db22903c00ef72
    https://github.com/scummvm/scummvm/commit/c6c0723605ac3a07a768d84535db22903c00ef72
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:26+01:00

Commit Message:
ALCACHOFA: Add reading of V1 images

Changed paths:
    engines/alcachofa/common.cpp
    engines/alcachofa/common.h
    engines/alcachofa/graphics.cpp
    engines/alcachofa/graphics.h
    engines/alcachofa/shape.cpp


diff --git a/engines/alcachofa/common.cpp b/engines/alcachofa/common.cpp
index c37ff0ca330..cf8e6ab65e9 100644
--- a/engines/alcachofa/common.cpp
+++ b/engines/alcachofa/common.cpp
@@ -160,7 +160,11 @@ bool readBool(ReadStream &stream) {
 	return stream.readByte() != 0;
 }
 
-Point readPoint(ReadStream &stream) {
+Point readPoint16(ReadStream &stream) {
+	return { stream.readSint16LE(), stream.readSint16LE() };
+}
+
+Point readPoint32(ReadStream &stream) {
 	return { (int16)stream.readSint32LE(), (int16)stream.readSint32LE() };
 }
 
diff --git a/engines/alcachofa/common.h b/engines/alcachofa/common.h
index cbef429d523..c30e05631ed 100644
--- a/engines/alcachofa/common.h
+++ b/engines/alcachofa/common.h
@@ -137,7 +137,8 @@ Math::Vector2d as2D(const Math::Vector3d &v);
 Math::Vector2d as2D(Common::Point p);
 
 bool readBool(Common::ReadStream &stream);
-Common::Point readPoint(Common::ReadStream &stream);
+Common::Point readPoint16(Common::ReadStream &stream);
+Common::Point readPoint32(Common::ReadStream &stream);
 Common::String readVarString(Common::ReadStream &stream);
 void skipVarString(Common::SeekableReadStream &stream);
 void syncPoint(Common::Serializer &serializer, Common::Point &point);
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 42fe7a9eecf..0206e2f890c 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -59,6 +59,92 @@ void IDebugRenderer::debugShape(const Shape &shape, Color color) {
 	}
 }
 
+ImageV1::ImageV1(SeekableReadStream &stream) {
+	_drawOffset = readPoint16(stream);
+	stream.skip(4); // another point
+	_size = readPoint16(stream);
+	uint32 pixelCount = stream.readUint32LE();
+	stream.skip(5); // yet another point and some flag
+	bool isPaletted = readBool(stream);
+
+	// for segments we could either scan the file first to sum the total segment count
+	// or we can not use reserve and rely on the capacity doubling as heuristic
+	// using reserve will guarantee we always reallocate _segments
+	scumm_assert(_size.x >= 0 && _size.y >= 0);
+	_lines.reserve(_size.y);
+	for (int16 y = 0; y < _size.y; y++) {
+		uint16 segmentCount = stream.readUint16LE();
+		_lines.push_back({ _segments.size(), _segments.size() + segmentCount });
+		for (uint16 i = 0; i < segmentCount; i++) {
+			_segments.push_back({
+				stream.readUint16LE(),
+				stream.readUint16LE(),
+				stream.readUint32LE() });
+		}
+	}
+
+	_pixels.resize(pixelCount);
+	stream.read(_pixels.data(), pixelCount);
+	if (isPaletted) {
+		_palette.resize(PALETTE_SIZE);
+		stream.read(_palette.data(), PALETTE_SIZE);
+	}
+	scumm_assert(!stream.err());
+}
+
+ManagedSurface *ImageV1::render() const {
+	ManagedSurface *surface = new ManagedSurface(_size.x, _size.y, PixelFormat::createFormatARGB32());
+	render(*surface, {});
+	return surface;
+}
+
+void ImageV1::render(ManagedSurface &target, Point dstOffset) const {
+	assert(target.format == PixelFormat::createFormatARGB32());
+	Point srcOffset = {};
+	if (dstOffset.x < 0) {
+		srcOffset.x = -dstOffset.x;
+		dstOffset.x = 0;
+	}
+	if (dstOffset.y < 0) {
+		srcOffset.y = -dstOffset.y;
+		dstOffset.y = 0;
+	}
+	if (dstOffset.x >= target.w || dstOffset.y >= target.h ||
+		srcOffset.x >= _size.x || srcOffset.y >= _size.y)
+		return;
+	const int maxSrcY = MIN((int)_size.y, target.h - dstOffset.y);
+	const uint bpp = _palette.empty() ? 3 : 1;
+
+	int dstY = dstOffset.y;
+	for (int srcY = srcOffset.y; srcY < maxSrcY; srcY++, dstY++) {
+		const auto line = _lines[srcY];
+		int dstX = dstOffset.x;
+		for (uint segmentI = line._start; segmentI < line._end; segmentI++) {
+			const auto segment = _segments[segmentI];
+			dstX += segment._xOffset;
+			if (dstX >= target.w)
+				break;
+			if (dstX + segment._width < 0)
+				continue;
+			uint srcPixelI = segment._dataOffset;
+			if (dstX < 0)
+				srcPixelI += -dstX * bpp;
+			uint width = MIN((int)segment._width, target.w - dstX);
+
+			byte* dstPixel = (byte*)target.getBasePtr(dstX, dstY);
+			for (uint x = 0; x < width; x++) {
+				const byte* srcPixel = _palette.empty()
+					? _pixels.data() + srcPixelI
+					: _palette.data() + 3 * _pixels[srcPixelI];
+				memcpy(dstPixel, srcPixel, 3);
+				dstPixel[3] = 0xff;
+				dstPixel += 4;
+				srcPixelI += bpp;
+			}
+		}
+	}
+}
+
 AnimationBase::AnimationBase(GameFileReference fileRef, AnimationFolder folder)
 	: _fileRef(move(fileRef))
 	, _folder(folder) {}
@@ -103,16 +189,28 @@ void AnimationBase::load() {
 	}
 	// Reading the images is a major bottleneck in loading, buffering helps a lot with that
 	ScopedPtr<SeekableReadStream> stream(wrapBufferedSeekableReadStream(&file, file.size(), DisposeAfterUse::NO));
+	if (g_engine->isV1())
+		readV1(*stream);
+	else
+		readV3(*stream);
 
-	uint spriteCount = stream->readUint32LE();
+	_isLoaded = true;
+}
+
+void AnimationBase::readV1(SeekableReadStream &stream) {
+	assert(false);
+}
+
+void AnimationBase::readV3(SeekableReadStream &stream) {
+	uint spriteCount = stream.readUint32LE();
 	assert(spriteCount < kMaxSpriteIDs);
 	_spriteBases.reserve(spriteCount);
 
-	uint imageCount = stream->readUint32LE();
+	uint imageCount = stream.readUint32LE();
 	_images.reserve(imageCount);
 	_imageOffsets.reserve(imageCount);
 	for (uint i = 0; i < imageCount; i++) {
-		_images.push_back(readImage(*stream));
+		_images.push_back(readImageV3(stream));
 	}
 
 	// an inconsistency, maybe a historical reason:
@@ -120,37 +218,35 @@ void AnimationBase::load() {
 	// have to be contiguous we do not need to do that ourselves.
 	// but let's check in Debug to be sure
 	for (uint i = 0; i < spriteCount; i++) {
-		_spriteBases.push_back(stream->readUint32LE());
+		_spriteBases.push_back(stream.readUint32LE());
 		assert(_spriteBases.back() < imageCount);
 	}
 #ifdef ALCACHOFA_DEBUG
 	for (uint i = spriteCount; i < kMaxSpriteIDs; i++)
-		assert(stream->readSint32LE() == 0);
+		assert(stream.readSint32LE() == 0);
 #else
-	stream->skip(sizeof(int32) * (kMaxSpriteIDs - spriteCount));
+	stream.skip(sizeof(int32) * (kMaxSpriteIDs - spriteCount));
 #endif
 
 	for (uint i = 0; i < imageCount; i++)
-		_imageOffsets.push_back(readPoint(*stream));
+		_imageOffsets.push_back(readPoint32(stream));
 	for (uint i = 0; i < kMaxSpriteIDs; i++)
-		_spriteIndexMapping[i] = stream->readSint32LE();
+		_spriteIndexMapping[i] = stream.readSint32LE();
 
-	uint frameCount = stream->readUint32LE();
+	uint frameCount = stream.readUint32LE();
 	_frames.reserve(frameCount);
 	_spriteOffsets.reserve(frameCount * spriteCount);
 	_totalDuration = 0;
 	for (uint i = 0; i < frameCount; i++) {
 		for (uint j = 0; j < spriteCount; j++)
-			_spriteOffsets.push_back(stream->readUint32LE());
+			_spriteOffsets.push_back(stream.readUint32LE());
 		AnimationFrame frame;
-		frame._center = readPoint(*stream);
-		frame._offset = readPoint(*stream);
-		frame._duration = stream->readUint32LE();
+		frame._center = readPoint32(stream);
+		frame._offset = readPoint32(stream);
+		frame._duration = stream.readUint32LE();
 		_frames.push_back(frame);
 		_totalDuration += frame._duration;
 	}
-
-	_isLoaded = true;
 }
 
 void AnimationBase::freeImages() {
@@ -168,7 +264,7 @@ void AnimationBase::freeImages() {
 	_isLoaded = false;
 }
 
-ManagedSurface *AnimationBase::readImage(SeekableReadStream &stream) const {
+ManagedSurface *AnimationBase::readImageV3(SeekableReadStream &stream) const {
 	SeekableSubReadStream subStream(&stream, stream.pos(), stream.size());
 	TGADecoder decoder;
 	if (!decoder.loadStream(subStream))
@@ -516,7 +612,7 @@ Graphic::Graphic() {}
 
 Graphic::Graphic(SeekableReadStream &stream) {
 	if (g_engine->isV1()) {
-		_topLeft = readPoint(stream);
+		_topLeft = readPoint32(stream);
 		_scale = stream.readSint16LE();
 		_order = (int8)stream.readSint16LE();
 	} else {
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 1cb5da94f8f..b2c86c7e8d5 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -122,6 +122,31 @@ public:
 	}
 };
 
+// as futureoptimization one could actually store these instead of converting to a surface
+// this will reduce memory usage and might reduce render times on software renderers
+class ImageV1 {
+public:
+	ImageV1(Common::SeekableReadStream &stream);
+
+	void render(Graphics::ManagedSurface &target, Common::Point offset) const;
+	Graphics::ManagedSurface *render() const;
+	
+private:
+	struct Segment {
+		uint16 _xOffset, _width;
+		uint32 _dataOffset;
+	};
+	struct Line { // indices into _segments
+		uint _start, _end;
+	};
+
+	Common::Point _drawOffset, _size;
+	Common::Array<byte> _pixels;
+	Common::Array<byte> _palette;
+	Common::Array<Segment> _segments;
+	Common::Array<Line> _lines;
+};
+
 enum class AnimationFolder {
 	Animations,
 	Masks,
@@ -154,7 +179,9 @@ protected:
 	void load();
 	void loadMissingAnimation();
 	void freeImages();
-	Graphics::ManagedSurface *readImage(Common::SeekableReadStream &stream) const;
+	void readV1(Common::SeekableReadStream &stream);
+	void readV3(Common::SeekableReadStream &stream);
+	Graphics::ManagedSurface *readImageV3(Common::SeekableReadStream &stream) const;
 	Common::Point imageSize(int32 imageI) const;
 	inline bool isLoaded() const { return _isLoaded; }
 
@@ -170,10 +197,11 @@ protected:
 	bool _isLoaded = false;
 	uint32 _totalDuration = 0;
 
-	int32 _spriteIndexMapping[kMaxSpriteIDs] = { 0 };
+	int32 _spriteIndexMapping[kMaxSpriteIDs] = { 0 }; ///< establishes render order back-to-front
 	Common::Array<uint32>
 		_spriteOffsets, ///< index offset per sprite and animation frame
 		_spriteBases; ///< base index per sprite
+	Common::Array<bool> _spriteEnabled;
 	Common::Array<AnimationFrame> _frames;
 	Common::Array<Graphics::ManagedSurface *> _images; ///< will contain nullptr for fake images
 	Common::Array<Common::Point> _imageOffsets;
diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
index 3a2c1fda964..a0bde018bd1 100644
--- a/engines/alcachofa/shape.cpp
+++ b/engines/alcachofa/shape.cpp
@@ -270,7 +270,7 @@ Shape::Shape(ReadStream &stream) {
 			? stream.readByte()
 			: pointsPerPolygon;
 		for (int j = 0; j < pointCount; j++)
-			_points.push_back(readPoint(stream));
+			_points.push_back(readPoint32(stream));
 		addPolygon(pointCount);
 	}
 }
@@ -348,7 +348,7 @@ PathFindingShape::PathFindingShape(ReadStream &stream) {
 
 	for (int i = 0; i < polygonCount; i++) {
 		for (uint j = 0; j < kPointsPerPolygon; j++)
-			_points.push_back(readPoint(stream));
+			_points.push_back(readPoint32(stream));
 		_polygonOrders.push_back(stream.readSByte());
 		for (uint j = 0; j < kPointsPerPolygon; j++)
 			_pointDepths.push_back(stream.readByte());
@@ -638,7 +638,7 @@ FloorColorShape::FloorColorShape(ReadStream &stream) {
 
 	for (int i = 0; i < polygonCount; i++) {
 		for (uint j = 0; j < kPointsPerPolygon; j++)
-			_points.push_back(readPoint(stream));
+			_points.push_back(readPoint32(stream));
 
 		// For the colors the alpha channel is not used so we store the brightness into it instead
 		// Brightness is store 0-100, but we can scale it up here


Commit: 0d9d8e1efa21122f06d7e335f2e8b5ecc2e28864
    https://github.com/scummvm/scummvm/commit/0d9d8e1efa21122f06d7e335f2e8b5ecc2e28864
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:26+01:00

Commit Message:
ALCACHOFA: Support reading V1 animations

Changed paths:
    engines/alcachofa/graphics.cpp
    engines/alcachofa/graphics.h


diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 0206e2f890c..de97be576f6 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -157,48 +157,118 @@ void AnimationBase::load() {
 	if (_isLoaded)
 		return;
 
-	assert(!_fileRef.isEmbedded());
-
-	String fullPath;
-	switch (_folder) {
-	case AnimationFolder::Animations:
-		fullPath = "Animaciones/";
-		break;
-	case AnimationFolder::Masks:
-		fullPath = "Mascaras/";
-		break;
-	case AnimationFolder::Backgrounds:
-		fullPath = "Fondos/";
-		break;
-	default:
-		assert(false && "Invalid AnimationFolder");
-		break;
-	}
-	if (_fileRef._path.size() < 4 || scumm_strnicmp(_fileRef._path.end() - 4, ".AN0", 4) != 0)
-		_fileRef._path += ".AN0";
-	fullPath += _fileRef._path;
-
-	File file;
-	if (!file.open(fullPath.c_str())) {
-		// original fallback
-		fullPath = "Mascaras/" + _fileRef._path;
-		if (!file.open(fullPath.c_str())) {
-			loadMissingAnimation();
-			return;
+	ScopedPtr<SeekableReadStream> rawStream;
+	if (_fileRef.isEmbedded())
+		rawStream = g_engine->world().openFileRef(_fileRef);
+	else {
+		// for real file paths we have to apply the folder and do some fallback
+		String fullPath;
+		switch (_folder) {
+		case AnimationFolder::Animations:
+			fullPath = "Animaciones/";
+			break;
+		case AnimationFolder::Masks:
+			fullPath = "Mascaras/";
+			break;
+		case AnimationFolder::Backgrounds:
+			fullPath = "Fondos/";
+			break;
+		default:
+			assert(false && "Invalid AnimationFolder");
+			break;
+		}
+		fullPath += _fileRef._path;
+		if (_fileRef._path.size() < 4 || scumm_strnicmp(_fileRef._path.end() - 4, ".AN0", 4) != 0)
+			fullPath += ".AN0";
+
+		File *file = new File();
+		if (!file->open(fullPath.c_str())) {
+			// original fallback
+			fullPath = "Mascaras/" + _fileRef._path;
+			if (!file->open(fullPath.c_str())) {
+				delete file;
+				file = nullptr;
+			}
 		}
+		rawStream.reset(file);
 	}
+	
+	if (rawStream == nullptr) {
+		loadMissingAnimation();
+		return;
+	}
+
 	// Reading the images is a major bottleneck in loading, buffering helps a lot with that
-	ScopedPtr<SeekableReadStream> stream(wrapBufferedSeekableReadStream(&file, file.size(), DisposeAfterUse::NO));
+	ScopedPtr<SeekableReadStream> stream(
+		wrapBufferedSeekableReadStream(rawStream.get(), rawStream->size(), DisposeAfterUse::NO));
 	if (g_engine->isV1())
 		readV1(*stream);
 	else
 		readV3(*stream);
-
 	_isLoaded = true;
 }
 
 void AnimationBase::readV1(SeekableReadStream &stream) {
-	assert(false);
+	skipVarString(stream); // another internal, unused name
+	uint spriteCount = stream.readUint32LE();
+	uint frameCount = stream.readUint32LE();
+	assert(spriteCount < kMaxSpriteIDsV1);
+	_spriteBases.reserve(spriteCount);
+	_spriteEnabled.reserve(spriteCount);
+	_spriteOffsets.reserve(spriteCount * frameCount);
+	
+	readPoint16(stream); // "totalSize", unused for now
+	stream.skip(4); // unknown
+	_totalDuration = stream.readUint32LE();
+	stream.skip(4);
+	byte alpha = stream.readByte(); // TODO: We *should* use this
+	stream.skip(8);
+
+	Array<byte> spriteOrder;
+	spriteOrder.reserve(spriteCount);
+	for (uint i = 0; i < spriteCount; i++) {
+		uint imageCount = stream.readUint32LE();
+		spriteOrder.push_back(stream.readByte());
+		_spriteEnabled.push_back(!readBool(stream));
+		stream.skip(4);
+
+		_spriteBases.push_back(_images.size());
+		for (uint j = 0; j < imageCount; j++) {
+			ImageV1 image(stream);
+			_imageOffsets.push_back(image.drawOffset());
+			_images.push_back(image.render());
+		}
+	}
+
+	// Sprite order is setup by setting up reverse index sequence and
+	// then stable sort descending by order (here: Bubblesort)
+	for (uint i = 0; i < spriteCount; i++)
+		_spriteIndexMapping[i] = (spriteCount - 1 - i);
+	for (uint i = 0; i < spriteCount; i++) {
+		bool hadChange = false;
+		for (uint j = 0; j < spriteCount - i - 1; j++) {
+			if (spriteOrder[_spriteIndexMapping[j]] < spriteOrder[_spriteIndexMapping[j + 1]])
+				SWAP(_spriteIndexMapping[j], _spriteIndexMapping[j + 1]);
+		}
+		if (!hadChange)
+			break;
+	}
+
+	for (uint i = 0; i < frameCount; i++) {
+		for (uint j = 0; j < spriteCount; j++) {
+			int imageI = stream.readSint32LE();
+			if (imageI <= 0) // we make sure that spriteBases + imageI <= 0 if the local imageI <= 0
+				imageI = -(int)_spriteOffsets.size();
+			_spriteOffsets.push_back(imageI);
+		}
+		stream.skip((kMaxSpriteIDsV1 - spriteCount) * 4);
+
+		AnimationFrame frame;
+		frame._center = readPoint16(stream);
+		frame._offset = readPoint16(stream);
+		frame._duration = stream.readUint16LE();
+		_frames.push_back(frame);
+	}
 }
 
 void AnimationBase::readV3(SeekableReadStream &stream) {
@@ -219,7 +289,7 @@ void AnimationBase::readV3(SeekableReadStream &stream) {
 	// but let's check in Debug to be sure
 	for (uint i = 0; i < spriteCount; i++) {
 		_spriteBases.push_back(stream.readUint32LE());
-		assert(_spriteBases.back() < imageCount);
+		assert((uint)_spriteBases.back() < imageCount);
 	}
 #ifdef ALCACHOFA_DEBUG
 	for (uint i = spriteCount; i < kMaxSpriteIDs; i++)
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index b2c86c7e8d5..30569469658 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -128,6 +128,8 @@ class ImageV1 {
 public:
 	ImageV1(Common::SeekableReadStream &stream);
 
+	inline Common::Point drawOffset() const { return _drawOffset; }
+
 	void render(Graphics::ManagedSurface &target, Common::Point offset) const;
 	Graphics::ManagedSurface *render() const;
 	
@@ -192,13 +194,14 @@ protected:
 		int offsetY);
 
 	static constexpr const uint kMaxSpriteIDs = 256;
+	static constexpr const uint kMaxSpriteIDsV1 = 8;
 	GameFileReference _fileRef;
 	AnimationFolder _folder;
 	bool _isLoaded = false;
 	uint32 _totalDuration = 0;
 
 	int32 _spriteIndexMapping[kMaxSpriteIDs] = { 0 }; ///< establishes render order back-to-front
-	Common::Array<uint32>
+	Common::Array<int32>
 		_spriteOffsets, ///< index offset per sprite and animation frame
 		_spriteBases; ///< base index per sprite
 	Common::Array<bool> _spriteEnabled;


Commit: b800ace2399489b18a5b3ba3f35afccbe89e034d
    https://github.com/scummvm/scummvm/commit/b800ace2399489b18a5b3ba3f35afccbe89e034d
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:26+01:00

Commit Message:
ALCACHOFA: Fix V1 animation reading

Changed paths:
    engines/alcachofa/graphics.cpp
    engines/alcachofa/graphics.h


diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index de97be576f6..1b0fae7d4c8 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -83,8 +83,9 @@ ImageV1::ImageV1(SeekableReadStream &stream) {
 		}
 	}
 
-	_pixels.resize(pixelCount);
-	stream.read(_pixels.data(), pixelCount);
+	const uint bpp = isPaletted ? 1 : 3;
+	_pixels.resize(pixelCount * bpp);
+	stream.read(_pixels.data(), pixelCount * bpp);
 	if (isPaletted) {
 		_palette.resize(PALETTE_SIZE);
 		stream.read(_palette.data(), PALETTE_SIZE);
@@ -92,14 +93,14 @@ ImageV1::ImageV1(SeekableReadStream &stream) {
 	scumm_assert(!stream.err());
 }
 
-ManagedSurface *ImageV1::render() const {
-	ManagedSurface *surface = new ManagedSurface(_size.x, _size.y, PixelFormat::createFormatARGB32());
-	render(*surface, {});
+ManagedSurface *ImageV1::render(byte alpha) const {
+	ManagedSurface *surface = new ManagedSurface(_size.x, _size.y, BlendBlit::getSupportedPixelFormat());
+	render(*surface, {}, alpha);
 	return surface;
 }
 
-void ImageV1::render(ManagedSurface &target, Point dstOffset) const {
-	assert(target.format == PixelFormat::createFormatARGB32());
+void ImageV1::render(ManagedSurface &target, Point dstOffset, byte alpha) const {
+	assert(target.format.bytesPerPixel == 4);
 	Point srcOffset = {};
 	if (dstOffset.x < 0) {
 		srcOffset.x = -dstOffset.x;
@@ -131,14 +132,13 @@ void ImageV1::render(ManagedSurface &target, Point dstOffset) const {
 				srcPixelI += -dstX * bpp;
 			uint width = MIN((int)segment._width, target.w - dstX);
 
-			byte* dstPixel = (byte*)target.getBasePtr(dstX, dstY);
+			uint32* dstPixel = (uint32*)target.getBasePtr(dstX, dstY);
 			for (uint x = 0; x < width; x++) {
 				const byte* srcPixel = _palette.empty()
 					? _pixels.data() + srcPixelI
 					: _palette.data() + 3 * _pixels[srcPixelI];
-				memcpy(dstPixel, srcPixel, 3);
-				dstPixel[3] = 0xff;
-				dstPixel += 4;
+				*dstPixel = target.format.ARGBToColor(alpha, srcPixel[0], srcPixel[1], srcPixel[2]);
+				dstPixel++;
 				srcPixelI += bpp;
 			}
 		}
@@ -209,10 +209,14 @@ void AnimationBase::load() {
 }
 
 void AnimationBase::readV1(SeekableReadStream &stream) {
+	char magic[4];
+	stream.read(magic, sizeof(magic));
+	scumm_assert(memcmp(magic, "ANI", 4) == 0);
+
 	skipVarString(stream); // another internal, unused name
 	uint spriteCount = stream.readUint32LE();
 	uint frameCount = stream.readUint32LE();
-	assert(spriteCount < kMaxSpriteIDsV1);
+	scumm_assert(spriteCount < kMaxSpriteIDsV1);
 	_spriteBases.reserve(spriteCount);
 	_spriteEnabled.reserve(spriteCount);
 	_spriteOffsets.reserve(spriteCount * frameCount);
@@ -221,7 +225,7 @@ void AnimationBase::readV1(SeekableReadStream &stream) {
 	stream.skip(4); // unknown
 	_totalDuration = stream.readUint32LE();
 	stream.skip(4);
-	byte alpha = stream.readByte(); // TODO: We *should* use this
+	byte alpha = stream.readByte();
 	stream.skip(8);
 
 	Array<byte> spriteOrder;
@@ -236,7 +240,7 @@ void AnimationBase::readV1(SeekableReadStream &stream) {
 		for (uint j = 0; j < imageCount; j++) {
 			ImageV1 image(stream);
 			_imageOffsets.push_back(image.drawOffset());
-			_images.push_back(image.render());
+			_images.push_back(image.render(alpha));
 		}
 	}
 
@@ -256,12 +260,12 @@ void AnimationBase::readV1(SeekableReadStream &stream) {
 
 	for (uint i = 0; i < frameCount; i++) {
 		for (uint j = 0; j < spriteCount; j++) {
-			int imageI = stream.readSint32LE();
+			int imageI = stream.readSByte();
 			if (imageI <= 0) // we make sure that spriteBases + imageI <= 0 if the local imageI <= 0
 				imageI = -(int)_spriteOffsets.size();
 			_spriteOffsets.push_back(imageI);
 		}
-		stream.skip((kMaxSpriteIDsV1 - spriteCount) * 4);
+		stream.skip(kMaxSpriteIDsV1 - spriteCount);
 
 		AnimationFrame frame;
 		frame._center = readPoint16(stream);
@@ -622,13 +626,13 @@ void Font::load() {
 		return;
 	AnimationBase::load();
 	// We now render all frames into a 16x16 atlas and fill up to power of two size just because it is easy here
-	// However in two out of three fonts the character 128 is massive, it looks like a bug
+	// However for V3 in two out of three fonts the character 128 is massive, it looks like a bug
 	// as we want easy regular-sized characters it is ignored
 
 	Point cellSize;
 	for (auto image : _images) {
 		assert(image != nullptr); // no fake pictures in fonts please
-		if (image == _images[128])
+		if (g_engine->isV3() && image == _images[128])
 			continue;
 		cellSize.x = MAX(cellSize.x, image->w);
 		cellSize.y = MAX(cellSize.y, image->h);
@@ -642,7 +646,7 @@ void Font::load() {
 	const float invWidth = 1.0f / atlasSurface.w;
 	const float invHeight = 1.0f / atlasSurface.h;
 	for (uint i = 0; i < _images.size(); i++) {
-		if (i == 128) continue;
+		if (g_engine->isV3() && i == 128) continue;
 
 		int offsetX = (i % 16) * cellSize.x + (cellSize.x - _images[i]->w) / 2;
 		int offsetY = (i / 16) * cellSize.y + (cellSize.y - _images[i]->h) / 2;
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 30569469658..b436e37223b 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -130,8 +130,8 @@ public:
 
 	inline Common::Point drawOffset() const { return _drawOffset; }
 
-	void render(Graphics::ManagedSurface &target, Common::Point offset) const;
-	Graphics::ManagedSurface *render() const;
+	void render(Graphics::ManagedSurface &target, Common::Point offset, byte alpha = 255) const;
+	Graphics::ManagedSurface *render(byte alpha = 255) const;
 	
 private:
 	struct Segment {


Commit: f89eb3f6a498fc48066b1656c59c827ef054b3f2
    https://github.com/scummvm/scummvm/commit/f89eb3f6a498fc48066b1656c59c827ef054b3f2
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:26+01:00

Commit Message:
ALCACHOFA: Fix script variable count

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 016804dce9a..32d56690453 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -94,6 +94,7 @@ Common::Error AlcachofaEngine::run() {
 		_scheduler.run();
 		// we run once to set the initial room, otherwise we could run into currentRoom == nullptr
 	}
+	assert(_player->currentRoom() != nullptr);
 
 	Common::Event e;
 	Graphics::FrameLimiter limiter(g_system, kDefaultFramerate, false);
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 5d777730431..59a34026ec1 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -69,6 +69,7 @@ Script::Script() {
 		// in V1 the memory size is implicitly stored in the variables
 		for (const auto &variable : _variableNames)
 			memorySize = MAX(memorySize, variable._value + (uint32)sizeof(int32));
+		memorySize *= sizeof(int32); // _variableNames already works with indices
 	}
 	if (memorySize % sizeof(int32) != 0)
 		error("Unexpected size of script memory");


Commit: 294faec0cb660999fa0ce60a1dc76ba0115d05ef
    https://github.com/scummvm/scummvm/commit/294faec0cb660999fa0ce60a1dc76ba0115d05ef
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:26+01:00

Commit Message:
ALCACHOFA: Implement differences in script instructions

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/game-movie-adventure-original.cpp
    engines/alcachofa/game-movie-adventure-special.cpp
    engines/alcachofa/game.cpp
    engines/alcachofa/game.h
    engines/alcachofa/script-debug.h
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 32d56690453..65a4a1c1e0c 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -90,7 +90,7 @@ Common::Error AlcachofaEngine::run() {
 	game().onLoadedGameFiles();
 
 	if (!tryLoadFromLauncher()) {
-		_script->createProcess(MainCharacterKind::None, "CREDITOS_INICIALES");
+		_script->createProcess(MainCharacterKind::None, _game->getInitScriptName());
 		_scheduler.run();
 		// we run once to set the initial room, otherwise we could run into currentRoom == nullptr
 	}
diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index d249c3bce22..3a11df7deaa 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -101,6 +101,38 @@ static constexpr const ScriptKernelTask kScriptKernelTaskMap[] = {
 	ScriptKernelTask::WaitForMusicToEnd
 };
 
+static constexpr const int kKernelTaskArgCounts[] = {
+	0,
+	2,
+	3,
+	1,
+	2,
+	1,
+	1,
+	2,
+	1,
+	1,
+	2,
+	1,
+	1,
+	1,
+	2,
+	2,
+	0,
+	1,
+	2,
+	1,
+	1,
+	1,
+	0,
+	1,
+	0,
+	2,
+	1,
+	0,
+	0
+};
+
 class GameMovieAdventureOriginal : public Game {
 public:
 	GameMovieAdventureOriginal() {
@@ -143,6 +175,10 @@ public:
 		return kNoXORKey;
 	}
 
+	const char *getInitScriptName() override {
+		return "INICIALIZAR_MUNDO";
+	}
+
 	Span<const ScriptOp> getScriptOpMap() override {
 		return { kScriptOpMap, ARRAYSIZE(kScriptOpMap) };
 	}
@@ -150,6 +186,11 @@ public:
 	Span<const ScriptKernelTask> getScriptKernelTaskMap() override {
 		return { kScriptKernelTaskMap, ARRAYSIZE(kScriptKernelTaskMap) };
 	}
+
+	int32 getKernelTaskArgCount(int32 taskI) override {
+		assert(taskI >= 0 && taskI < ARRAYSIZE(kKernelTaskArgCounts));
+		return kKernelTaskArgCounts[taskI];
+	}
 };
 
 Game *Game::createForMovieAdventureOriginal() {
diff --git a/engines/alcachofa/game-movie-adventure-special.cpp b/engines/alcachofa/game-movie-adventure-special.cpp
index 66a5f6f8e5b..3295cd9439c 100644
--- a/engines/alcachofa/game-movie-adventure-special.cpp
+++ b/engines/alcachofa/game-movie-adventure-special.cpp
@@ -228,6 +228,10 @@ public:
 		return kEmbeddedXORKey;
 	}
 
+	const char *getInitScriptName() override {
+		return "CREDITOS_INICIALES";
+	}
+
 	void updateScriptVariables() override {
 		Script &script = g_engine->script();
 		if (g_engine->input().wasAnyMousePressed()) // yes, this variable is never reset by the engine (only by script)
diff --git a/engines/alcachofa/game.cpp b/engines/alcachofa/game.cpp
index 36c2abbf59c..a77b0ca7f73 100644
--- a/engines/alcachofa/game.cpp
+++ b/engines/alcachofa/game.cpp
@@ -39,6 +39,11 @@ void Game::onLoadedGameFiles() {}
 
 void Game::drawScreenStates() {}
 
+int32 Game::getKernelTaskArgCount(int32 kernelTaskI) {
+	(void)kernelTaskI;
+	return 0;
+}
+
 bool Game::doesRoomHaveBackground(const Room *room) {
 	return true;
 }
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index bd562331bf5..bfb54423058 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -61,8 +61,10 @@ public:
 	virtual const char *getDialogFileName() = 0;
 	virtual const char *getObjectFileName() = 0;
 	virtual char getTextFileKey() = 0;
+	virtual const char *getInitScriptName() = 0;
 	virtual Common::Span<const ScriptOp> getScriptOpMap() = 0;
 	virtual Common::Span<const ScriptKernelTask> getScriptKernelTaskMap() = 0;
+	virtual int32 getKernelTaskArgCount(int32 kernelTaskI); // only necessary for V1
 
 	virtual bool doesRoomHaveBackground(const Room *room);
 	virtual void unknownRoomObject(const Common::String &type);
diff --git a/engines/alcachofa/script-debug.h b/engines/alcachofa/script-debug.h
index f6e6d1fa807..96bab4d7232 100644
--- a/engines/alcachofa/script-debug.h
+++ b/engines/alcachofa/script-debug.h
@@ -28,8 +28,10 @@ static const char *const ScriptOpNames[] = {
 	"Nop",
 	"Dup",
 	"PushAddr",
+	"PushDynAddr",
 	"PushValue",
 	"Deref",
+	"Pop1",
 	"PopN",
 	"Store",
 	"LoadString",
@@ -110,7 +112,11 @@ static const char *const KernelCallNames[] = {
 	"FadeIn2",
 	"FadeOut2",
 	"LerpCamXYZ",
-	"LerpCamToObjectKeepingZ"
+	"LerpCamToObjectKeepingZ",
+
+	"SheriffTakesCharacter",
+	"ChangeDoor",
+	"Disguise"
 };
 
 }
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 59a34026ec1..61b5b87c67d 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -250,8 +250,12 @@ struct ScriptTask final : public Task {
 				error("Script process reached instruction out-of-bounds");
 			const auto &instruction = _script._instructions[_pc++];
 			if (debugChannelSet(SCRIPT_DEBUG_LVL_INSTRUCTIONS, kDebugScript)) {
+				const char *opName = "<invalid>";
+				if (instruction._op >= 0 && (uint32)instruction._op < opMap.size())
+					opName = ScriptOpNames[(int)opMap[(uint32)instruction._op]];
+
 				debugN("%u: %5u %-12s %8d Stack: ",
-					process().pid(), _pc - 1, ScriptOpNames[(int)instruction._op], instruction._arg);
+					process().pid(), _pc - 1, opName, instruction._arg);
 				if (_stack.empty())
 					debug("empty");
 				else {
@@ -290,12 +294,24 @@ struct ScriptTask final : public Task {
 			case ScriptOp::PushAddr:
 				pushVariable(instruction._arg);
 				break;
+			case ScriptOp::PushDynAddr: {
+				int32 address = popNumber();
+				if (address < 0 || (uint32)address >= _script._strings->size())
+					error("Script tried to push invalid address: %d", address);
+				else if ((uint32)address < _script._variables.size() * 4)
+					pushVariable(address);
+				else
+					pushString((uint32)address);
+			}break;
 			case ScriptOp::PushValue:
 				pushNumber(instruction._arg);
 				break;
 			case ScriptOp::Deref:
 				pushNumber(popVariable());
 				break;
+			case ScriptOp::Pop1:
+				popN(1);
+				break;
 			case ScriptOp::PopN:
 				popN(instruction._arg);
 				break;
@@ -370,6 +386,11 @@ struct ScriptTask final : public Task {
 			case ScriptOp::BitOr:
 				pushNumber(popNumber() | popNumber()); //-V501
 				break;
+			case ScriptOp::ReturnVoid: {
+				_pc = popInstruction();
+				if (_pc == UINT_MAX)
+					return TaskReturn::finish(0);
+			}break;
 			case ScriptOp::ReturnValue: {
 				int32 returnValue = popNumber();
 				_pc = popInstruction();
@@ -418,16 +439,25 @@ struct ScriptTask final : public Task {
 private:
 	void setCharacterVariables() {
 		_script.variable("m_o_f") = (int32)process().character();
-		_script.variable("m_o_f_real") = (int32)g_engine->player().activeCharacterKind();
+		if (g_engine->isV3())
+			_script.variable("m_o_f_real") = (int32)g_engine->player().activeCharacterKind();
 	}
 
 	void handleReturnFromKernelCall(int32 returnValue) {
-		// this is also original done, every KernelCall is followed by a PopN of the arguments
-		// only *after* the PopN the return value is pushed so that the following script can use it
-		scumm_assert(
-			_pc < _script._instructions.size() &&
-			g_engine->game().getScriptOpMap()[_script._instructions[_pc]._op] == ScriptOp::PopN);
-		popN(_script._instructions[_pc++]._arg);
+		// before pushing the return value the arguments have to be popped
+		// in V3 this is "by the script" by having a special PopN op
+		// in V1 we have to know the proper number of arguments per kernel call, so we do it in kernelCall
+
+		if (g_engine->isV3()) {
+			scumm_assert(
+				_pc < _script._instructions.size() &&
+				g_engine->game().getScriptOpMap()[_script._instructions[_pc]._op] == ScriptOp::PopN);
+			popN(_script._instructions[_pc++]._arg);
+		}
+		else {
+			assert(g_engine->isV1());
+			popN(g_engine->game().getKernelTaskArgCount(_lastKernelTaskI));
+		}
 		pushNumber(returnValue);
 	}
 
@@ -558,6 +588,7 @@ private:
 			g_engine->game().unknownKernelTask(taskI);
 			return TaskReturn::finish(-1);
 		}
+		_lastKernelTaskI = taskI;
 		const auto task = taskMap[taskI];
 
 		debugC(SCRIPT_DEBUG_LVL_KERNELCALLS, kDebugScript, "%u: %5u Kernel %-25s",
@@ -975,6 +1006,7 @@ private:
 	Stack<StackEntry> _stack;
 	String _name;
 	uint32 _pc = 0;
+	int32 _lastKernelTaskI = 0;
 	bool _returnsFromKernelCall = false;
 	bool _isFirstExecution = true;
 	FakeLock _lock;


Commit: 81d9c9c6d20d4f46d287c80449e144df63c3ee8c
    https://github.com/scummvm/scummvm/commit/81d9c9c6d20d4f46d287c80449e144df63c3ee8c
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:26+01:00

Commit Message:
ALCACHOFA: Fix script variable count

Changed paths:
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 61b5b87c67d..d077f9e0229 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -68,7 +68,7 @@ Script::Script() {
 	if (memorySize == 0) {
 		// in V1 the memory size is implicitly stored in the variables
 		for (const auto &variable : _variableNames)
-			memorySize = MAX(memorySize, variable._value + (uint32)sizeof(int32));
+			memorySize = MAX(memorySize, variable._value + 1);
 		memorySize *= sizeof(int32); // _variableNames already works with indices
 	}
 	if (memorySize % sizeof(int32) != 0)
@@ -438,9 +438,11 @@ struct ScriptTask final : public Task {
 
 private:
 	void setCharacterVariables() {
-		_script.variable("m_o_f") = (int32)process().character();
-		if (g_engine->isV3())
+		if (g_engine->isV3()) {
+			_script.variable("m_o_f") = (int32)process().character();
 			_script.variable("m_o_f_real") = (int32)g_engine->player().activeCharacterKind();
+		} else
+			_script.variable("m_o_f") = (int32)g_engine->player().activeCharacterKind();
 	}
 
 	void handleReturnFromKernelCall(int32 returnValue) {
@@ -572,7 +574,13 @@ private:
 	}
 
 	MainCharacter &relatedCharacter() {
-		if (process().character() == MainCharacterKind::None)
+		if (g_engine->isV1()) {
+			auto character = g_engine->player().activeCharacter();
+			if (character == nullptr)
+				error("Script tried to use character before setting an active character");
+			return *character;
+		}
+		if (process().character() == MainCharacterKind::None) 
 			error("Script tried to use character from non-character-related process");
 		return g_engine->world().getMainCharacterByKind(process().character());
 	}


Commit: 2a92c67e265cbf6548a3cc1c72ebf20ece3f631a
    https://github.com/scummvm/scummvm/commit/2a92c67e265cbf6548a3cc1c72ebf20ece3f631a
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:26+01:00

Commit Message:
ALCACHOFA: Fix V1 backgrounds and empty animations

Changed paths:
    engines/alcachofa/game-movie-adventure-original.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/graphics.h
    engines/alcachofa/rooms.cpp
    engines/alcachofa/rooms.h


diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index 3a11df7deaa..3521da5998f 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -191,6 +191,25 @@ public:
 		assert(taskI >= 0 && taskI < ARRAYSIZE(kKernelTaskArgCounts));
 		return kKernelTaskArgCounts[taskI];
 	}
+
+	void missingAnimation(const String &fileName) override {
+		static const char *exemptions[] = {
+			nullptr
+		};
+
+		const auto isInExemptions = [&] (const char *const *const list) {
+			for (const char *const *exemption = list; *exemption != nullptr; exemption++) {
+				if (fileName.equalsIgnoreCase(*exemption))
+					return true;
+			}
+			return false;
+		};
+
+		if (isInExemptions(exemptions))
+			debugC(1, kDebugGraphics, "Animation exemption triggered: %s", fileName.c_str());
+		else
+			Game::missingAnimation(fileName);
+	}
 };
 
 Game *Game::createForMovieAdventureOriginal() {
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 1b0fae7d4c8..a8f054dd000 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -158,9 +158,15 @@ void AnimationBase::load() {
 		return;
 
 	ScopedPtr<SeekableReadStream> rawStream;
-	if (_fileRef.isEmbedded())
-		rawStream = g_engine->world().openFileRef(_fileRef);
-	else {
+	if (_fileRef.isEmbedded()) {
+		if (_fileRef._size == 0) {
+			// this happens for some special cases in original movie adventure
+			// we cannot really check against an allowlist here, so we don't
+			setToEmpty();
+			return;
+		} else
+			rawStream = g_engine->world().openFileRef(_fileRef);
+	} else {
 		// for real file paths we have to apply the folder and do some fallback
 		String fullPath;
 		switch (_folder) {
@@ -194,7 +200,9 @@ void AnimationBase::load() {
 	}
 	
 	if (rawStream == nullptr) {
-		loadMissingAnimation();
+		// only allow missing animations we know are faulty in the original game
+		g_engine->game().missingAnimation(_fileRef._path);
+		setToEmpty();
 		return;
 	}
 
@@ -372,11 +380,8 @@ ManagedSurface *AnimationBase::readImageV3(SeekableReadStream &stream) const {
 	return target;
 }
 
-void AnimationBase::loadMissingAnimation() {
-	// only allow missing animations we know are faulty in the original game
-	g_engine->game().missingAnimation(_fileRef._path);
-
-	// otherwise setup a functioning but empty animation
+void AnimationBase::setToEmpty() {
+	assert(!_isLoaded);
 	_isLoaded = true;
 	_totalDuration = 1;
 	_spriteIndexMapping[0] = 0;
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index b436e37223b..384f35a312f 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -179,8 +179,8 @@ protected:
 	~AnimationBase();
 
 	void load();
-	void loadMissingAnimation();
 	void freeImages();
+	void setToEmpty();
 	void readV1(Common::SeekableReadStream &stream);
 	void readV3(Common::SeekableReadStream &stream);
 	Graphics::ManagedSurface *readImageV3(Common::SeekableReadStream &stream) const;
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 2e61097e8d5..8c133bea73f 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -94,15 +94,20 @@ static ObjectBase *readRoomObject(Room *room, const String &type, SeekableReadSt
 		return nullptr; // handled in Room::Room
 }
 
-void Room::readRoomV1(SeekableReadStream &stream, bool readObjects) {
+void Room::readRoomV1(SeekableReadStream &stream, bool shouldReadObjects) {
 	_name = readVarString(stream);
 	_backgroundName = readVarString(stream);
 	_musicId = (int)stream.readByte();
 	_characterAlphaTint = stream.readByte();
 	skipVarString(stream);
 
-	if (readObjects)
-		readObjectsAndBackground(stream, kBaseScale);
+	if (shouldReadObjects)
+		readObjects(stream, kBaseScale);
+
+	if (!_backgroundName.empty() && g_engine->game().doesRoomHaveBackground(this)) {
+		// in V1 _backgroundName refers to an object which represents the background
+		_backgroundObject = getObjectByName(_backgroundName.c_str());
+	}
 }
 
 void Room::readRoomV3(SeekableReadStream &stream, bool hasUselessByte) {
@@ -119,10 +124,16 @@ void Room::readRoomV3(SeekableReadStream &stream, bool hasUselessByte) {
 	if (hasUselessByte)
 		stream.readByte();
 
-	readObjectsAndBackground(stream, backgroundScale);
+	readObjects(stream, backgroundScale);
+
+	if (g_engine->game().doesRoomHaveBackground(this)) {
+		// in V3 _background is the name of an animation, we have to create the object
+		_backgroundObject = new Background(this, _backgroundName, backgroundScale);
+		_objects.push_back(_backgroundObject);
+	}
 }
 
-void Room::readObjectsAndBackground(SeekableReadStream &stream, int16 backgroundScale) {
+void Room::readObjects(SeekableReadStream &stream, int16 backgroundScale) {
 	uint32 objectEnd = stream.readUint32LE();
 	while (objectEnd > 0) {
 		const auto type = readVarString(stream);
@@ -140,8 +151,6 @@ void Room::readObjectsAndBackground(SeekableReadStream &stream, int16 background
 			_objects.push_back(object);
 		objectEnd = stream.readUint32LE();
 	}
-	if (g_engine->game().doesRoomHaveBackground(this))
-		_objects.push_back(new Background(this, _backgroundName, backgroundScale));
 
 	if (!_floors[0].empty())
 		_activeFloorI = 0;
@@ -151,7 +160,7 @@ RoomWithFloor::RoomWithFloor(World *world, SeekableReadStream &stream) : Room(wo
 	readRoomV1(stream, false);
 	_floors[0] = PathFindingShape(stream);
 	_floors[1] = PathFindingShape(stream);
-	readObjectsAndBackground(stream, kBaseScale);
+	readObjects(stream, kBaseScale);
 }
 
 Room::~Room() {
@@ -252,8 +261,7 @@ void Room::updateInteraction() {
 }
 
 void Room::updateRoomBounds() {
-	auto background = getObjectByName("Background");
-	auto graphic = background == nullptr ? nullptr : background->graphic();
+	auto graphic = _backgroundObject == nullptr ? nullptr : _backgroundObject->graphic();
 	if (graphic != nullptr) {
 		auto bgSize = graphic->animation().imageSize(0);
 		/* This fixes a bug where if the background image is invalid the original engine
@@ -399,7 +407,7 @@ Inventory::Inventory(World *world, SeekableReadStream &stream) : Room(world) {
 	if (g_engine->isV1()) {
 		readRoomV1(stream, false);
 		stream.skip(1); // denoted as "sinusar" but unused
-		readObjectsAndBackground(stream, kBaseScale);
+		readObjects(stream, kBaseScale);
 	} else
 		readRoomV3(stream, true);
 }
@@ -501,7 +509,7 @@ void Inventory::drawAsOverlay(int32 scrollY) {
 		int8 oldOrder = graphic->order();
 		graphic->topLeft().y += scrollY;
 		graphic->order() = -kForegroundOrderCount;
-		if (object->name().equalsIgnoreCase("Background"))
+		if (object == _backgroundObject)
 			graphic->order()++;
 		object->draw();
 		graphic->topLeft().y = oldY;
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index 442f21c75f3..93e91aaba07 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -68,7 +68,7 @@ protected:
 	Room(World *world);
 	void readRoomV1(Common::SeekableReadStream &stream, bool readObjects);
 	void readRoomV3(Common::SeekableReadStream &stream, bool hasUselessByte);
-	void readObjectsAndBackground(Common::SeekableReadStream &stream, int16 backgroundScale);
+	void readObjects(Common::SeekableReadStream &stream, int16 backgroundScale);
 	void updateScripts();
 	void updateRoomBounds();
 	void updateInteraction();
@@ -78,6 +78,7 @@ protected:
 	ShapeObject *getSelectedObject(ShapeObject *best = nullptr) const;
 
 	World *_world;
+	ObjectBase *_backgroundObject = nullptr;
 	Common::String _name, _backgroundName;
 	PathFindingShape _floors[2];
 	bool _fixedCameraOnEntering = false;


Commit: ab1cbeb1722278cd8ab5995acbad5ea906b60112
    https://github.com/scummvm/scummvm/commit/ab1cbeb1722278cd8ab5995acbad5ea906b60112
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:26+01:00

Commit Message:
ALCACHOFA: Replace dependencies on version-dependent script variables

Changed paths:
    engines/alcachofa/game-movie-adventure-original.cpp
    engines/alcachofa/game-movie-adventure-special.cpp
    engines/alcachofa/game.h
    engines/alcachofa/global-ui.cpp


diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index 3521da5998f..26da9514d3d 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -192,6 +192,15 @@ public:
 		return kKernelTaskArgCounts[taskI];
 	}
 
+	void updateScriptVariables() override {
+		g_engine->script().variable("EstanAmbos") =
+			g_engine->world().mortadelo().room() == g_engine->world().filemon().room();
+	}
+
+	bool shouldClipCamera() override {
+		return true;
+	}
+
 	void missingAnimation(const String &fileName) override {
 		static const char *exemptions[] = {
 			nullptr
diff --git a/engines/alcachofa/game-movie-adventure-special.cpp b/engines/alcachofa/game-movie-adventure-special.cpp
index 3295cd9439c..d8650a6cad2 100644
--- a/engines/alcachofa/game-movie-adventure-special.cpp
+++ b/engines/alcachofa/game-movie-adventure-special.cpp
@@ -241,6 +241,21 @@ public:
 		script.variable("textoson") = g_engine->config().subtitles() ? 1 : 0;
 	}
 
+	void updateScriptVariables() override {
+		Script &script = g_engine->script();
+		if (g_engine->input().wasAnyMousePressed()) // yes, this variable is never reset by the engine (only by script)
+			script.variable("SeHaPulsadoRaton") = 1;
+
+		script.setScriptTimer(!script.variable("CalcularTiempoSinPulsarRaton"));
+		script.variable("EstanAmbos") = g_engine->world().mortadelo().room() == g_engine->world().filemon().room();
+		script.variable("textoson") = g_engine->config().subtitles() ? 1 : 0;
+		script.variable("modored") = 0; // this is signalling whether a network connection is established
+	}
+
+	bool shouldClipCamera() override {
+		return g_engine->script().variable("EncuadrarCamara");
+	}
+
 	void onLoadedGameFiles() override {
 		// this notifies the script whether we are a demo
 		if (g_engine->world().loadedMapCount() == 2)
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index bfb54423058..fee0bd54216 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -65,6 +65,8 @@ public:
 	virtual Common::Span<const ScriptOp> getScriptOpMap() = 0;
 	virtual Common::Span<const ScriptKernelTask> getScriptKernelTaskMap() = 0;
 	virtual int32 getKernelTaskArgCount(int32 kernelTaskI); // only necessary for V1
+	virtual void updateScriptVariables() = 0;
+	virtual bool shouldClipCamera() = 0;
 
 	virtual bool doesRoomHaveBackground(const Room *room);
 	virtual void unknownRoomObject(const Common::String &type);
diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index 4164dec0fc0..67415976a1e 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -252,6 +252,7 @@ void GlobalUI::drawScreenStates() {
 		return;
 
 	auto &drawQueue = g_engine->drawQueue();
+	int32 borderWidth;
 	if (_isPermanentFaded)
 		drawQueue.add<FadeDrawRequest>(FadeType::ToBlack, 1.0f, -9);
 	else


Commit: 7ff8d738bbc46682822dc45ae5fc4bfe47e10ca9
    https://github.com/scummvm/scummvm/commit/7ff8d738bbc46682822dc45ae5fc4bfe47e10ca9
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:26+01:00

Commit Message:
ALCACHOFA: Add kernel task Disguise

Changed paths:
    engines/alcachofa/camera.cpp
    engines/alcachofa/camera.h
    engines/alcachofa/script.cpp
    engines/alcachofa/tasks.h


diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 4977bbb0ee6..565025839ab 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -668,4 +668,65 @@ Task *Camera::shake(Process &process, Math::Vector2d amplitude, Math::Vector2d f
 	return new CamShakeTask(process, amplitude, frequency, duration);
 }
 
+// The original name for this task is "disfraza" which I can only translate as "disguise"
+// It is a slightly bouncing vertical camera movement with fixed distance
+
+struct CamDisguiseTask final : public Task {
+	CamDisguiseTask(Process &process, int32 durationMs)
+		: Task(process)
+		, _camera(g_engine->camera())
+		, _durationMs(durationMs) {}
+
+	CamDisguiseTask(Process &process, Serializer &s)
+		: Task(process)
+		, _camera(g_engine->camera()) {
+		CamDisguiseTask::syncGame(s);
+	}
+
+	TaskReturn run() override {
+		if (_startTime == 0) {
+			_startPosition = { _camera.usedCenter().x(), _camera.usedCenter().y() };
+			_startTime = g_engine->getMillis();
+		}
+		if (_durationMs <= 0 || g_engine->getMillis() - _startTime >= (uint32)_durationMs)
+			return TaskReturn::finish(0);
+
+		Vector2d newPosition = _startPosition;
+		uint32 t = (g_engine->getMillis() - _startTime) * 5;
+		if (t <= 50)
+			newPosition.setY(_startPosition.getY() + t);
+		else if (t <= 150)
+			newPosition.setY(_startPosition.getY() - t + 100);
+		else if (t <= 200)
+			newPosition.setY(_startPosition.getY() + t - 200);
+		_camera.setPosition(newPosition);
+
+		return TaskReturn::yield();
+	}
+
+	void debugPrint() override {
+		g_engine->getDebugger()->debugPrintf("\"Disguise\" camera for %dms", _durationMs);
+	}
+
+	void syncGame(Serializer &s) override {
+		Task::syncGame(s);
+		s.syncAsSint32LE(_durationMs);
+		s.syncAsUint32LE(_startTime);
+		syncVector(s, _startPosition);
+	}
+
+	const char *taskName() const override;
+
+private:
+	Camera &_camera;
+	int32 _durationMs;
+	uint32 _startTime = 0;
+	Vector2d _startPosition;
+};
+DECLARE_TASK(CamDisguiseTask)
+
+Task *Camera::disguise(Process &process, int32 duration) {
+	return new CamDisguiseTask(process, duration);
+}
+
 } // namespace Alcachofa
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index 5a091cf2a06..f22a44bb6a2 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -36,6 +36,7 @@ static constexpr const float kInvBaseScale = 1.0f / kBaseScale;
 
 class Camera {
 public:
+	inline Math::Vector3d usedCenter() const { return _cur._usedCenter; }
 	inline Math::Angle rotation() const { return _cur._rotation; }
 	inline Math::Vector2d &shake() { return _shake; }
 	inline WalkingCharacter *followTarget() { return _followTarget; }
@@ -73,6 +74,7 @@ public:
 		int32 duration, EasingType moveEasingType, EasingType scaleEasingType);
 	Task *waitToStop(Process &process);
 	Task *shake(Process &process, Math::Vector2d amplitude, Math::Vector2d frequency, int32 duration);
+	Task *disguise(Process &process, int32 duration);
 
 private:
 	friend struct CamLerpTask;
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index d077f9e0229..405e16608a2 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -945,6 +945,9 @@ private:
 				as3D(pointObject->position()), targetScale,
 				getNumberArg(2), (EasingType)getNumberArg(3), (EasingType)getNumberArg(4)));
 		}
+		case ScriptKernelTask::Disguise:
+			// a somewhat bouncy vertical camera movement used in V1
+			return TaskReturn::waitFor(g_engine->camera().disguise(process(), getNumberArg(0)));
 
 		// Fades
 		case ScriptKernelTask::FadeType0:
diff --git a/engines/alcachofa/tasks.h b/engines/alcachofa/tasks.h
index 8c95f719033..976b2e38cab 100644
--- a/engines/alcachofa/tasks.h
+++ b/engines/alcachofa/tasks.h
@@ -38,6 +38,7 @@ DEFINE_TASK(CamLerpRotationTask)
 DEFINE_TASK(CamShakeTask)
 DEFINE_TASK(CamWaitToStopTask)
 DEFINE_TASK(CamSetInactiveAttributeTask)
+DEFINE_TASK(CamDisguiseTask)
 DEFINE_TASK(SayTextTask)
 DEFINE_TASK(AnimateCharacterTask)
 DEFINE_TASK(LerpLodBiasTask)
@@ -51,6 +52,6 @@ DEFINE_TASK(ScriptTimerTask)
 DEFINE_TASK(ScriptTask)
 DEFINE_TASK(PlaySoundTask)
 DEFINE_TASK(WaitForMusicTask)
-DEFINE_TASK(DelayTask)  
+DEFINE_TASK(DelayTask)
 
 #undef DEFINE_TASK


Commit: 4d269d4ac13cf8004b3fa7c068599cbe50668336
    https://github.com/scummvm/scummvm/commit/4d269d4ac13cf8004b3fa7c068599cbe50668336
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:26+01:00

Commit Message:
ALCACHOFA: Fix regression with file-based animation refs

Changed paths:
    engines/alcachofa/game-movie-adventure-special.cpp
    engines/alcachofa/graphics.cpp


diff --git a/engines/alcachofa/game-movie-adventure-special.cpp b/engines/alcachofa/game-movie-adventure-special.cpp
index d8650a6cad2..0214a41fd6c 100644
--- a/engines/alcachofa/game-movie-adventure-special.cpp
+++ b/engines/alcachofa/game-movie-adventure-special.cpp
@@ -418,34 +418,36 @@ public:
 
 	void missingAnimation(const String &fileName) override {
 		static const char *exemptions[] = {
-			"ANIMACION.AN0",
-			"DESPACHO_SUPER2_OL_SOMBRAS2.AN0",
-			"PP_MORTA.AN0",
-			"DESPACHO_SUPER2___FONDO_PP_SUPER.AN0",
-			"ESTOMAGO.AN0",
-			"CREDITOS.AN0",
-			"MONITOR___OL_EFECTO_FONDO.AN0",
+			"ANIMACION",
+			"DESPACHO_SUPER2_OL_SOMBRAS2",
+			"PP_MORTA",
+			"DESPACHO_SUPER2___FONDO_PP_SUPER",
+			"ESTOMAGO",
+			"CREDITOS",
+			"MONITOR___OL_EFECTO_FONDO",
 			nullptr
 		};
 
 		// these only happen in the german demo
 		static const char *demoExemptions[] = {
-			"TROZO_1.AN0",
-			"TROZO_2.AN0",
-			"TROZO_3.AN0",
-			"TROZO_4.AN0",
-			"TROZO_5.AN0",
-			"TROZO_6.AN0",
-			"NOTA_CINE_NEGRO.AN0",
-			"PP_JOHN_WAYNE_2.AN0",
-			"ARQUEOLOGO_ESTATICO_TIA.AN0",
-			"ARQUEOLOGO_HABLANDO_TIA.AN0",
+			"TROZO_1",
+			"TROZO_2",
+			"TROZO_3",
+			"TROZO_4",
+			"TROZO_5",
+			"TROZO_6",
+			"NOTA_CINE_NEGRO",
+			"PP_JOHN_WAYNE_2",
+			"ARQUEOLOGO_ESTATICO_TIA",
+			"ARQUEOLOGO_HABLANDO_TIA",
 			nullptr
 		};
 
+		bool hasExtension = fileName.hasSuffixIgnoreCase(".AN0");
 		const auto isInExemptions = [&] (const char *const *const list) {
 			for (const char *const *exemption = list; *exemption != nullptr; exemption++) {
-				if (fileName.equalsIgnoreCase(*exemption))
+				if ((hasExtension && fileName.hasPrefixIgnoreCase(*exemption)) ||
+					(!hasExtension && fileName.equalsIgnoreCase(*exemption)))
 					return true;
 			}
 			return false;
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index a8f054dd000..600e98f1be3 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -169,28 +169,31 @@ void AnimationBase::load() {
 	} else {
 		// for real file paths we have to apply the folder and do some fallback
 		String fullPath;
-		switch (_folder) {
-		case AnimationFolder::Animations:
-			fullPath = "Animaciones/";
-			break;
-		case AnimationFolder::Masks:
-			fullPath = "Mascaras/";
-			break;
-		case AnimationFolder::Backgrounds:
-			fullPath = "Fondos/";
-			break;
-		default:
-			assert(false && "Invalid AnimationFolder");
-			break;
-		}
-		fullPath += _fileRef._path;
-		if (_fileRef._path.size() < 4 || scumm_strnicmp(_fileRef._path.end() - 4, ".AN0", 4) != 0)
-			fullPath += ".AN0";
+		const auto getFullPath = [&] (AnimationFolder folder) {
+			switch (folder) {
+			case AnimationFolder::Animations:
+				fullPath = "Animaciones/";
+				break;
+			case AnimationFolder::Masks:
+				fullPath = "Mascaras/";
+				break;
+			case AnimationFolder::Backgrounds:
+				fullPath = "Fondos/";
+				break;
+			default:
+				assert(false && "Invalid AnimationFolder");
+				break;
+			}
+			fullPath += _fileRef._path;
+			if (_fileRef._path.size() < 4 || scumm_strnicmp(_fileRef._path.end() - 4, ".AN0", 4) != 0)
+				fullPath += ".AN0";
+		};
+		getFullPath(_folder);
 
 		File *file = new File();
 		if (!file->open(fullPath.c_str())) {
 			// original fallback
-			fullPath = "Mascaras/" + _fileRef._path;
+			getFullPath(AnimationFolder::Masks);
 			if (!file->open(fullPath.c_str())) {
 				delete file;
 				file = nullptr;


Commit: d368ed8354ea9d90363d334cef85c37eed6b0269
    https://github.com/scummvm/scummvm/commit/d368ed8354ea9d90363d334cef85c37eed6b0269
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:26+01:00

Commit Message:
ALCACHOFA: Use game-specific video paths

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/game-movie-adventure-original.cpp
    engines/alcachofa/game-movie-adventure-special.cpp
    engines/alcachofa/game.h
    engines/alcachofa/graphics.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 65a4a1c1e0c..dbbbf8da484 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -145,7 +145,7 @@ void AlcachofaEngine::playVideo(int32 videoId) {
 	// Video files are either MPEG PS or AVI
 	FakeLock lock("playVideo", _eventLoopSemaphore);
 	File *file = new File();
-	if (!file->open(Path(Common::String::format("Data/DATA%02d.BIN", videoId + 1)))) {
+	if (!file->open(game().getVideoPath(videoId))) {
 		delete file;
 		game().invalidVideo(videoId, "open file");
 		return;
diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index 26da9514d3d..313993980bc 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -197,6 +197,10 @@ public:
 			g_engine->world().mortadelo().room() == g_engine->world().filemon().room();
 	}
 
+	Path getVideoPath(int32 videoId) override {
+		return Path(String::format("disk1/Install/bin/data%02d.bin", videoId));
+	}
+
 	bool shouldClipCamera() override {
 		return true;
 	}
diff --git a/engines/alcachofa/game-movie-adventure-special.cpp b/engines/alcachofa/game-movie-adventure-special.cpp
index 0214a41fd6c..1336aaefd1e 100644
--- a/engines/alcachofa/game-movie-adventure-special.cpp
+++ b/engines/alcachofa/game-movie-adventure-special.cpp
@@ -252,6 +252,10 @@ public:
 		script.variable("modored") = 0; // this is signalling whether a network connection is established
 	}
 
+	Path getVideoPath(int32 videoId) override {
+		return Path(String::format("Data/DATA%02d.BIN", videoId + 1));
+	}
+
 	bool shouldClipCamera() override {
 		return g_engine->script().variable("EncuadrarCamara");
 	}
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index fee0bd54216..ce7d6b1ac36 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -66,6 +66,7 @@ public:
 	virtual Common::Span<const ScriptKernelTask> getScriptKernelTaskMap() = 0;
 	virtual int32 getKernelTaskArgCount(int32 kernelTaskI); // only necessary for V1
 	virtual void updateScriptVariables() = 0;
+	virtual Common::Path getVideoPath(int32 videoId) = 0;
 	virtual bool shouldClipCamera() = 0;
 
 	virtual bool doesRoomHaveBackground(const Room *room);
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 600e98f1be3..02ffe98a899 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -94,7 +94,7 @@ ImageV1::ImageV1(SeekableReadStream &stream) {
 }
 
 ManagedSurface *ImageV1::render(byte alpha) const {
-	ManagedSurface *surface = new ManagedSurface(_size.x, _size.y, BlendBlit::getSupportedPixelFormat());
+	ManagedSurface *surface = new ManagedSurface(_size.x, _size.y, g_engine->renderer().getPixelFormat());
 	render(*surface, {}, alpha);
 	return surface;
 }


Commit: 5108587f16ca346400ac6eb861749777149a6ec2
    https://github.com/scummvm/scummvm/commit/5108587f16ca346400ac6eb861749777149a6ec2
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:26+01:00

Commit Message:
ALCACHOFA: Use game-specific sound paths

Changed paths:
    engines/alcachofa/game-movie-adventure-original.cpp
    engines/alcachofa/game-movie-adventure-special.cpp
    engines/alcachofa/game.h
    engines/alcachofa/rooms.cpp
    engines/alcachofa/rooms.h
    engines/alcachofa/sounds.cpp
    engines/alcachofa/sounds.h


diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index 313993980bc..e12889f7e5c 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -201,6 +201,16 @@ public:
 		return Path(String::format("disk1/Install/bin/data%02d.bin", videoId));
 	}
 
+	String getSoundPath(const char *filename) override {
+		return filename;
+	}
+
+	String getMusicPath(int32 trackId) override {
+		const Room *room = g_engine->player().currentRoom();
+		const int diskId = room != nullptr && room->mapIndex() == 1 ? 2 : 1;
+		return String::format("disk%d/track%02d", diskId, trackId);
+	}
+
 	bool shouldClipCamera() override {
 		return true;
 	}
diff --git a/engines/alcachofa/game-movie-adventure-special.cpp b/engines/alcachofa/game-movie-adventure-special.cpp
index 1336aaefd1e..8991eb8ead7 100644
--- a/engines/alcachofa/game-movie-adventure-special.cpp
+++ b/engines/alcachofa/game-movie-adventure-special.cpp
@@ -256,6 +256,14 @@ public:
 		return Path(String::format("Data/DATA%02d.BIN", videoId + 1));
 	}
 
+	String getSoundPath(const char *filename) override {
+		return String("Sonidos/") + filename;
+	}
+
+	String getMusicPath(int32 trackId) override {
+		return String::format("Sonidos/T%d", trackId);
+	}
+
 	bool shouldClipCamera() override {
 		return g_engine->script().variable("EncuadrarCamara");
 	}
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index ce7d6b1ac36..6f94414c396 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -67,6 +67,8 @@ public:
 	virtual int32 getKernelTaskArgCount(int32 kernelTaskI); // only necessary for V1
 	virtual void updateScriptVariables() = 0;
 	virtual Common::Path getVideoPath(int32 videoId) = 0;
+	virtual Common::String getSoundPath(const char *filename) = 0; ///< Without file-extension
+	virtual Common::String getMusicPath(int32 trackId) = 0; ///< Without file-extension
 	virtual bool shouldClipCamera() = 0;
 
 	virtual bool doesRoomHaveBackground(const Room *room);
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 8c133bea73f..29e4569c981 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -32,9 +32,13 @@ using namespace Common;
 
 namespace Alcachofa {
 
-Room::Room(World *world) : _world(world) {}
+Room::Room(World *world)
+	: _world(world)
+	, _mapIndex(world->loadedMapCount()) {}
 
-Room::Room(World *world, SeekableReadStream &stream) : _world(world) {
+Room::Room(World *world, SeekableReadStream &stream)
+	: _world(world)
+	, _mapIndex(world->loadedMapCount()) {
 	if (g_engine->isV1())
 		readRoomV1(stream, true);
 	else
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index 93e91aaba07..a07acce1567 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -35,6 +35,7 @@ public:
 	virtual ~Room();
 
 	inline World &world() { return *_world; }
+	inline uint8 mapIndex() const { return _mapIndex; }
 	inline const Common::String &name() const { return _name; }
 	inline const PathFindingShape *activeFloor() const {
 		return _activeFloorI < 0 ? nullptr : &_floors[_activeFloorI];
@@ -85,6 +86,7 @@ protected:
 	int8 _activeFloorI = -1;
 	int _musicId = -1;
 	uint8
+		_mapIndex,
 		_characterAlphaTint = 0,
 		_characterAlphaPremultiplier = 100; ///< for some reason in percent instead of 0-255
 
diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index b26795fc006..50a028ff5d1 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -112,26 +112,27 @@ static AudioStream *loadSND(File *file) {
 	}
 }
 
-static AudioStream *openAudio(const char *fileName) {
-	String path = String::format("Sonidos/%s.SND", fileName);
+static AudioStream *openAudio(const String &basePath) {
+	String path = basePath + ".SND";
 	File *file = new File();
 	if (file->open(path.c_str()))
 		return file->size() == 0 // Movie Adventure has some null-size audio files, they are treated like infinite silence
 			? makeSilentAudioStream(8000, false) 
 			: loadSND(file);
+
 	path.setChar('W', path.size() - 3);
 	path.setChar('A', path.size() - 2);
 	path.setChar('V', path.size() - 1);
 	if (file->open(path.c_str()))
 		return makeWAVStream(file, DisposeAfterUse::YES);
-	delete file;
 
-	g_engine->game().missingSound(fileName);
+	delete file;
+	g_engine->game().missingSound(basePath);
 	return nullptr;
 }
 
-SoundHandle Sounds::playSoundInternal(const char *fileName, byte volume, Mixer::SoundType type) {
-	AudioStream *stream = openAudio(fileName);
+SoundHandle Sounds::playSoundInternal(const String &path, byte volume, Mixer::SoundType type) {
+	AudioStream *stream = openAudio(path);
 	if (stream == nullptr && (type == Mixer::kSpeechSoundType || type == Mixer::kMusicSoundType)) {
 		/* If voice files are missing, the player could still read the subtitle
 		 * For this we return infinite silent audio which the user has to skip
@@ -199,12 +200,14 @@ SoundHandle Sounds::playSoundInternal(const char *fileName, byte volume, Mixer::
 
 SoundHandle Sounds::playVoice(const String &fileName, byte volume) {
 	debugC(1, kDebugSounds, "Play voice: %s at %d", fileName.c_str(), (int)volume);
-	return playSoundInternal(fileName.c_str(), volume, Mixer::kSpeechSoundType);
+	auto path = g_engine->game().getSoundPath(fileName.c_str());
+	return playSoundInternal(path.c_str(), volume, Mixer::kSpeechSoundType);
 }
 
 SoundHandle Sounds::playSFX(const String &fileName, byte volume) {
 	debugC(1, kDebugSounds, "Play SFX: %s at %d", fileName.c_str(), (int)volume);
-	return playSoundInternal(fileName.c_str(), volume, Mixer::kSFXSoundType);
+	auto path = g_engine->game().getSoundPath(fileName.c_str());
+	return playSoundInternal(path.c_str(), volume, Mixer::kSFXSoundType);
 }
 
 void Sounds::stopAll() {
@@ -305,10 +308,8 @@ void Sounds::startMusic(int musicId) {
 	debugC(2, kDebugSounds, "startMusic %d", musicId);
 	assert(musicId >= 0);
 	fadeMusic();
-	constexpr size_t kBufferSize = 16;
-	char filenameBuffer[kBufferSize];
-	snprintf(filenameBuffer, kBufferSize, "T%d", musicId);
-	_musicSoundID = playSoundInternal(filenameBuffer, Mixer::kMaxChannelVolume, Mixer::kMusicSoundType);
+	auto path = g_engine->game().getMusicPath(musicId);
+	_musicSoundID = playSoundInternal(path, Mixer::kMaxChannelVolume, Mixer::kMusicSoundType);
 	_isMusicPlaying = true;
 	_nextMusicID = musicId;
 }
diff --git a/engines/alcachofa/sounds.h b/engines/alcachofa/sounds.h
index 693fd1a934a..a6577b0b9ee 100644
--- a/engines/alcachofa/sounds.h
+++ b/engines/alcachofa/sounds.h
@@ -72,7 +72,7 @@ private:
 		Common::Array<int16> _samples; ///< might not be filled, only voice samples are preloaded for lip-sync
 	};
 	Playback *getPlaybackById(SoundHandle id);
-	SoundHandle playSoundInternal(const char *fileName, byte volume, Audio::Mixer::SoundType type);
+	SoundHandle playSoundInternal(const Common::String &path, byte volume, Audio::Mixer::SoundType type);
 
 	Common::Array<Playback> _playbacks;
 	Audio::Mixer *_mixer;


Commit: e8e352c20beb00849f314da5da3adbe4cd5661b5
    https://github.com/scummvm/scummvm/commit/e8e352c20beb00849f314da5da3adbe4cd5661b5
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:26+01:00

Commit Message:
ALCACHOFA: Fix V1 image rendering

Changed paths:
    engines/alcachofa/graphics.cpp


diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 02ffe98a899..b431223d5fd 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -125,8 +125,10 @@ void ImageV1::render(ManagedSurface &target, Point dstOffset, byte alpha) const
 			dstX += segment._xOffset;
 			if (dstX >= target.w)
 				break;
-			if (dstX + segment._width < 0)
+			if (dstX + segment._width < 0) {
+				dstX += segment._width;
 				continue;
+			}
 			uint srcPixelI = segment._dataOffset;
 			if (dstX < 0)
 				srcPixelI += -dstX * bpp;
@@ -137,10 +139,11 @@ void ImageV1::render(ManagedSurface &target, Point dstOffset, byte alpha) const
 				const byte* srcPixel = _palette.empty()
 					? _pixels.data() + srcPixelI
 					: _palette.data() + 3 * _pixels[srcPixelI];
-				*dstPixel = target.format.ARGBToColor(alpha, srcPixel[0], srcPixel[1], srcPixel[2]);
+				*dstPixel = target.format.ARGBToColor(alpha, srcPixel[2], srcPixel[1], srcPixel[0]);
 				dstPixel++;
 				srcPixelI += bpp;
 			}
+			dstX += segment._width;
 		}
 	}
 }


Commit: 1d69d82ed2f99b98201484ab750dbf6947737e2b
    https://github.com/scummvm/scummvm/commit/1d69d82ed2f99b98201484ab750dbf6947737e2b
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:26+01:00

Commit Message:
ALCACHOFA: Disable cursor frame update on V1

Changed paths:
    engines/alcachofa/player.cpp


diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index fe0e4651abe..208df698250 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -51,7 +51,9 @@ void Player::resetCursor() {
 }
 
 void Player::updateCursor() {
-	if (g_engine->menu().isOpen() || !_isGameLoaded)
+	if (g_engine->isV1())
+		_cursorFrameI = 0;
+	else if (g_engine->menu().isOpen() || !_isGameLoaded)
 		_cursorFrameI = 0;
 	else if (_selectedObject == nullptr)
 		_cursorFrameI = !g_engine->input().isMouseLeftDown() || _pressedObject != nullptr ? 6 : 7;


Commit: ff4195f43ec18448ed3786b9288351b28e6a406e
    https://github.com/scummvm/scummvm/commit/ff4195f43ec18448ed3786b9288351b28e6a406e
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:27+01:00

Commit Message:
ALCACHOFA: Adapt V1 isAllowedToOpenMenu

Changed paths:
    engines/alcachofa/game-movie-adventure-original.cpp
    engines/alcachofa/game-movie-adventure-special.cpp
    engines/alcachofa/game.h
    engines/alcachofa/player.cpp


diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index e12889f7e5c..dd9c92fb060 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -215,6 +215,10 @@ public:
 		return true;
 	}
 
+	bool isAllowedToOpenMenu() override {
+		return dynamic_cast<RoomWithFloor *>(g_engine->player().currentRoom()) != nullptr;
+	}
+
 	void missingAnimation(const String &fileName) override {
 		static const char *exemptions[] = {
 			nullptr
diff --git a/engines/alcachofa/game-movie-adventure-special.cpp b/engines/alcachofa/game-movie-adventure-special.cpp
index 8991eb8ead7..7f1e4469592 100644
--- a/engines/alcachofa/game-movie-adventure-special.cpp
+++ b/engines/alcachofa/game-movie-adventure-special.cpp
@@ -268,6 +268,10 @@ public:
 		return g_engine->script().variable("EncuadrarCamara");
 	}
 
+	bool isAllowedToOpenMenu() override {
+		return !g_engine->script().variable("prohibirESC");
+	}
+
 	void onLoadedGameFiles() override {
 		// this notifies the script whether we are a demo
 		if (g_engine->world().loadedMapCount() == 2)
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index 6f94414c396..63d3833238a 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -70,6 +70,7 @@ public:
 	virtual Common::String getSoundPath(const char *filename) = 0; ///< Without file-extension
 	virtual Common::String getMusicPath(int32 trackId) = 0; ///< Without file-extension
 	virtual bool shouldClipCamera() = 0;
+	virtual bool isAllowedToOpenMenu() = 0; ///< only the game-specific condition
 
 	virtual bool doesRoomHaveBackground(const Room *room);
 	virtual void unknownRoomObject(const Common::String &type);
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 208df698250..6d87fff0e7b 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -352,7 +352,7 @@ bool Player::isAllowedToOpenMenu() {
 		isGameLoaded() &&
 		!g_engine->menu().isOpen() &&
 		g_engine->sounds().musicSemaphore().isReleased() &&
-		!g_engine->script().variable("prohibirESC") &&
+		g_engine->game().isAllowedToOpenMenu() &&
 		!_isInTemporaryRoom; // we cannot reliably store this state across multiple room changes
 }
 


Commit: 1deb9a115627afbebea271238fe6029ebac80eb9
    https://github.com/scummvm/scummvm/commit/1deb9a115627afbebea271238fe6029ebac80eb9
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:27+01:00

Commit Message:
ALCACHOFA: Adapt different MainCharacterKind values in V1

Changed paths:
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 405e16608a2..63b5fe7adb6 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -442,7 +442,7 @@ private:
 			_script.variable("m_o_f") = (int32)process().character();
 			_script.variable("m_o_f_real") = (int32)g_engine->player().activeCharacterKind();
 		} else
-			_script.variable("m_o_f") = (int32)g_engine->player().activeCharacterKind();
+			_script.variable("m_o_f") = (int32)g_engine->player().activeCharacterKind() - 1; // there is no "None" character kind in V1
 	}
 
 	void handleReturnFromKernelCall(int32 returnValue) {
@@ -540,6 +540,23 @@ private:
 		return entry._number;
 	}
 
+	MainCharacterKind getMainCharacterKindArg(uint argI) {
+		int32 value = getNumberArg(argI);
+		if (g_engine->isV3()) {
+			if (value < 0 || value > 2)
+				error("Unexpected value for main character kind: %d", value);
+			else
+				return (MainCharacterKind)value;
+		}
+		else {
+			if (value < 0 || value > 1)
+				error("Unexpected value for main character kind: %d", value);
+			return value == 0
+				? MainCharacterKind::Mortadelo
+				: MainCharacterKind::Filemon;
+		}
+	}
+
 	const char *getStringArg(uint argI) {
 		auto entry = getArg(argI);
 		if (entry._type != StackEntryType::String)
@@ -638,12 +655,12 @@ private:
 			g_engine->scheduler().createProcess<ScriptTask>(process().character(), *this);
 			return TaskReturn::finish(0); // 0 means this is the forking process
 		case ScriptKernelTask::KillProcesses:
-			killProcessesFor((MainCharacterKind)getNumberArg(0));
+			killProcessesFor(getMainCharacterKindArg(0));
 			return TaskReturn::finish(1);
 
 		// player/world state changes
 		case ScriptKernelTask::ChangeCharacter: {
-			MainCharacterKind kind = (MainCharacterKind)getNumberArg(0);
+			MainCharacterKind kind = getMainCharacterKindArg(0);
 			killProcessesFor(MainCharacterKind::None); // yes, kill for all characters
 			auto &camera = g_engine->camera();
 			auto &player = g_engine->player();
@@ -846,7 +863,7 @@ private:
 			relatedCharacter().pickup(getStringArg(0), !getNumberArg(1));
 			return TaskReturn::finish(1);
 		case ScriptKernelTask::CharacterPickup: {
-			auto &character = g_engine->world().getMainCharacterByKind((MainCharacterKind)getNumberArg(1));
+			auto &character = g_engine->world().getMainCharacterByKind(getMainCharacterKindArg(1));
 			character.pickup(getStringArg(0), !getNumberArg(2));
 			return TaskReturn::finish(1);
 		}
@@ -854,12 +871,12 @@ private:
 			relatedCharacter().drop(getStringArg(0));
 			return TaskReturn::finish(1);
 		case ScriptKernelTask::CharacterDrop: {
-			auto &character = g_engine->world().getMainCharacterByKind((MainCharacterKind)getNumberArg(1));
+			auto &character = g_engine->world().getMainCharacterByKind(getMainCharacterKindArg(1));
 			character.drop(getOptionalStringArg(0));
 			return TaskReturn::finish(1);
 		}
 		case ScriptKernelTask::ClearInventory:
-			switch ((MainCharacterKind)getNumberArg(0)) {
+			switch (getMainCharacterKindArg(0)) {
 			case MainCharacterKind::Mortadelo:
 				g_engine->world().mortadelo().clearInventory();
 				break;
@@ -877,8 +894,9 @@ private:
 			return TaskReturn::waitFor(g_engine->camera().waitToStop(process()));
 		case ScriptKernelTask::CamFollow: {
 			WalkingCharacter *target = nullptr;
-			if (getNumberArg(0) != 0)
-				target = &g_engine->world().getMainCharacterByKind((MainCharacterKind)getNumberArg(0));
+			auto kind = getMainCharacterKindArg(0);
+			if (kind != MainCharacterKind::None)
+				target = &g_engine->world().getMainCharacterByKind(kind);
 			g_engine->camera().setFollow(target, getNumberArg(1) != 0);
 			return TaskReturn::finish(1);
 		}


Commit: f880658010ab8a3f2e3985f6474bcc73443bc8ba
    https://github.com/scummvm/scummvm/commit/f880658010ab8a3f2e3985f6474bcc73443bc8ba
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:27+01:00

Commit Message:
ALCACHOFA: Adapt V1 text layout and rendering

Changed paths:
    engines/alcachofa/graphics.cpp
    engines/alcachofa/graphics.h


diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index b431223d5fd..afbf83b70b5 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -630,7 +630,33 @@ void Animation::drawEffect(int32 frameI, Vector3d topLeft, Vector2d size, Vector
 	renderer.quad(as2D(topLeft), size, kWhite, rotation, texMin + texOffset, texMax + texOffset);
 }
 
-Font::Font(GameFileReference fileRef) : AnimationBase(move(fileRef)) {}
+Font::Font(GameFileReference fileRef)
+	: AnimationBase(move(fileRef))
+	, _charToImage(g_engine->isV1() ? 33 : 32)
+	, _spaceImageI(g_engine->isV1() ? 94 : 0)
+	, _charSpacing(g_engine->isV1() ? 3 : 0) {}
+
+static void fixFontAtlasColors(ManagedSurface &surface) {
+	// In V1 the font contains green and black pixels where
+	//  - black pixels should stay black
+	//  - green pixels should be the text color
+	// The image segments determine the alpha.
+	// For rendering in OpenGL we want the green pixels to be white
+	// so multiplication with the text color still works (BlendMode::Tinted)
+	assert(surface.format.bytesPerPixel == 4);
+	const uint32 alphaMask = uint32(255) << surface.format.aShift;
+	const uint32 black = alphaMask;
+	const uint32 white = ~0;
+
+	for (int16 y = 0; y < surface.h; y++) {
+		uint32 *pixel = (uint32 *)surface.getBasePtr(0, y);
+		for (int16 x = 0; x < surface.w; x++, pixel++) {
+			auto alpha = *pixel & alphaMask;
+			*pixel = !alpha ? *pixel
+				: (*pixel & ~alphaMask) ? white : black;
+		}
+	}
+}
 
 void Font::load() {
 	if (_isLoaded)
@@ -668,6 +694,9 @@ void Font::load() {
 		_texMaxs[i].setX((offsetX + _images[i]->w) * invWidth);
 		_texMaxs[i].setY((offsetY + _images[i]->h) * invHeight);
 	}
+	if (g_engine->isV1())
+		fixFontAtlasColors(atlasSurface);
+
 	_texture = g_engine->renderer().createTexture(atlasSurface.w, atlasSurface.h, false);
 	_texture->update(atlasSurface);
 	debugCN(1, kDebugGraphics, "Rendered font atlas %s at %dx%d", _fileRef._path.c_str(), atlasSurface.w, atlasSurface.h);
@@ -682,9 +711,15 @@ void Font::freeImages() {
 	_texMaxs.clear();
 }
 
-void Font::drawCharacter(int32 imageI, Point centerPoint, Color color) {
-	assert(imageI >= 0 && (uint)imageI < _images.size());
-	Vector2d center = as2D(centerPoint + _imageOffsets[imageI]);
+void Font::drawCharacter(byte ch, Point centerPoint, Color color) {
+	if (!isVisibleChar(ch))
+		return;
+
+	int32 imageI = ch - _charToImage;
+	Point offset = g_engine->isV1()
+		? Point(0, spaceSize().y - _images[imageI]->h)
+		: _imageOffsets[imageI];
+	Vector2d center = as2D(centerPoint + offset);
 	Vector2d size(_images[imageI]->w, _images[imageI]->h);
 
 	auto &renderer = g_engine->renderer();
@@ -693,6 +728,22 @@ void Font::drawCharacter(int32 imageI, Point centerPoint, Color color) {
 	renderer.quad(center, size, color, Angle(), _texMins[imageI], _texMaxs[imageI]);
 }
 
+bool Font::isVisibleChar(byte ch) const {
+	return ch >= _charToImage &&
+		(uint)(ch - _charToImage) < _images.size() &&
+		ch - _charToImage != _spaceImageI;
+}
+
+Point Font::characterSize(byte ch) const {
+	return isVisibleChar(ch)
+		? imageSize(ch - _charToImage) + Point(_charSpacing, 0)
+		: imageSize(_spaceImageI);
+}
+
+Point Font::spaceSize() const {
+	return imageSize(_spaceImageI);
+}
+
 Graphic::Graphic() {}
 
 Graphic::Graphic(SeekableReadStream &stream) {
@@ -870,14 +921,6 @@ static const byte *trimTrailing(const byte *text, const byte *begin, bool trimSp
 	return text;
 }
 
-static Point characterSize(const Font &font, byte ch) {
-	if (ch <= ' ' || (uint)(ch - ' ') >= font.imageCount())
-		ch = 0;
-	else
-		ch -= ' ';
-	return font.imageSize(ch);
-}
-
 TextDrawRequest::TextDrawRequest(Font &font, const char *originalText, Point pos, int maxWidth, bool centered, Color color, int8 order)
 	: IDrawRequest(order)
 	, _font(font)
@@ -904,7 +947,7 @@ TextDrawRequest::TextDrawRequest(Font &font, const char *originalText, Point pos
 		}
 
 		if (*itChar != '\r' && *itChar)
-			lineWidth += characterSize(font, *itChar).x;
+			lineWidth += font.characterSize(*itChar).x;
 		if (lineWidth <= maxWidth && *itChar != '\r' && *itChar) {
 			itChar++;
 			continue;
@@ -936,7 +979,7 @@ TextDrawRequest::TextDrawRequest(Font &font, const char *originalText, Point pos
 		lineWidth = 0;
 		for (auto ch : _lines[i]) {
 			if (ch != '\r' && ch)
-				lineWidth += characterSize(font, ch).x;
+				lineWidth += font.characterSize(ch).x;
 		}
 		_posX[i] = lineWidth;
 		_width = MAX(_width, lineWidth);
@@ -954,7 +997,7 @@ TextDrawRequest::TextDrawRequest(Font &font, const char *originalText, Point pos
 		fill(_posX.begin(), _posX.end(), pos.x);
 
 	// setup height and y position
-	_height = (int)lineCount * (font.imageSize(0).y * 4 / 3);
+	_height = (int)lineCount * (font.spaceSize().y * 4 / 3);
 	_posY = pos.y;
 	if (centered)
 		_posY -= _height / 2;
@@ -965,17 +1008,15 @@ TextDrawRequest::TextDrawRequest(Font &font, const char *originalText, Point pos
 }
 
 void TextDrawRequest::draw() {
-	const Point spaceSize = _font.imageSize(0);
 	Point cursor(0, _posY);
 	for (uint i = 0; i < _lines.size(); i++) {
 		cursor.x = _posX[i];
 		for (auto ch : _lines[i]) {
-			const Point charSize = characterSize(_font, ch);
-			if (ch > ' ' && (uint)(ch - ' ') < _font.imageCount())
-				_font.drawCharacter(ch - ' ', Point(cursor.x, cursor.y), _color);
+			const Point charSize = _font.characterSize(ch);
+			_font.drawCharacter(ch, Point(cursor.x, cursor.y), _color);
 			cursor.x += charSize.x;
 		}
-		cursor.y += spaceSize.y * 4 / 3;
+		cursor.y += _font.spaceSize().y * 4 / 3;
 	}
 }
 
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 384f35a312f..55649d22100 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -276,13 +276,17 @@ public:
 
 	void load();
 	void freeImages();
-	void drawCharacter(int32 imageI, Common::Point center, Color color);
+	void drawCharacter(byte ch, Common::Point center, Color color);
 
 	using AnimationBase::isLoaded;
 	using AnimationBase::imageSize;
+	bool isVisibleChar(byte ch) const;
+	Common::Point characterSize(byte ch) const;
+	Common::Point spaceSize() const;
 	inline uint imageCount() const { return _images.size(); }
 
 private:
+	const byte _charToImage, _spaceImageI, _charSpacing;
 	Common::Array<Math::Vector2d> _texMins, _texMaxs;
 	Common::ScopedPtr<ITexture> _texture;
 };


Commit: 783301dbcf30b1d7f2c561448ba894530d214174
    https://github.com/scummvm/scummvm/commit/783301dbcf30b1d7f2c561448ba894530d214174
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:27+01:00

Commit Message:
ALCACHOFA: Fix room bounds for V1

Changed paths:
    engines/alcachofa/camera.cpp
    engines/alcachofa/camera.h
    engines/alcachofa/graphics.cpp
    engines/alcachofa/rooms.cpp
    engines/alcachofa/rooms.h


diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 565025839ab..6e72525ed29 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -37,6 +37,13 @@ void Camera::resetRotationAndScale() {
 	_cur._usedCenter.z() = 0;
 }
 
+void Camera::setRoomBounds(Point min, Point size, int16 bgScale) {
+	float scaleFactor = bgScale * kInvBaseScale;
+	_roomMin = as2D(min) * scaleFactor;
+	_roomMax = _roomMin + as2D(size) * scaleFactor;
+	_roomScale = 0.0f;
+}
+
 void Camera::setRoomBounds(Point bgSize, int16 bgScale) {
 	float scaleFactor = 1 - bgScale * kInvBaseScale;
 	_roomMin = Vector2d(
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index f22a44bb6a2..a5a1573e5a3 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -46,7 +46,8 @@ public:
 	Math::Vector3d transform3Dto2D(Math::Vector3d v) const;
 	Common::Point transform3Dto2D(Common::Point p) const;
 	void resetRotationAndScale();
-	void setRoomBounds(Common::Point bgSize, int16 bgScale);
+	void setRoomBounds(Common::Point min, Common::Point size, int16 bgScale); ///< Used in V1
+	void setRoomBounds(Common::Point bgSize, int16 bgScale); ///< Used in V3
 	void setFollow(WalkingCharacter *target, bool catchUp = false);
 	void setPosition(Math::Vector2d v);
 	void setPosition(Math::Vector3d v);
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index afbf83b70b5..076ac56be8b 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -568,6 +568,15 @@ void Animation::prerenderFrame(int32 frameI) {
 	_renderedPremultiplyAlpha = _premultiplyAlpha;
 }
 
+struct TexCoords {
+	TexCoords(const Rect &inner, int16 outerW, int16 outerH) {
+		_min = Vector2d(0.5f / outerW, 0.5f / outerH);
+		_max = Vector2d((inner.width() - 0.5f) / outerW, (inner.height() - 0.5f) / outerH);
+	}
+
+	Vector2d _min, _max;
+};
+
 void Animation::outputRect2D(int32 frameI, float scale, Vector2d &topLeft, Vector2d &size) const {
 	auto bounds = frameBounds(frameI);
 	topLeft += as2D(totalFrameOffset(frameI)) * scale;
@@ -577,8 +586,7 @@ void Animation::outputRect2D(int32 frameI, float scale, Vector2d &topLeft, Vecto
 void Animation::draw2D(int32 frameI, Vector2d topLeft, float scale, BlendMode blendMode, Color color) {
 	prerenderFrame(frameI);
 	auto bounds = frameBounds(frameI);
-	Vector2d texMin(0, 0);
-	Vector2d texMax((float)bounds.width() / _renderedSurface.w, (float)bounds.height() / _renderedSurface.h);
+	TexCoords tex(bounds, _renderedSurface.w, _renderedSurface.h);
 
 	Vector2d size;
 	outputRect2D(frameI, scale, topLeft, size);
@@ -586,7 +594,7 @@ void Animation::draw2D(int32 frameI, Vector2d topLeft, float scale, BlendMode bl
 	auto &renderer = g_engine->renderer();
 	renderer.setTexture(_renderedTexture.get());
 	renderer.setBlendMode(blendMode);
-	renderer.quad(topLeft, size, color, Angle(), texMin, texMax);
+	renderer.quad(topLeft, size, color, Angle(), tex._min, tex._max);
 }
 
 void Animation::outputRect3D(int32 frameI, float scale, Vector3d &topLeft, Vector2d &size) const {
@@ -599,8 +607,7 @@ void Animation::outputRect3D(int32 frameI, float scale, Vector3d &topLeft, Vecto
 void Animation::draw3D(int32 frameI, Vector3d topLeft, float scale, BlendMode blendMode, Color color) {
 	prerenderFrame(frameI);
 	auto bounds = frameBounds(frameI);
-	Vector2d texMin(0, 0);
-	Vector2d texMax((float)bounds.width() / _renderedSurface.w, (float)bounds.height() / _renderedSurface.h);
+	TexCoords tex(bounds, _renderedSurface.w, _renderedSurface.h);
 
 	Vector2d size;
 	outputRect3D(frameI, scale, topLeft, size);
@@ -609,14 +616,13 @@ void Animation::draw3D(int32 frameI, Vector3d topLeft, float scale, BlendMode bl
 	auto &renderer = g_engine->renderer();
 	renderer.setTexture(_renderedTexture.get());
 	renderer.setBlendMode(blendMode);
-	renderer.quad(as2D(topLeft), size, color, rotation, texMin, texMax);
+	renderer.quad(as2D(topLeft), size, color, rotation, tex._min, tex._max);
 }
 
 void Animation::drawEffect(int32 frameI, Vector3d topLeft, Vector2d size, Vector2d texOffset, BlendMode blendMode) {
 	prerenderFrame(frameI);
 	auto bounds = frameBounds(frameI);
-	Vector2d texMin(0, 0);
-	Vector2d texMax((float)bounds.width() / _renderedSurface.w, (float)bounds.height() / _renderedSurface.h);
+	TexCoords tex(bounds, _renderedSurface.w, _renderedSurface.h);
 
 	topLeft += as3D(totalFrameOffset(frameI));
 	topLeft = g_engine->camera().transform3Dto2D(topLeft);
@@ -627,7 +633,7 @@ void Animation::drawEffect(int32 frameI, Vector3d topLeft, Vector2d size, Vector
 	auto &renderer = g_engine->renderer();
 	renderer.setTexture(_renderedTexture.get());
 	renderer.setBlendMode(blendMode);
-	renderer.quad(as2D(topLeft), size, kWhite, rotation, texMin + texOffset, texMax + texOffset);
+	renderer.quad(as2D(topLeft), size, kWhite, rotation, tex._min + texOffset, tex._max + texOffset);
 }
 
 Font::Font(GameFileReference fileRef)
@@ -646,7 +652,7 @@ static void fixFontAtlasColors(ManagedSurface &surface) {
 	assert(surface.format.bytesPerPixel == 4);
 	const uint32 alphaMask = uint32(255) << surface.format.aShift;
 	const uint32 black = alphaMask;
-	const uint32 white = ~0;
+	const uint32 white = ~uint32(0);
 
 	for (int16 y = 0; y < surface.h; y++) {
 		uint32 *pixel = (uint32 *)surface.getBasePtr(0, y);
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 29e4569c981..90ce2e9f843 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -39,10 +39,13 @@ Room::Room(World *world)
 Room::Room(World *world, SeekableReadStream &stream)
 	: _world(world)
 	, _mapIndex(world->loadedMapCount()) {
-	if (g_engine->isV1())
-		readRoomV1(stream, true);
+	if (g_engine->isV1()) {
+		readRoomV1(stream);
+		readObjects(stream);
+	}
 	else
 		readRoomV3(stream, false);
+	initBackground();
 }
 
 static ObjectBase *readRoomObject(Room *room, const String &type, SeekableReadStream &stream) {
@@ -98,20 +101,12 @@ static ObjectBase *readRoomObject(Room *room, const String &type, SeekableReadSt
 		return nullptr; // handled in Room::Room
 }
 
-void Room::readRoomV1(SeekableReadStream &stream, bool shouldReadObjects) {
+void Room::readRoomV1(SeekableReadStream &stream) {
 	_name = readVarString(stream);
 	_backgroundName = readVarString(stream);
 	_musicId = (int)stream.readByte();
 	_characterAlphaTint = stream.readByte();
 	skipVarString(stream);
-
-	if (shouldReadObjects)
-		readObjects(stream, kBaseScale);
-
-	if (!_backgroundName.empty() && g_engine->game().doesRoomHaveBackground(this)) {
-		// in V1 _backgroundName refers to an object which represents the background
-		_backgroundObject = getObjectByName(_backgroundName.c_str());
-	}
 }
 
 void Room::readRoomV3(SeekableReadStream &stream, bool hasUselessByte) {
@@ -119,7 +114,7 @@ void Room::readRoomV3(SeekableReadStream &stream, bool hasUselessByte) {
 	_backgroundName = _name;
 	_musicId = (int)stream.readByte();
 	_characterAlphaTint = stream.readByte();
-	auto backgroundScale = stream.readSint16LE();
+	_backgroundScale = stream.readSint16LE();
 	_floors[0] = PathFindingShape(stream);
 	_floors[1] = PathFindingShape(stream);
 	_fixedCameraOnEntering = readBool(stream);
@@ -128,16 +123,25 @@ void Room::readRoomV3(SeekableReadStream &stream, bool hasUselessByte) {
 	if (hasUselessByte)
 		stream.readByte();
 
-	readObjects(stream, backgroundScale);
+	readObjects(stream);
+}
 
-	if (g_engine->game().doesRoomHaveBackground(this)) {
-		// in V3 _background is the name of an animation, we have to create the object
-		_backgroundObject = new Background(this, _backgroundName, backgroundScale);
+void Room::initBackground() {
+	if (!g_engine->game().doesRoomHaveBackground(this))
+		return;
+
+	if (g_engine->isV1()) {
+		// in V1 _backgroundName refers to an object which represents the background
+		if (!_backgroundName.empty())
+			_backgroundObject = getObjectByName(_backgroundName.c_str());
+	} else {
+		// in V3 _backgroundName is the name of an animation, we have to create the object
+		_backgroundObject = new Background(this, _backgroundName, _backgroundScale);
 		_objects.push_back(_backgroundObject);
 	}
 }
 
-void Room::readObjects(SeekableReadStream &stream, int16 backgroundScale) {
+void Room::readObjects(SeekableReadStream &stream) {
 	uint32 objectEnd = stream.readUint32LE();
 	while (objectEnd > 0) {
 		const auto type = readVarString(stream);
@@ -161,10 +165,11 @@ void Room::readObjects(SeekableReadStream &stream, int16 backgroundScale) {
 }
 
 RoomWithFloor::RoomWithFloor(World *world, SeekableReadStream &stream) : Room(world) {
-	readRoomV1(stream, false);
+	readRoomV1(stream);
 	_floors[0] = PathFindingShape(stream);
 	_floors[1] = PathFindingShape(stream);
-	readObjects(stream, kBaseScale);
+	readObjects(stream);
+	initBackground();
 }
 
 Room::~Room() {
@@ -268,13 +273,20 @@ void Room::updateRoomBounds() {
 	auto graphic = _backgroundObject == nullptr ? nullptr : _backgroundObject->graphic();
 	if (graphic != nullptr) {
 		auto bgSize = graphic->animation().imageSize(0);
-		/* This fixes a bug where if the background image is invalid the original engine
-		 * would not update the background size. This would be around 1024,768 due to
-		 * previous rooms in the bug instances I found.
-		 */
-		if (bgSize == Point(0, 0))
-			bgSize = Point(1024, 768);
-		g_engine->camera().setRoomBounds(bgSize, graphic->scale());
+		if (g_engine->isV1()) {
+			g_engine->camera().setRoomBounds(
+				graphic->topLeft() + Point(400, 300),
+				bgSize - Point(800, 600),
+				graphic->scale());
+		} else {
+			/* The fallback fixes a bug where if the background image is invalid the original engine
+			 * would not update the background size. This would be around 1024,768 due to
+			 * previous rooms in the bug instances I found.
+			 */
+			if (bgSize == Point(0, 0))
+				bgSize = Point(1024, 768);
+			g_engine->camera().setRoomBounds(bgSize, graphic->scale());
+		}
 	}
 }
 
@@ -366,6 +378,7 @@ ShapeObject *Room::getSelectedObject(ShapeObject *best) const {
 
 OptionsMenu::OptionsMenu(World *world, SeekableReadStream &stream) : Room(world) {
 	readRoomV3(stream, true);
+	initBackground();
 }
 
 bool OptionsMenu::updateInput() {
@@ -401,19 +414,22 @@ void OptionsMenu::clearLastSelectedObject() {
 
 ConnectMenu::ConnectMenu(World *world, SeekableReadStream &stream) : Room(world) {
 	readRoomV3(stream, true);
+	initBackground();
 }
 
 ListenMenu::ListenMenu(World *world, SeekableReadStream &stream) : Room(world) {
 	readRoomV3(stream, true);
+	initBackground();
 }
 
 Inventory::Inventory(World *world, SeekableReadStream &stream) : Room(world) {
 	if (g_engine->isV1()) {
-		readRoomV1(stream, false);
+		readRoomV1(stream);
 		stream.skip(1); // denoted as "sinusar" but unused
-		readObjects(stream, kBaseScale);
+		readObjects(stream);
 	} else
 		readRoomV3(stream, true);
+	initBackground();
 }
 
 Inventory::~Inventory() {
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index a07acce1567..3d3f9fda9ca 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -67,9 +67,10 @@ public:
 
 protected:
 	Room(World *world);
-	void readRoomV1(Common::SeekableReadStream &stream, bool readObjects);
+	void readRoomV1(Common::SeekableReadStream &stream);
 	void readRoomV3(Common::SeekableReadStream &stream, bool hasUselessByte);
-	void readObjects(Common::SeekableReadStream &stream, int16 backgroundScale);
+	void readObjects(Common::SeekableReadStream &stream);
+	void initBackground();
 	void updateScripts();
 	void updateRoomBounds();
 	void updateInteraction();
@@ -85,6 +86,7 @@ protected:
 	bool _fixedCameraOnEntering = false;
 	int8 _activeFloorI = -1;
 	int _musicId = -1;
+	int16 _backgroundScale = kBaseScale;
 	uint8
 		_mapIndex,
 		_characterAlphaTint = 0,


Commit: d182cef022b91b836040f928befa2c10ef0849a3
    https://github.com/scummvm/scummvm/commit/d182cef022b91b836040f928befa2c10ef0849a3
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:27+01:00

Commit Message:
ALCACHOFA: Fix faulty rebase

Changed paths:
    engines/alcachofa/alcachofa.h
    engines/alcachofa/game-movie-adventure-special.cpp
    engines/alcachofa/game.cpp
    engines/alcachofa/game.h
    engines/alcachofa/global-ui.cpp


diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 04386c5e1af..8e25ea0a865 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -117,10 +117,6 @@ public:
 	inline bool isV3() const { return gameDescription().isVersionBetween(30, 39); }
 
 	inline const AlcachofaGameDescription &gameDescription() const { return *_gameDescription; }
-	inline bool isV1() const { return _gameDescription->engineVersion == EngineVersion::V1; }
-	inline bool isV2() const { return _gameDescription->engineVersion == EngineVersion::V2; }
-	inline bool isV3() const { return _gameDescription->engineVersion == EngineVersion::V3; }
-
 	inline IRenderer &renderer() { return *_renderer; }
 	inline DrawQueue &drawQueue() { return *_drawQueue; }
 	inline Camera &camera() { return _camera; }
diff --git a/engines/alcachofa/game-movie-adventure-special.cpp b/engines/alcachofa/game-movie-adventure-special.cpp
index 7f1e4469592..79f7fb6fe6f 100644
--- a/engines/alcachofa/game-movie-adventure-special.cpp
+++ b/engines/alcachofa/game-movie-adventure-special.cpp
@@ -241,17 +241,6 @@ public:
 		script.variable("textoson") = g_engine->config().subtitles() ? 1 : 0;
 	}
 
-	void updateScriptVariables() override {
-		Script &script = g_engine->script();
-		if (g_engine->input().wasAnyMousePressed()) // yes, this variable is never reset by the engine (only by script)
-			script.variable("SeHaPulsadoRaton") = 1;
-
-		script.setScriptTimer(!script.variable("CalcularTiempoSinPulsarRaton"));
-		script.variable("EstanAmbos") = g_engine->world().mortadelo().room() == g_engine->world().filemon().room();
-		script.variable("textoson") = g_engine->config().subtitles() ? 1 : 0;
-		script.variable("modored") = 0; // this is signalling whether a network connection is established
-	}
-
 	Path getVideoPath(int32 videoId) override {
 		return Path(String::format("Data/DATA%02d.BIN", videoId + 1));
 	}
diff --git a/engines/alcachofa/game.cpp b/engines/alcachofa/game.cpp
index a77b0ca7f73..46767b7b393 100644
--- a/engines/alcachofa/game.cpp
+++ b/engines/alcachofa/game.cpp
@@ -207,9 +207,10 @@ void Game::invalidVideo(int32 videoId, const char *context) {
 Game *Game::create() {
 	const auto &desc = g_engine->gameDescription();
 	switch (desc.engineVersion) {
-	case EngineVersion::V1:
+	case EngineVersion::V1_0:
 		return createForMovieAdventureOriginal();
-	case EngineVersion::V3:
+	case EngineVersion::V3_0:
+	case EngineVersion::V3_1:
 		return createForMovieAdventureSpecial();
 	default:
 		error("Unsupported or invalid engine version: %d", (int)desc.engineVersion);
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index 63d3833238a..9cfd282cd96 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -56,16 +56,12 @@ public:
 	virtual Common::Span<const ScriptOp> getScriptOpMap() = 0;
 	virtual Common::Span<const ScriptKernelTask> getScriptKernelTaskMap() = 0;
 	virtual void updateScriptVariables() = 0;
-	virtual bool shouldClipCamera() = 0;
 	virtual void drawScreenStates();
 	virtual const char *getDialogFileName() = 0;
 	virtual const char *getObjectFileName() = 0;
 	virtual char getTextFileKey() = 0;
 	virtual const char *getInitScriptName() = 0;
-	virtual Common::Span<const ScriptOp> getScriptOpMap() = 0;
-	virtual Common::Span<const ScriptKernelTask> getScriptKernelTaskMap() = 0;
 	virtual int32 getKernelTaskArgCount(int32 kernelTaskI); // only necessary for V1
-	virtual void updateScriptVariables() = 0;
 	virtual Common::Path getVideoPath(int32 videoId) = 0;
 	virtual Common::String getSoundPath(const char *filename) = 0; ///< Without file-extension
 	virtual Common::String getMusicPath(int32 trackId) = 0; ///< Without file-extension
diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index 67415976a1e..4164dec0fc0 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -252,7 +252,6 @@ void GlobalUI::drawScreenStates() {
 		return;
 
 	auto &drawQueue = g_engine->drawQueue();
-	int32 borderWidth;
 	if (_isPermanentFaded)
 		drawQueue.add<FadeDrawRequest>(FadeType::ToBlack, 1.0f, -9);
 	else


Commit: d10f4db574f7a6108937b0971c2ecd5baac81635
    https://github.com/scummvm/scummvm/commit/d10f4db574f7a6108937b0971c2ecd5baac81635
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:27+01:00

Commit Message:
ALCACHOFA: Remove unused isGameLoaded variable

Changed paths:
    engines/alcachofa/global-ui.cpp
    engines/alcachofa/menu.cpp
    engines/alcachofa/player.cpp
    engines/alcachofa/player.h
    engines/alcachofa/rooms.cpp


diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index 4164dec0fc0..f58cfc42839 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -79,7 +79,7 @@ void GlobalUI::updateClosingInventory() {
 
 bool GlobalUI::updateOpeningInventory() {
 	static constexpr float kSpeed = 10 / 3.0f / 1000.0f;
-	if (g_engine->menu().isOpen() || !g_engine->player().isGameLoaded())
+	if (g_engine->menu().isOpen())
 		return false;
 
 	const bool userWantsToOpenInventory =
@@ -127,7 +127,6 @@ bool GlobalUI::isHoveringChangeButton() const {
 bool GlobalUI::updateChangingCharacter() {
 	auto &player = g_engine->player();
 	if (g_engine->menu().isOpen() ||
-		!player.isGameLoaded() ||
 		_isOpeningInventory)
 		return false;
 	_changeButton.frameI() = 0;
@@ -164,7 +163,6 @@ bool GlobalUI::updateChangingCharacter() {
 void GlobalUI::drawChangingButton() {
 	auto &player = g_engine->player();
 	if (g_engine->menu().isOpen() ||
-		!player.isGameLoaded() ||
 		!player.semaphore().isReleased() ||
 		_isOpeningInventory ||
 		_isClosingInventory)
diff --git a/engines/alcachofa/menu.cpp b/engines/alcachofa/menu.cpp
index 7749c49bab0..75ff0c669d7 100644
--- a/engines/alcachofa/menu.cpp
+++ b/engines/alcachofa/menu.cpp
@@ -215,7 +215,6 @@ void Menu::triggerMainMenuAction(MainMenuAction action) {
 		break;
 	case MainMenuAction::NewGame:
 		// this action might be unused just like the only room it would appear: MENUPRINCIPALINICIO
-		g_engine->player().isGameLoaded() = true;
 		g_engine->script().createProcess(MainCharacterKind::None, g_engine->world().initScriptName());
 		break;
 	default:
@@ -360,10 +359,7 @@ void Menu::triggerOptionsValue(OptionsMenuValue valueId, float value) {
 void Menu::continueMainMenu() {
 	g_engine->config().saveToScummVM();
 	g_engine->syncSoundSettings();
-	g_engine->player().changeRoom(
-		g_engine->player().isGameLoaded() ? "MENUPRINCIPAL" : "MENUPRINCIPALINICIO",
-		true
-	);
+	g_engine->player().changeRoom("MENUPRINCIPAL", true);
 
 	updateSelectedSavefile(false);
 }
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 6d87fff0e7b..7d51681b615 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -53,7 +53,7 @@ void Player::resetCursor() {
 void Player::updateCursor() {
 	if (g_engine->isV1())
 		_cursorFrameI = 0;
-	else if (g_engine->menu().isOpen() || !_isGameLoaded)
+	else if (g_engine->menu().isOpen())
 		_cursorFrameI = 0;
 	else if (_selectedObject == nullptr)
 		_cursorFrameI = !g_engine->input().isMouseLeftDown() || _pressedObject != nullptr ? 6 : 7;
@@ -349,7 +349,6 @@ void Player::setActiveCharacter(MainCharacterKind kind) {
 
 bool Player::isAllowedToOpenMenu() {
 	return
-		isGameLoaded() &&
 		!g_engine->menu().isOpen() &&
 		g_engine->sounds().musicSemaphore().isReleased() &&
 		g_engine->game().isAllowedToOpenMenu() &&
@@ -386,7 +385,6 @@ void Player::syncGame(Serializer &s) {
 		_pressedObject = nullptr;
 		_heldItem = nullptr;
 		_nextLastDialogCharacter = 0;
-		_isGameLoaded = true;
 		_roomBeforeInventory = nullptr;
 		_isInTemporaryRoom = false;
 		fill(_lastDialogCharacters, _lastDialogCharacters + kMaxLastDialogCharacters, nullptr);
diff --git a/engines/alcachofa/player.h b/engines/alcachofa/player.h
index 99fc0476857..094f9b33cbd 100644
--- a/engines/alcachofa/player.h
+++ b/engines/alcachofa/player.h
@@ -39,8 +39,6 @@ public:
 	MainCharacter *inactiveCharacter() const;
 	FakeSemaphore &semaphoreFor(MainCharacterKind kind);
 
-	inline bool &isGameLoaded() { return _isGameLoaded; }
-
 	inline MainCharacterKind activeCharacterKind() const {
 		return _activeCharacter == nullptr ? MainCharacterKind::None : _activeCharacter->kind();
 	}
@@ -72,9 +70,7 @@ private:
 	void *_pressedObject = nullptr; // terrible but GlobalUI wants to store a Graphic pointer
 	Item *_heldItem = nullptr;
 	int32 _cursorFrameI = 0;
-	bool
-		_isGameLoaded = true,
-		_didLoadGlobalRooms = false,
+	bool _didLoadGlobalRooms = false,
 		_isInTemporaryRoom = false;
 	Character *_lastDialogCharacters[kMaxLastDialogCharacters] = { nullptr };
 	int _nextLastDialogCharacter = 0;
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 90ce2e9f843..fff502f70c4 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -230,7 +230,7 @@ bool Room::updateInput() {
 
 	bool canInteract = !player.activeCharacter()->isBusy();
 	// A complicated network condition can prevent interaction at this point
-	if (g_engine->menu().isOpen() || !player.isGameLoaded())
+	if (g_engine->menu().isOpen())
 		canInteract = true;
 	if (canInteract) {
 		player.resetCursor();


Commit: f27397c0c0d30f982f5b53558af01b823b34628d
    https://github.com/scummvm/scummvm/commit/f27397c0c0d30f982f5b53558af01b823b34628d
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:29+01:00

Commit Message:
ALCACHOFA: Fix disguise kernel proc and prepare locking changes

The new game versions use different locking strategies. Also sometimes semaphores are *unlocked* during some section. This requires some infrastructure changes to support both worlds (and hopefully the next couple engine versions as well)

Changed paths:
    engines/alcachofa/alcachofa.h
    engines/alcachofa/detection_tables.h
    engines/alcachofa/game-movie-adventure-original.cpp
    engines/alcachofa/game-movie-adventure-special.cpp
    engines/alcachofa/game.h
    engines/alcachofa/input.cpp
    engines/alcachofa/input.h
    engines/alcachofa/scheduler.cpp
    engines/alcachofa/scheduler.h
    engines/alcachofa/script.cpp
    engines/alcachofa/tasks.h


diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 8e25ea0a865..cd9fa49ad57 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -60,10 +60,11 @@ static constexpr int16 kBigThumbnailWidth = 341; // for in-game
 static constexpr int16 kBigThumbnailHeight = 256;
 
 
-enum class SaveVersion : Common::Serializer::Version {
-	Initial = 0
+namespace SaveVersion {
+	static constexpr const Common::Serializer::Version kInitial = 0;
+	static constexpr const Common::Serializer::Version kWithEngineV10 = 1;
 };
-static constexpr SaveVersion kCurrentSaveVersion = SaveVersion::Initial;
+static constexpr const Common::Serializer::Version kCurrentSaveVersion = SaveVersion::kInitial;
 
 class MySerializer : public Common::Serializer {
 public:
diff --git a/engines/alcachofa/detection_tables.h b/engines/alcachofa/detection_tables.h
index bee3943db5b..b907e020ed1 100644
--- a/engines/alcachofa/detection_tables.h
+++ b/engines/alcachofa/detection_tables.h
@@ -146,7 +146,7 @@ const AlcachofaGameDescription gameDescriptions[] = {
 		EngineVersion::V1_0
 	},
 
-	{ AD_TABLE_END_MARKER }
+	{ AD_TABLE_END_MARKER, EngineVersion::V1_0 }
 };
 
 } // End of namespace Alcachofa
diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index dd9c92fb060..e4c86af036b 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -219,6 +219,14 @@ public:
 		return dynamic_cast<RoomWithFloor *>(g_engine->player().currentRoom()) != nullptr;
 	}
 
+	bool shouldScriptLockInteraction() override {
+		return true;
+	}
+
+	bool shouldChangeCharacterUseGameLock() override {
+		return false;
+	}
+
 	void missingAnimation(const String &fileName) override {
 		static const char *exemptions[] = {
 			nullptr
diff --git a/engines/alcachofa/game-movie-adventure-special.cpp b/engines/alcachofa/game-movie-adventure-special.cpp
index 79f7fb6fe6f..fc921390f5b 100644
--- a/engines/alcachofa/game-movie-adventure-special.cpp
+++ b/engines/alcachofa/game-movie-adventure-special.cpp
@@ -261,6 +261,14 @@ public:
 		return !g_engine->script().variable("prohibirESC");
 	}
 
+	bool shouldScriptLockInteraction() override {
+		return false;
+	}
+
+	bool shouldChangeCharacterUseGameLock() override {
+		return true;
+	}
+
 	void onLoadedGameFiles() override {
 		// this notifies the script whether we are a demo
 		if (g_engine->world().loadedMapCount() == 2)
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index 9cfd282cd96..4001629b513 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -38,7 +38,7 @@ class Process;
 struct ScriptInstruction;
 
 /**
- * @brief Provides functionality specific to a game title.
+ * @brief Provides functionality specific to a game title / engine version.
  * Also includes all exemptions to inconsistencies in the original games.
  *
  * If an error is truly unrecoverable or a warning never an engine bug, no method is necessary here
@@ -67,6 +67,8 @@ public:
 	virtual Common::String getMusicPath(int32 trackId) = 0; ///< Without file-extension
 	virtual bool shouldClipCamera() = 0;
 	virtual bool isAllowedToOpenMenu() = 0; ///< only the game-specific condition
+	virtual bool shouldScriptLockInteraction() = 0;
+	virtual bool shouldChangeCharacterUseGameLock() = 0;
 
 	virtual bool doesRoomHaveBackground(const Room *room);
 	virtual void unknownRoomObject(const Common::String &type);
diff --git a/engines/alcachofa/input.cpp b/engines/alcachofa/input.cpp
index bacbd725e26..bbf89cbfa5e 100644
--- a/engines/alcachofa/input.cpp
+++ b/engines/alcachofa/input.cpp
@@ -102,4 +102,31 @@ void Input::updateMousePos3D() {
 	_mousePos3D = { (int16)pos3D.x(), (int16)pos3D.y() };
 }
 
+struct WaitForInputTask final : public Task {
+	WaitForInputTask(Process &process) : Task(process) {}
+
+	WaitForInputTask(Process &process, Serializer &s) : Task(process) {}
+
+	TaskReturn run() override {
+		TASK_BEGIN;
+		process().unlockInteraction();
+		do {
+			TASK_YIELD(1);
+		} while(!g_engine->input().wasAnyMousePressed());
+		process().lockInteraction();
+		TASK_END;
+	}
+
+	void debugPrint() override {
+		g_engine->getDebugger()->debugPrintf("Wait for input");
+	}
+
+	const char *taskName() const override;
+};
+DECLARE_TASK(WaitForInputTask)
+
+Task *Input::waitForInput(Process &process) {
+	return new WaitForInputTask(process);
+}
+
 }
diff --git a/engines/alcachofa/input.h b/engines/alcachofa/input.h
index 4f02eb13d7e..71285556b7f 100644
--- a/engines/alcachofa/input.h
+++ b/engines/alcachofa/input.h
@@ -27,6 +27,9 @@
 
 namespace Alcachofa {
 
+struct Task;
+class Process;
+
 class Input {
 public:
 	inline bool wasMouseLeftPressed() const { return _wasMouseLeftPressed; }
@@ -47,6 +50,7 @@ public:
 	void nextFrame();
 	bool handleEvent(const Common::Event &event);
 	void toggleDebugInput(bool debugMode); ///< Toggles input debug mode which blocks any input not retrieved with debugInput
+	Task *waitForInput(Process &process);
 
 private:
 	void updateMousePos3D();
diff --git a/engines/alcachofa/scheduler.cpp b/engines/alcachofa/scheduler.cpp
index 7266ca1b0be..4ff916198af 100644
--- a/engines/alcachofa/scheduler.cpp
+++ b/engines/alcachofa/scheduler.cpp
@@ -188,6 +188,25 @@ void Process::syncGame(Serializer &s) {
 			_tasks[i]->syncGame(s);
 		}
 	}
+
+	bool hasLock = !_interactionLock.isReleased();
+	s.syncAsByte(hasLock, SaveVersion::kWithEngineV10);
+	if (s.isLoading() && hasLock)
+		_interactionLock = FakeLock("process-interaction", g_engine->player().semaphore());
+}
+
+void Process::lockInteraction() {
+	if (!g_engine->game().shouldScriptLockInteraction())
+		return;
+	assert(_interactionLock.isReleased());
+	_interactionLock = FakeLock("process-interaction", g_engine->player().semaphore());
+}
+
+void Process::unlockInteraction() {
+	if (!g_engine->game().shouldScriptLockInteraction())
+		return;
+	assert(!_interactionLock.isReleased());
+	_interactionLock.release();
 }
 
 static void killProcessesForIn(MainCharacterKind characterKind, Array<Process *> &processes, uint firstIndex) {
diff --git a/engines/alcachofa/scheduler.h b/engines/alcachofa/scheduler.h
index fa88887be44..00eb07098f6 100644
--- a/engines/alcachofa/scheduler.h
+++ b/engines/alcachofa/scheduler.h
@@ -177,6 +177,8 @@ public:
 	TaskReturnType run();
 	void debugPrint();
 	void syncGame(Common::Serializer &s);
+	void lockInteraction();
+	void unlockInteraction();
 
 private:
 	friend class Scheduler;
@@ -185,6 +187,7 @@ private:
 	Common::Stack<Task *> _tasks;
 	Common::String _name;
 	int32 _lastReturnValue = 0;
+	FakeLock _interactionLock;
 };
 
 class Scheduler {
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 63b5fe7adb6..b70a21d8162 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -213,7 +213,7 @@ struct ScriptTask final : public Task {
 		, _script(g_engine->script())
 		, _name(name)
 		, _pc(pc)
-		, _lock(Common::move(lock)) {
+		, _characterLock(Common::move(lock)) {
 		pushInstruction(UINT_MAX);
 		debugC(SCRIPT_DEBUG_LVL_TASKS, kDebugScript, "%u: Script start at %u", process.pid(), pc);
 	}
@@ -223,7 +223,7 @@ struct ScriptTask final : public Task {
 		, _script(g_engine->script())
 		, _name(forkParent._name + " FORKED")
 		, _pc(forkParent._pc)
-		, _lock(forkParent._lock) {
+		, _characterLock(forkParent._characterLock) {
 		for (uint i = 0; i < forkParent._stack.size(); i++)
 			_stack.push(forkParent._stack[i]);
 		pushNumber(1); // this task is the forked one
@@ -237,6 +237,8 @@ struct ScriptTask final : public Task {
 	}
 
 	TaskReturn run() override {
+		if (_isFirstExecution)
+			process().lockInteraction();
 		if (_isFirstExecution || _returnsFromKernelCall)
 			setCharacterVariables();
 		if (_returnsFromKernelCall) {
@@ -411,7 +413,7 @@ struct ScriptTask final : public Task {
 	}
 
 	void syncGame(Serializer &s) override {
-		assert(s.isSaving() || (_lock.isReleased() && _stack.empty()));
+		assert(s.isSaving() || (_characterLock.isReleased() && _stack.empty()));
 
 		s.syncString(_name);
 		s.syncAsUint32LE(_pc);
@@ -428,10 +430,10 @@ struct ScriptTask final : public Task {
 				_stack[i].syncGame(s);
 		}
 
-		bool hasLock = !_lock.isReleased();
+		bool hasLock = !_characterLock.isReleased();
 		s.syncAsByte(hasLock);
 		if (s.isLoading() && hasLock)
-			_lock = FakeLock("script", g_engine->player().semaphoreFor(process().character()));
+			_characterLock = FakeLock("script-character", g_engine->player().semaphoreFor(process().character()));
 	}
 
 	const char *taskName() const override;
@@ -661,7 +663,6 @@ private:
 		// player/world state changes
 		case ScriptKernelTask::ChangeCharacter: {
 			MainCharacterKind kind = getMainCharacterKindArg(0);
-			killProcessesFor(MainCharacterKind::None); // yes, kill for all characters
 			auto &camera = g_engine->camera();
 			auto &player = g_engine->player();
 			camera.resetRotationAndScale();
@@ -672,9 +673,14 @@ private:
 				camera.setFollow(player.activeCharacter());
 				camera.backup(0);
 			}
-			process().character() = MainCharacterKind::None;
-			assert(player.semaphore().isReleased());
-			_lock = FakeLock("script", player.semaphore());
+
+			if (g_engine->game().shouldChangeCharacterUseGameLock()) {
+				killProcessesFor(MainCharacterKind::None); // yes, kill for all characters
+				kind = MainCharacterKind::None;
+			}
+			process().character() = kind;
+			_characterLock = FakeLock("script", player.semaphoreFor(kind));
+			
 			return TaskReturn::finish(1);
 		}
 		case ScriptKernelTask::ChangeRoom:
@@ -963,9 +969,14 @@ private:
 				as3D(pointObject->position()), targetScale,
 				getNumberArg(2), (EasingType)getNumberArg(3), (EasingType)getNumberArg(4)));
 		}
-		case ScriptKernelTask::Disguise:
+		case ScriptKernelTask::Disguise: {
 			// a somewhat bouncy vertical camera movement used in V1
-			return TaskReturn::waitFor(g_engine->camera().disguise(process(), getNumberArg(0)));
+			// or waiting for user to click
+			const auto duration = getNumberArg(0);
+			return TaskReturn::waitFor(duration == 0
+				? g_engine->input().waitForInput(process())
+				: g_engine->camera().disguise(process(), duration));
+		}
 
 		// Fades
 		case ScriptKernelTask::FadeType0:
@@ -1025,7 +1036,7 @@ private:
 		g_engine->scheduler().killAllProcessesFor(kind);
 		g_engine->sounds().fadeOutVoiceAndSFX(200);
 		g_engine->player().stopLastDialogCharacters();
-		_lock.release(); // yes this seems dangerous, but it is original..
+		_characterLock.release(); // yes this seems dangerous, but it is original..
 		auto &character = g_engine->world().getMainCharacterByKind(kind);
 		character.resetUsingObjectAndDialogMenu();
 		assert(character.semaphore().isReleased()); // this process should be the last to hold a lock if at all...
@@ -1038,7 +1049,7 @@ private:
 	int32 _lastKernelTaskI = 0;
 	bool _returnsFromKernelCall = false;
 	bool _isFirstExecution = true;
-	FakeLock _lock;
+	FakeLock _characterLock;
 };
 DECLARE_TASK(ScriptTask)
 
diff --git a/engines/alcachofa/tasks.h b/engines/alcachofa/tasks.h
index 976b2e38cab..b42a680103c 100644
--- a/engines/alcachofa/tasks.h
+++ b/engines/alcachofa/tasks.h
@@ -50,6 +50,7 @@ DEFINE_TASK(FadeTask)
 DEFINE_TASK(DoorTask)
 DEFINE_TASK(ScriptTimerTask)
 DEFINE_TASK(ScriptTask)
+DEFINE_TASK(WaitForInputTask)
 DEFINE_TASK(PlaySoundTask)
 DEFINE_TASK(WaitForMusicTask)
 DEFINE_TASK(DelayTask)


Commit: 1d7468a6cce64b3ce1a6bc00dc1237969ab0e2d7
    https://github.com/scummvm/scummvm/commit/1d7468a6cce64b3ce1a6bc00dc1237969ab0e2d7
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:29+01:00

Commit Message:
ALCACHOFA: Adapt interaction and menu conditions for V1

Changed paths:
    engines/alcachofa/game-movie-adventure-original.cpp
    engines/alcachofa/game-movie-adventure-special.cpp
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/game.h
    engines/alcachofa/player.cpp


diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index e4c86af036b..638eeba49d2 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -216,7 +216,12 @@ public:
 	}
 
 	bool isAllowedToOpenMenu() override {
-		return dynamic_cast<RoomWithFloor *>(g_engine->player().currentRoom()) != nullptr;
+		return g_engine->player().semaphore().isReleased() &&
+			dynamic_cast<RoomWithFloor *>(g_engine->player().currentRoom()) != nullptr;
+	}
+
+	bool isAllowedToInteract() override {
+		return true; // original would be checking an unused script variable "Ocupados"
 	}
 
 	bool shouldScriptLockInteraction() override {
diff --git a/engines/alcachofa/game-movie-adventure-special.cpp b/engines/alcachofa/game-movie-adventure-special.cpp
index fc921390f5b..a5dc92ee83b 100644
--- a/engines/alcachofa/game-movie-adventure-special.cpp
+++ b/engines/alcachofa/game-movie-adventure-special.cpp
@@ -258,7 +258,12 @@ public:
 	}
 
 	bool isAllowedToOpenMenu() override {
-		return !g_engine->script().variable("prohibirESC");
+		return !g_engine->script().variable("prohibirESC") &&
+			g_engine->sounds().musicSemaphore().isReleased();
+	}
+
+	bool isAllowedToInteract() override {
+		return g_engine->player().semaphore().isReleased();
 	}
 
 	bool shouldScriptLockInteraction() override {
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 8cfc74ce801..4ea63b6bb76 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -855,7 +855,7 @@ MainCharacter::~MainCharacter() {
 }
 
 bool MainCharacter::isBusy() const {
-	return !_semaphore.isReleased() || !g_engine->player().semaphore().isReleased();
+	return !_semaphore.isReleased() || !g_engine->game().isAllowedToInteract();
 }
 
 void MainCharacter::update() {
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index 4001629b513..e94d0bcfb50 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -66,6 +66,7 @@ public:
 	virtual Common::String getSoundPath(const char *filename) = 0; ///< Without file-extension
 	virtual Common::String getMusicPath(int32 trackId) = 0; ///< Without file-extension
 	virtual bool shouldClipCamera() = 0;
+	virtual bool isAllowedToInteract() = 0;
 	virtual bool isAllowedToOpenMenu() = 0; ///< only the game-specific condition
 	virtual bool shouldScriptLockInteraction() = 0;
 	virtual bool shouldChangeCharacterUseGameLock() = 0;
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 7d51681b615..e173b9a1a5a 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -350,7 +350,6 @@ void Player::setActiveCharacter(MainCharacterKind kind) {
 bool Player::isAllowedToOpenMenu() {
 	return
 		!g_engine->menu().isOpen() &&
-		g_engine->sounds().musicSemaphore().isReleased() &&
 		g_engine->game().isAllowedToOpenMenu() &&
 		!_isInTemporaryRoom; // we cannot reliably store this state across multiple room changes
 }


Commit: 1cdfcdbfe5e2b49c861a7f8f36cbc1f452943036
    https://github.com/scummvm/scummvm/commit/1cdfcdbfe5e2b49c861a7f8f36cbc1f452943036
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:29+01:00

Commit Message:
ALCACHOFA: Fix room transitions when examining items

Changed paths:
    engines/alcachofa/player.cpp
    engines/alcachofa/rooms.cpp


diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index e173b9a1a5a..183a87ba814 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -348,10 +348,9 @@ void Player::setActiveCharacter(MainCharacterKind kind) {
 }
 
 bool Player::isAllowedToOpenMenu() {
-	return
-		!g_engine->menu().isOpen() &&
-		g_engine->game().isAllowedToOpenMenu() &&
-		!_isInTemporaryRoom; // we cannot reliably store this state across multiple room changes
+	return !g_engine->menu().isOpen() &&
+		   g_engine->game().isAllowedToOpenMenu() &&
+		   !_isInTemporaryRoom; // we cannot reliably store this state across multiple room changes
 }
 
 void Player::syncGame(Serializer &s) {
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index fff502f70c4..efe93fadc35 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -543,7 +543,7 @@ void Inventory::open() {
 	updateItemsByActiveCharacter();
 }
 
-void InventoryV3::close() {
+void Inventory::close() {
 	g_engine->camera().restore(1);
 	g_engine->globalUI().startClosingInventory();
 }


Commit: cb07e9349d011dbce1c2cfcf8ef729292b5299c4
    https://github.com/scummvm/scummvm/commit/cb07e9349d011dbce1c2cfcf8ef729292b5299c4
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:29+01:00

Commit Message:
ALCACHOFA: V1: Fix change character button

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/game-movie-adventure-original.cpp
    engines/alcachofa/game-movie-adventure-special.cpp
    engines/alcachofa/game.h
    engines/alcachofa/global-ui.cpp
    engines/alcachofa/global-ui.h
    engines/alcachofa/rooms.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index dbbbf8da484..7d07e0a11f0 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -84,7 +84,7 @@ Common::Error AlcachofaEngine::run() {
 	_drawQueue.reset(new DrawQueue(_renderer.get()));
 	_script.reset(new Script());
 	_player.reset(new Player());
-	_globalUI.reset(new GlobalUI());
+	_globalUI.reset(isV1() ? static_cast<GlobalUI *>(new GlobalUIV1()) : new GlobalUIV3());
 	_menu.reset(new Menu());
 	setMillis(0);
 	game().onLoadedGameFiles();
diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index 638eeba49d2..fec007b11a9 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -211,6 +211,10 @@ public:
 		return String::format("disk%d/track%02d", diskId, trackId);
 	}
 
+	int32 getCharacterJingle(MainCharacterKind kind) override {
+		return kind == MainCharacterKind::Mortadelo ? 15 : 16;
+	}
+
 	bool shouldClipCamera() override {
 		return true;
 	}
diff --git a/engines/alcachofa/game-movie-adventure-special.cpp b/engines/alcachofa/game-movie-adventure-special.cpp
index a5dc92ee83b..7864c4c652a 100644
--- a/engines/alcachofa/game-movie-adventure-special.cpp
+++ b/engines/alcachofa/game-movie-adventure-special.cpp
@@ -253,6 +253,11 @@ public:
 		return String::format("Sonidos/T%d", trackId);
 	}
 
+	int32 getCharacterJingle(MainCharacterKind kind) override {
+		return g_engine->script().variable(
+			kind == MainCharacterKind::Mortadelo ? "PistaMorta" : "PistaFile");
+	}
+
 	bool shouldClipCamera() override {
 		return g_engine->script().variable("EncuadrarCamara");
 	}
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index e94d0bcfb50..821f539a544 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -36,6 +36,7 @@ class Door;
 class Room;
 class Process;
 struct ScriptInstruction;
+class GlobalUI;
 
 /**
  * @brief Provides functionality specific to a game title / engine version.
@@ -65,6 +66,7 @@ public:
 	virtual Common::Path getVideoPath(int32 videoId) = 0;
 	virtual Common::String getSoundPath(const char *filename) = 0; ///< Without file-extension
 	virtual Common::String getMusicPath(int32 trackId) = 0; ///< Without file-extension
+	virtual int32 getCharacterJingle(MainCharacterKind kind) = 0;
 	virtual bool shouldClipCamera() = 0;
 	virtual bool isAllowedToInteract() = 0;
 	virtual bool isAllowedToOpenMenu() = 0; ///< only the game-specific condition
diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index f58cfc42839..e91ce6b4160 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -113,17 +113,6 @@ Animation *GlobalUI::activeAnimation() const {
 		: _iconMortadelo.get();
 }
 
-bool GlobalUI::isHoveringChangeButton() const {
-	auto mousePos = g_engine->input().mousePos2D();
-	auto anim = activeAnimation();
-	auto offset = anim->totalFrameOffset(0);
-	auto bounds = anim->frameBounds(0);
-
-	const int minX = g_system->getWidth() + offset.x;
-	const int maxY = bounds.height() + offset.y;
-	return mousePos.x >= minX && mousePos.y <= maxY;
-}
-
 bool GlobalUI::updateChangingCharacter() {
 	auto &player = g_engine->player();
 	if (g_engine->menu().isOpen() ||
@@ -147,12 +136,7 @@ bool GlobalUI::updateChangingCharacter() {
 	player.changeRoom(player.activeCharacter()->room()->name(), false);
 	g_engine->game().onUserChangedCharacter();
 
-	int32 characterJingle = g_engine->script().variable(
-		player.activeCharacterKind() == MainCharacterKind::Mortadelo
-		? "PistaMorta"
-		: "PistaFile"
-	);
-	g_engine->sounds().startMusic(characterJingle);
+	g_engine->sounds().startMusic(g_engine->game().getCharacterJingle(player.activeCharacterKind()));
 	g_engine->sounds().queueMusic(player.currentRoom()->musicID());
 
 	_changeButton.setAnimation(activeAnimation());
@@ -160,7 +144,18 @@ bool GlobalUI::updateChangingCharacter() {
 	return true;
 }
 
-void GlobalUI::drawChangingButton() {
+bool GlobalUIV3::isHoveringChangeButton() const {
+	auto mousePos = g_engine->input().mousePos2D();
+	auto anim = activeAnimation();
+	auto offset = anim->totalFrameOffset(0);
+	auto bounds = anim->frameBounds(0);
+
+	const int minX = g_system->getWidth() + offset.x;
+	const int maxY = bounds.height() + offset.y;
+	return mousePos.x >= minX && mousePos.y <= maxY;
+}
+
+void GlobalUIV3::drawChangingButton() {
 	auto &player = g_engine->player();
 	if (g_engine->menu().isOpen() ||
 		!player.semaphore().isReleased() ||
@@ -188,6 +183,26 @@ void GlobalUI::drawChangingButton() {
 	g_engine->drawQueue().add<AnimationDrawRequest>(_changeButton, false, BlendMode::AdditiveAlpha);
 }
 
+bool GlobalUIV1::isHoveringChangeButton() const {
+	auto mousePos = g_engine->input().mousePos2D();
+	auto imageSize = activeAnimation()->imageSize(0);
+
+	return mousePos.x >= g_system->getWidth() - imageSize.x && mousePos.y < imageSize.y;
+}
+
+void GlobalUIV1::drawChangingButton() {
+	if (g_engine->menu().isOpen())
+		return;
+
+	auto anim = activeAnimation();
+	_changeButton.setAnimation(anim);
+	_changeButton.topLeft() = { (int16)(g_system->getWidth() - anim->imageSize(0).x), 0 };
+	_changeButton.reset(); // make sure frameI == 0
+	g_engine->drawQueue().add<AnimationDrawRequest>(_changeButton, false, BlendMode::AdditiveAlpha);
+
+	//g_engine->drawQueue().add<AnimationDrawRequest>(anim, 0, Math::Vector2d(x, 0), -9);
+}
+
 struct CenterBottomTextTask final : public Task {
 	CenterBottomTextTask(Process &process, int32 dialogId, uint32 durationMs)
 		: Task(process)
diff --git a/engines/alcachofa/global-ui.h b/engines/alcachofa/global-ui.h
index dd3722a30cc..8204138d788 100644
--- a/engines/alcachofa/global-ui.h
+++ b/engines/alcachofa/global-ui.h
@@ -32,22 +32,23 @@ Common::Rect closeInventoryTriggerBounds();
 class GlobalUI {
 public:
 	GlobalUI();
+	virtual ~GlobalUI() {}
 
 	inline Font &generalFont() const { assert(_generalFont != nullptr); return *_generalFont; }
 	inline Font &dialogFont() const { assert(_dialogFont != nullptr); return *_dialogFont; }
 	inline bool &isPermanentFaded() { return _isPermanentFaded; }
 
 	bool updateChangingCharacter();
-	void drawChangingButton();
+	virtual void drawChangingButton() = 0;
 	bool updateOpeningInventory();
 	void updateClosingInventory();
 	void startClosingInventory();
 	void drawScreenStates(); // black borders and/or permanent fade
 	void syncGame(Common::Serializer &s);
 
-private:
+protected:
 	Animation *activeAnimation() const;
-	bool isHoveringChangeButton() const;
+	virtual bool isHoveringChangeButton() const = 0;
 
 	Graphic _changeButton;
 	Common::ScopedPtr<Font>
@@ -65,6 +66,22 @@ private:
 	uint32 _timeForInventory = 0;
 };
 
+class GlobalUIV1 final : public GlobalUI {
+public:
+	void drawChangingButton() override;
+
+protected:
+	bool isHoveringChangeButton() const override;
+};
+
+class GlobalUIV3 final : public GlobalUI {
+public:
+	void drawChangingButton() override;
+
+protected:
+	bool isHoveringChangeButton() const override;
+};
+
 Task *showCenterBottomText(Process &process, int32 dialogId, uint32 durationMs);
 
 }
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index efe93fadc35..9a558ed2e80 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -789,8 +789,8 @@ bool World::loadWorldFileV1(const char *path) {
 	};
 	readGlobalAnim(GlobalAnimationKind::GeneralFont, GlobalAnimationKind::DialogFont);
 	readGlobalAnim(GlobalAnimationKind::Cursor);
+	readGlobalAnim(GlobalAnimationKind::FilemonIcon, GlobalAnimationKind::FilemonDisabledIcon); // note this is swapped in V1
 	readGlobalAnim(GlobalAnimationKind::MortadeloIcon, GlobalAnimationKind::MortadeloDisabledIcon);
-	readGlobalAnim(GlobalAnimationKind::FilemonIcon, GlobalAnimationKind::FilemonDisabledIcon);
 	readGlobalAnim(GlobalAnimationKind::InventoryIcon, GlobalAnimationKind::InventoryDisabledIcon);
 
 	readRooms(*file);


Commit: 030df0392a76bf66e9618c0876d196b6f62076bf
    https://github.com/scummvm/scummvm/commit/030df0392a76bf66e9618c0876d196b6f62076bf
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:29+01:00

Commit Message:
ALCACHOFA: V1: Add inventory button

Changed paths:
    engines/alcachofa/global-ui.cpp
    engines/alcachofa/global-ui.h
    engines/alcachofa/rooms.cpp


diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index e91ce6b4160..044ee034d62 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -28,22 +28,6 @@ using namespace Common;
 
 namespace Alcachofa {
 
-// originally the inventory only reacts to exactly top-left/bottom-right which is fine in
-// fullscreen when you just slam the mouse cursor into the corner.
-// In any other scenario this is cumbersome so I expand this area.
-// And it is still pretty bad, especially in windowed mode so there is a key to open/close as well
-static constexpr int16 kInventoryTriggerSize = 10;
-
-Rect openInventoryTriggerBounds() {
-	int16 size = kInventoryTriggerSize * 1024 / g_system->getWidth();
-	return Rect(0, 0, size, size);
-}
-
-Rect closeInventoryTriggerBounds() {
-	int16 size = kInventoryTriggerSize * 1024 / g_system->getWidth();
-	return Rect(g_system->getWidth() - size, g_system->getHeight() - size, g_system->getWidth(), g_system->getHeight());
-}
-
 GlobalUI::GlobalUI() {
 	auto &world = g_engine->world();
 	_generalFont.reset(new Font(world.getGlobalAnimation(GlobalAnimationKind::GeneralFont)));
@@ -59,7 +43,11 @@ GlobalUI::GlobalUI() {
 	_iconInventory->load();
 }
 
-void GlobalUI::startClosingInventory() {
+void GlobalUIV1::startClosingInventory() {
+	// nothing to do here, the inventory closes instantly
+}
+
+void GlobalUIV3::startClosingInventory() {
 	_isOpeningInventory = false;
 	_isClosingInventory = true;
 	_timeForInventory = g_engine->getMillis();
@@ -77,13 +65,20 @@ void GlobalUI::updateClosingInventory() {
 		g_engine->world().inventory().drawAsOverlay((int32)(g_system->getHeight() * (deltaTime * kSpeed)));
 }
 
-bool GlobalUI::updateOpeningInventory() {
+// originally the inventory only reacts to exactly top-left/bottom-right which is fine in
+// fullscreen when you just slam the mouse cursor into the corner.
+// In any other scenario this is cumbersome so I expand this area.
+// And it is still pretty bad, especially in windowed mode so there is a key to open/close as well
+static constexpr int16 kInventoryTriggerSizeV3 = 10;
+
+bool GlobalUIV3::updateOpeningInventory() {
 	static constexpr float kSpeed = 10 / 3.0f / 1000.0f;
 	if (g_engine->menu().isOpen())
 		return false;
 
+	int16 size = kInventoryTriggerSizeV3 * 1024 / g_system->getWidth();
 	const bool userWantsToOpenInventory =
-		openInventoryTriggerBounds().contains(g_engine->input().mousePos2D()) ||
+		Rect(0, 0, size, size).contains(g_engine->input().mousePos2D()) ||
 		g_engine->input().wasInventoryKeyPressed();
 
 	if (_isOpeningInventory) {
@@ -107,6 +102,23 @@ bool GlobalUI::updateOpeningInventory() {
 	return false;
 }
 
+bool GlobalUIV1::updateOpeningInventory() {
+	auto mousePos = g_engine->input().mousePos2D();
+	auto imageSize = _iconInventory->imageSize(0);
+	const bool isHovering = mousePos.x < imageSize.x && mousePos.y < imageSize.y;
+	const bool userClickedOnButton = isHovering && g_engine->input().wasMouseLeftReleased();
+
+	if (g_engine->menu().isOpen())
+		return false;
+	if (userClickedOnButton || g_engine->input().wasInventoryKeyPressed()) {
+		g_engine->player().activeCharacter()->stopWalking();
+		g_engine->world().inventory().updateItemsByActiveCharacter();
+		g_engine->world().inventory().open();
+		return true;
+	}
+	return isHovering;
+}
+
 Animation *GlobalUI::activeAnimation() const {
 	return g_engine->player().activeCharacterKind() == MainCharacterKind::Mortadelo
 		? _iconFilemon.get()
@@ -183,6 +195,16 @@ void GlobalUIV3::drawChangingButton() {
 	g_engine->drawQueue().add<AnimationDrawRequest>(_changeButton, false, BlendMode::AdditiveAlpha);
 }
 
+bool GlobalUIV3::isHoveringInventoryExit() const {
+	int16 size = kInventoryTriggerSizeV3 * 1024 / g_system->getWidth();
+	auto bounds = Rect(g_system->getWidth() - size, g_system->getHeight() - size, g_system->getWidth(), g_system->getHeight());;
+	return bounds.contains(g_engine->input().mousePos2D());
+}
+
+void GlobalUIV3::drawInventoryButton() {
+	// while there are animations for the inventory button, they are unused in V3
+}
+
 bool GlobalUIV1::isHoveringChangeButton() const {
 	auto mousePos = g_engine->input().mousePos2D();
 	auto imageSize = activeAnimation()->imageSize(0);
@@ -191,7 +213,7 @@ bool GlobalUIV1::isHoveringChangeButton() const {
 }
 
 void GlobalUIV1::drawChangingButton() {
-	if (g_engine->menu().isOpen())
+	if (g_engine->menu().isOpen() || !g_engine->player().semaphore().isReleased())
 		return;
 
 	auto anim = activeAnimation();
@@ -203,6 +225,25 @@ void GlobalUIV1::drawChangingButton() {
 	//g_engine->drawQueue().add<AnimationDrawRequest>(anim, 0, Math::Vector2d(x, 0), -9);
 }
 
+static constexpr int16 kInventoryTriggerSizeV1 = 70;
+
+bool GlobalUIV1::isHoveringInventoryExit() const {
+	auto mousePos = g_engine->input().mousePos2D();
+
+	return mousePos.x >= g_system->getWidth() - kInventoryTriggerSizeV1 &&
+		mousePos.y >= g_system->getHeight() - kInventoryTriggerSizeV1;
+}
+
+void GlobalUIV1::drawInventoryButton() {
+	if (g_engine->menu().isOpen())
+		return;
+
+	_inventoryButton.setAnimation(_iconInventory.get());
+	_inventoryButton.reset();
+	_inventoryButton.order() = -9;
+	g_engine->drawQueue().add<AnimationDrawRequest>(_inventoryButton, false, BlendMode::AdditiveAlpha);
+}
+
 struct CenterBottomTextTask final : public Task {
 	CenterBottomTextTask(Process &process, int32 dialogId, uint32 durationMs)
 		: Task(process)
diff --git a/engines/alcachofa/global-ui.h b/engines/alcachofa/global-ui.h
index 8204138d788..d45257bb02a 100644
--- a/engines/alcachofa/global-ui.h
+++ b/engines/alcachofa/global-ui.h
@@ -26,9 +26,6 @@
 
 namespace Alcachofa {
 
-Common::Rect openInventoryTriggerBounds();
-Common::Rect closeInventoryTriggerBounds();
-
 class GlobalUI {
 public:
 	GlobalUI();
@@ -40,9 +37,11 @@ public:
 
 	bool updateChangingCharacter();
 	virtual void drawChangingButton() = 0;
-	bool updateOpeningInventory();
+	virtual void drawInventoryButton() = 0;
+	virtual bool updateOpeningInventory() = 0; ///< returns true iff interaction is handled
+	virtual void startClosingInventory() = 0;
+	virtual bool isHoveringInventoryExit() const = 0;
 	void updateClosingInventory();
-	void startClosingInventory();
 	void drawScreenStates(); // black borders and/or permanent fade
 	void syncGame(Common::Serializer &s);
 
@@ -50,7 +49,7 @@ protected:
 	Animation *activeAnimation() const;
 	virtual bool isHoveringChangeButton() const = 0;
 
-	Graphic _changeButton;
+	Graphic _changeButton, _inventoryButton;
 	Common::ScopedPtr<Font>
 		_generalFont,
 		_dialogFont;
@@ -69,6 +68,10 @@ protected:
 class GlobalUIV1 final : public GlobalUI {
 public:
 	void drawChangingButton() override;
+	void drawInventoryButton() override;
+	bool updateOpeningInventory() override;
+	void startClosingInventory() override;
+	bool isHoveringInventoryExit() const override;
 
 protected:
 	bool isHoveringChangeButton() const override;
@@ -77,6 +80,10 @@ protected:
 class GlobalUIV3 final : public GlobalUI {
 public:
 	void drawChangingButton() override;
+	void drawInventoryButton() override;
+	bool updateOpeningInventory() override;
+	void startClosingInventory() override;
+	bool isHoveringInventoryExit() const override;
 
 protected:
 	bool isHoveringChangeButton() const override;
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 9a558ed2e80..ac5a569621b 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -240,6 +240,7 @@ bool Room::updateInput() {
 			player.updateCursor();
 		}
 		player.drawCursor();
+		g_engine->globalUI().drawInventoryButton();
 	}
 
 	if (player.currentRoom() == this) {
@@ -440,11 +441,11 @@ bool Inventory::updateInput() {
 	auto &player = g_engine->player();
 	auto &input = g_engine->input();
 	auto *hoveredItem = getHoveredItem();
+	if (player.activeCharacter()->isBusy())
+		return true;
+	player.drawCursor(0);
 
-	if (!player.activeCharacter()->isBusy())
-		player.drawCursor(0);
-
-	if (hoveredItem != nullptr && !player.activeCharacter()->isBusy()) {
+	if (hoveredItem != nullptr) {
 		if ((input.wasMouseLeftPressed() && player.heldItem() == nullptr) ||
 			(input.wasMouseLeftReleased() && player.heldItem() != nullptr) ||
 			input.wasMouseRightReleased()) {
@@ -460,18 +461,15 @@ bool Inventory::updateInput() {
 	}
 
 	const bool userWantsToCloseInventory =
-		closeInventoryTriggerBounds().contains(input.mousePos2D()) ||
+		g_engine->globalUI().isHoveringInventoryExit() ||
 		input.wasMenuKeyPressed() ||
 		input.wasInventoryKeyPressed();
-	if (!player.activeCharacter()->isBusy() &&
-		userWantsToCloseInventory) {
+	if (userWantsToCloseInventory) {
 		player.changeRoomToBeforeInventory();
 		close();
 	}
 
-	if (!player.activeCharacter()->isBusy() &&
-		hoveredItem == nullptr &&
-		input.wasMouseRightReleased()) {
+	if (hoveredItem == nullptr && input.wasMouseRightReleased()) {
 		player.heldItem() = nullptr;
 		return false;
 	}


Commit: 6cb669d0e9a59cc595f47db0fd806efec6f89496
    https://github.com/scummvm/scummvm/commit/6cb669d0e9a59cc595f47db0fd806efec6f89496
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:29+01:00

Commit Message:
ALCACHOFA: Add script debug command

Changed paths:
    engines/alcachofa/console.cpp
    engines/alcachofa/console.h


diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
index a6228a67a6a..f3e702f449f 100644
--- a/engines/alcachofa/console.cpp
+++ b/engines/alcachofa/console.cpp
@@ -51,6 +51,7 @@ Console::Console() : GUI::Debugger() {
 	registerCmd("tp", WRAP_METHOD(Console, cmdTeleport));
 	registerCmd("toggleRoomFloor", WRAP_METHOD(Console, cmdToggleRoomFloor));
 	registerCmd("playVideo", WRAP_METHOD(Console, cmdPlayVideo));
+	registerCmd("script", WRAP_METHOD(Console, cmdScript));
 }
 
 Console::~Console() {}
@@ -294,4 +295,23 @@ bool Console::cmdPlayVideo(int argc, const char **args) {
 	return true;
 }
 
+bool Console::cmdScript(int argc, const char **args) {
+	if (argc != 2) {
+		debugPrintf("usage: %s <procedure name>\n", args[0]);
+		return true;
+	}
+
+	auto process = g_engine->script().createProcess(
+		g_engine->player().activeCharacterKind(),
+		args[1],
+		ScriptFlags::AllowMissing);
+	if (process == nullptr) {
+		debugPrintf("No such procedure exists");
+		return true;
+	} else {
+		process->name() += " (Console)";
+		return false;
+	}
+}
+
 } // End of namespace Alcachofa
diff --git a/engines/alcachofa/console.h b/engines/alcachofa/console.h
index acfbb4fea6f..f7c843887f9 100644
--- a/engines/alcachofa/console.h
+++ b/engines/alcachofa/console.h
@@ -61,6 +61,7 @@ private:
 	bool cmdTeleport(int argc, const char **args);
 	bool cmdToggleRoomFloor(int argc, const char **args);
 	bool cmdPlayVideo(int argc, const char **args);
+	bool cmdScript(int argc, const char **args);
 
 	bool _showGraphics = false;
 	bool _showInteractables = false;


Commit: ce073b3bece7dc2d99dc79a38a8a68dfb3eecda9
    https://github.com/scummvm/scummvm/commit/ce073b3bece7dc2d99dc79a38a8a68dfb3eecda9
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:29+01:00

Commit Message:
ALCACHOFA: V1: Fix stack handling of ReturnVoid

Changed paths:
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index b70a21d8162..2b9d21114f4 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -390,6 +390,7 @@ struct ScriptTask final : public Task {
 				break;
 			case ScriptOp::ReturnVoid: {
 				_pc = popInstruction();
+				pushNumber(instruction._arg); // this does not seem to be original, but it works..
 				if (_pc == UINT_MAX)
 					return TaskReturn::finish(0);
 			}break;


Commit: e8c9400dca805b94d867aea21b89d057a921ebd5
    https://github.com/scummvm/scummvm/commit/e8c9400dca805b94d867aea21b89d057a921ebd5
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:29+01:00

Commit Message:
ALCACHOFA: V1: Fix animation sprite order

Changed paths:
    engines/alcachofa/graphics.cpp


diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 076ac56be8b..e9ee1856c5a 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -230,7 +230,7 @@ void AnimationBase::readV1(SeekableReadStream &stream) {
 	skipVarString(stream); // another internal, unused name
 	uint spriteCount = stream.readUint32LE();
 	uint frameCount = stream.readUint32LE();
-	scumm_assert(spriteCount < kMaxSpriteIDsV1);
+	scumm_assert(spriteCount <= kMaxSpriteIDsV1);
 	_spriteBases.reserve(spriteCount);
 	_spriteEnabled.reserve(spriteCount);
 	_spriteOffsets.reserve(spriteCount * frameCount);
@@ -258,15 +258,17 @@ void AnimationBase::readV1(SeekableReadStream &stream) {
 		}
 	}
 
-	// Sprite order is setup by setting up reverse index sequence and
+	// Sprite order is setup by setting up index sequence and
 	// then stable sort descending by order (here: Bubblesort)
 	for (uint i = 0; i < spriteCount; i++)
-		_spriteIndexMapping[i] = (spriteCount - 1 - i);
+		_spriteIndexMapping[i] = i;
 	for (uint i = 0; i < spriteCount; i++) {
 		bool hadChange = false;
 		for (uint j = 0; j < spriteCount - i - 1; j++) {
-			if (spriteOrder[_spriteIndexMapping[j]] < spriteOrder[_spriteIndexMapping[j + 1]])
+			if (spriteOrder[_spriteIndexMapping[j]] < spriteOrder[_spriteIndexMapping[j + 1]]) {
 				SWAP(_spriteIndexMapping[j], _spriteIndexMapping[j + 1]);
+				hadChange = true;
+			}
 		}
 		if (!hadChange)
 			break;


Commit: 04fc32f14c51d6187fe1772aad3e37e575b47c3c
    https://github.com/scummvm/scummvm/commit/04fc32f14c51d6187fe1772aad3e37e575b47c3c
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:30+01:00

Commit Message:
ALCACHOFA: V1: Fix room bounds and 3D draw positions

Changed paths:
    engines/alcachofa/camera.cpp
    engines/alcachofa/camera.h
    engines/alcachofa/console.cpp
    engines/alcachofa/console.h
    engines/alcachofa/graphics.cpp
    engines/alcachofa/rooms.cpp


diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 6e72525ed29..9037fe15f36 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -37,11 +37,11 @@ void Camera::resetRotationAndScale() {
 	_cur._usedCenter.z() = 0;
 }
 
-void Camera::setRoomBounds(Point min, Point size, int16 bgScale) {
-	float scaleFactor = bgScale * kInvBaseScale;
-	_roomMin = as2D(min) * scaleFactor;
-	_roomMax = _roomMin + as2D(size) * scaleFactor;
-	_roomScale = 0.0f;
+void Camera::setRoomBounds(Point min, Point size) {
+	Point screenSize(g_system->getWidth(), g_system->getHeight());
+	_roomMin = as2D(min + screenSize / 2);
+	_roomMax = _roomMin + as2D(size - screenSize);
+	_roomScale = 0;
 }
 
 void Camera::setRoomBounds(Point bgSize, int16 bgScale) {
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index a5a1573e5a3..c6774da47bf 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -46,7 +46,7 @@ public:
 	Math::Vector3d transform3Dto2D(Math::Vector3d v) const;
 	Common::Point transform3Dto2D(Common::Point p) const;
 	void resetRotationAndScale();
-	void setRoomBounds(Common::Point min, Common::Point size, int16 bgScale); ///< Used in V1
+	void setRoomBounds(Common::Point min, Common::Point size); ///< Used in V1
 	void setRoomBounds(Common::Point bgSize, int16 bgScale); ///< Used in V3
 	void setFollow(WalkingCharacter *target, bool catchUp = false);
 	void setPosition(Math::Vector2d v);
diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
index f3e702f449f..9206f1c19d4 100644
--- a/engines/alcachofa/console.cpp
+++ b/engines/alcachofa/console.cpp
@@ -52,6 +52,7 @@ Console::Console() : GUI::Debugger() {
 	registerCmd("toggleRoomFloor", WRAP_METHOD(Console, cmdToggleRoomFloor));
 	registerCmd("playVideo", WRAP_METHOD(Console, cmdPlayVideo));
 	registerCmd("script", WRAP_METHOD(Console, cmdScript));
+	registerCmd("toggleObject", WRAP_METHOD(Console, cmdToggleObject));
 }
 
 Console::~Console() {}
@@ -306,7 +307,7 @@ bool Console::cmdScript(int argc, const char **args) {
 		args[1],
 		ScriptFlags::AllowMissing);
 	if (process == nullptr) {
-		debugPrintf("No such procedure exists");
+		debugPrintf("No such procedure exists\n");
 		return true;
 	} else {
 		process->name() += " (Console)";
@@ -314,4 +315,22 @@ bool Console::cmdScript(int argc, const char **args) {
 	}
 }
 
+bool Console::cmdToggleObject(int argc, const char **args) {
+	if (argc < 2) {
+		debugPrintf("usage: %s <object> [<object> ...]\n", args[0]);
+		return true;
+	}
+
+	for (int i = 1; i < argc; i++) {
+		auto object = g_engine->world().getObjectByName(args[i]);
+		if (object == nullptr)
+			object = g_engine->world().getObjectByNameFromAnyRoom(args[i]);
+		if (object == nullptr)
+			debugPrintf("No such object: %s\n", args[i]);
+		else
+			object->toggle(!object->isEnabled());
+	}
+	return true;
+}
+
 } // End of namespace Alcachofa
diff --git a/engines/alcachofa/console.h b/engines/alcachofa/console.h
index f7c843887f9..b288a8bfb68 100644
--- a/engines/alcachofa/console.h
+++ b/engines/alcachofa/console.h
@@ -62,6 +62,7 @@ private:
 	bool cmdToggleRoomFloor(int argc, const char **args);
 	bool cmdPlayVideo(int argc, const char **args);
 	bool cmdScript(int argc, const char **args);
+	bool cmdToggleObject(int argc, const char **args);
 
 	bool _showGraphics = false;
 	bool _showInteractables = false;
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index e9ee1856c5a..d47b1d07cdd 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -601,7 +601,10 @@ void Animation::draw2D(int32 frameI, Vector2d topLeft, float scale, BlendMode bl
 
 void Animation::outputRect3D(int32 frameI, float scale, Vector3d &topLeft, Vector2d &size) const {
 	auto bounds = frameBounds(frameI);
-	topLeft += as3D(totalFrameOffset(frameI)) * scale;
+	if (g_engine->isV3())
+		topLeft += as3D(totalFrameOffset(frameI)) * scale;
+	else // scale by depth position is not used in V1
+		topLeft += as3D(totalFrameOffset(frameI)) * scale / (topLeft.z() * kInvBaseScale);
 	topLeft = g_engine->camera().transform3Dto2D(topLeft);
 	size = Vector2d(bounds.width(), bounds.height()) * scale * topLeft.z();
 }
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index ac5a569621b..470264603b5 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -275,10 +275,7 @@ void Room::updateRoomBounds() {
 	if (graphic != nullptr) {
 		auto bgSize = graphic->animation().imageSize(0);
 		if (g_engine->isV1()) {
-			g_engine->camera().setRoomBounds(
-				graphic->topLeft() + Point(400, 300),
-				bgSize - Point(800, 600),
-				graphic->scale());
+			g_engine->camera().setRoomBounds(graphic->topLeft(), bgSize);
 		} else {
 			/* The fallback fixes a bug where if the background image is invalid the original engine
 			 * would not update the background size. This would be around 1024,768 due to


Commit: c5d17a19f4092ba985e8c339117d008b0ab0af33
    https://github.com/scummvm/scummvm/commit/c5d17a19f4092ba985e8c339117d008b0ab0af33
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:30+01:00

Commit Message:
ALCACHOFA: V1: Implement V1-style menu

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.h
    engines/alcachofa/game-movie-adventure-original.cpp
    engines/alcachofa/game-movie-adventure-special.cpp
    engines/alcachofa/game.cpp
    engines/alcachofa/game.h
    engines/alcachofa/menu.cpp
    engines/alcachofa/menu.h
    engines/alcachofa/objects.h
    engines/alcachofa/ui-objects.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 7d07e0a11f0..cbc232ecd90 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -85,7 +85,7 @@ Common::Error AlcachofaEngine::run() {
 	_script.reset(new Script());
 	_player.reset(new Player());
 	_globalUI.reset(isV1() ? static_cast<GlobalUI *>(new GlobalUIV1()) : new GlobalUIV3());
-	_menu.reset(new Menu());
+	_menu.reset(isV1() ? static_cast<Menu *>(new MenuV1()) : new MenuV3());
 	setMillis(0);
 	game().onLoadedGameFiles();
 
@@ -415,7 +415,8 @@ void AlcachofaEngine::getSavegameThumbnail(Graphics::Surface &thumbnail) {
 	}
 
 	// otherwise we have to rerender
-	thumbnail.create(kBigThumbnailWidth, kBigThumbnailHeight, g_engine->renderer().getPixelFormat());
+	auto size = g_engine->game().getThumbnailResolution();
+	thumbnail.create(size.x, size.y, g_engine->renderer().getPixelFormat());
 	if (g_engine->player().currentRoom() == nullptr)
 		return; // but without a room we would render only black anyway
 
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index cd9fa49ad57..406cd757732 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -56,9 +56,7 @@ struct AlcachofaGameDescription;
 
 constexpr int16 kSmallThumbnailWidth = 160; // for ScummVM
 constexpr int16 kSmallThumbnailHeight = 120;
-static constexpr int16 kBigThumbnailWidth = 341; // for in-game
-static constexpr int16 kBigThumbnailHeight = 256;
-
+// the in-game save thumbnail size is determined by engine verison
 
 namespace SaveVersion {
 	static constexpr const Common::Serializer::Version kInitial = 0;
diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index fec007b11a9..453db2c7c9a 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -148,6 +148,10 @@ public:
 		return Point(800, 600);
 	}
 
+	Point getThumbnailResolution() override {
+		return Point(266, 200);
+	}
+
 	static constexpr const char *kMapFiles[] = {
 		"oeste.emc",
 		"terror.emc",
@@ -175,6 +179,10 @@ public:
 		return kNoXORKey;
 	}
 
+	const char *getMenuRoom() override {
+		return "MENU";
+	}
+
 	const char *getInitScriptName() override {
 		return "INICIALIZAR_MUNDO";
 	}
diff --git a/engines/alcachofa/game-movie-adventure-special.cpp b/engines/alcachofa/game-movie-adventure-special.cpp
index 7864c4c652a..da35b7a0ff8 100644
--- a/engines/alcachofa/game-movie-adventure-special.cpp
+++ b/engines/alcachofa/game-movie-adventure-special.cpp
@@ -204,6 +204,10 @@ public:
 		return Point(1024, 768);
 	}
 
+	Point getThumbnailResolution() override {
+		return Point(341, 256);
+	}
+
 	const char *const *getMapFiles() override {
 		return kMapFiles;
 	}
@@ -228,6 +232,10 @@ public:
 		return kEmbeddedXORKey;
 	}
 
+	const char *getMenuRoom() override {
+		return "MENUPRINCIPAL";
+	}
+
 	const char *getInitScriptName() override {
 		return "CREDITOS_INICIALES";
 	}
diff --git a/engines/alcachofa/game.cpp b/engines/alcachofa/game.cpp
index 46767b7b393..9ed6a7dfefc 100644
--- a/engines/alcachofa/game.cpp
+++ b/engines/alcachofa/game.cpp
@@ -180,6 +180,10 @@ void Game::unknownScriptProcedure(const String &procedure) {
 	_message("Unknown required procedure: %s", procedure.c_str());
 }
 
+void Game::unknownMenuAction(int32 actionId) {
+	_message("Unknown button action: %d", actionId);
+}
+
 void Game::missingSound(const String &fileName) {
 	_message("Missing sound file: %s", fileName.c_str());
 }
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index 821f539a544..a626d0f8ec5 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -52,6 +52,7 @@ public:
 
 	virtual void onLoadedGameFiles();
 	virtual Common::Point getResolution() = 0;
+	virtual Common::Point getThumbnailResolution() = 0;
 	virtual const char *const *getMapFiles() = 0; ///< Returns a nullptr-terminated list
 	virtual GameFileReference getScriptFileRef() = 0;
 	virtual Common::Span<const ScriptOp> getScriptOpMap() = 0;
@@ -61,6 +62,7 @@ public:
 	virtual const char *getDialogFileName() = 0;
 	virtual const char *getObjectFileName() = 0;
 	virtual char getTextFileKey() = 0;
+	virtual const char *getMenuRoom() = 0;
 	virtual const char *getInitScriptName() = 0;
 	virtual int32 getKernelTaskArgCount(int32 kernelTaskI); // only necessary for V1
 	virtual Common::Path getVideoPath(int32 videoId) = 0;
@@ -106,6 +108,7 @@ public:
 	virtual void unknownCamLerpTarget(const char *action, const char *name);
 	virtual void unknownKernelTask(int task);
 	virtual void unknownScriptProcedure(const Common::String &procedure);
+	virtual void unknownMenuAction(int32 actionId);
 
 	virtual void missingAnimation(const Common::String &fileName);
 	virtual void missingSound(const Common::String &fileName);
diff --git a/engines/alcachofa/menu.cpp b/engines/alcachofa/menu.cpp
index 75ff0c669d7..5576f6fac1d 100644
--- a/engines/alcachofa/menu.cpp
+++ b/engines/alcachofa/menu.cpp
@@ -19,6 +19,7 @@
  *
  */
 
+#include "common/config-manager.h"
 #include "gui/message.h"
 #include "graphics/thumbnail.h"
 
@@ -34,7 +35,8 @@ using namespace Graphics;
 namespace Alcachofa {
 
 static void createThumbnail(ManagedSurface &surface) {
-	surface.create(kBigThumbnailWidth, kBigThumbnailHeight, g_engine->renderer().getPixelFormat());
+	const auto size = g_engine->game().getThumbnailResolution();
+	surface.create(size.x, size.y, g_engine->renderer().getPixelFormat());
 }
 
 static void convertToGrayscale(ManagedSurface &surface) {
@@ -65,6 +67,8 @@ Menu::Menu()
 	: _interactionSemaphore("menu")
 	, _saveFileMgr(g_system->getSavefileManager()) {}
 
+Menu::~Menu() {}
+
 void Menu::resetAfterLoad() {
 	_isOpen = false;
 	_openAtNextFrame = false;
@@ -88,7 +92,7 @@ void Menu::updateOpeningMenu() {
 	_millisBeforeMenu = g_engine->getMillis();
 	_previousRoom = g_engine->player().currentRoom();
 	_isOpen = true;
-	g_engine->player().changeRoom("MENUPRINCIPAL", true);
+	g_engine->player().changeRoom(g_engine->game().getMenuRoom(), true);
 	_savefiles = _saveFileMgr->listSavefiles(g_engine->getSaveStatePattern());
 	sort(_savefiles.begin(), _savefiles.end()); // the pattern ensures that the last file has the greatest slot
 	_selectedSavefileI = _savefiles.size();
@@ -101,6 +105,13 @@ void Menu::updateOpeningMenu() {
 		g_system->getWidth() / 2.0f, g_system->getHeight() / 2.0f, 0.0f));
 }
 
+void MenuV1::updateOpeningMenu() {
+	bool willOpen = _openAtNextFrame;
+	Menu::updateOpeningMenu();
+	if (willOpen)
+		switchToState(MainMenuAction::ConfirmSavestate);
+}
+
 static int parseSavestateSlot(const String &filename) {
 	if (filename.size() < 5) // minimal name would be "t.###"
 		return 1;
@@ -108,22 +119,12 @@ static int parseSavestateSlot(const String &filename) {
 }
 
 void Menu::updateSelectedSavefile(bool hasJustSaved) {
-	auto getButton = [] (const char *name) {
-		MenuButton *button = dynamic_cast<MenuButton *>(g_engine->player().currentRoom()->getObjectByName(name));
-		scumm_assert(button != nullptr);
-		return button;
-	};
-
-	bool isOldSavefile = _selectedSavefileI < _savefiles.size();
-	getButton("CARGAR")->isInteractable() = isOldSavefile;
-	getButton("ANTERIOR")->toggle(_selectedSavefileI > 0);
-	getButton("SIGUIENTE")->toggle(isOldSavefile);
-
 	if (hasJustSaved) {
 		// we just saved in-game so we also still have the correct thumbnail in memory
 		_selectedThumbnail.copyFrom(_bigThumbnail);
-	} else if (isOldSavefile) {
+	} else if (!isOnNewSlot()) {
 		if (!tryReadOldSavefile()) {
+			// an old savefile we could not read, use blank thumbnail and default description
 			_selectedSavefileDescription = String::format("Savestate %d",
 				parseSavestateSlot(_savefiles[_selectedSavefileI]));
 			createThumbnail(_selectedThumbnail);
@@ -131,7 +132,8 @@ void Menu::updateSelectedSavefile(bool hasJustSaved) {
 	} else {
 		// the unsaved gamestate is shown as grayscale
 		_selectedThumbnail.copyFrom(_bigThumbnail);
-		convertToGrayscale(_selectedThumbnail);
+		if (g_engine->isV3())
+			convertToGrayscale(_selectedThumbnail);
 	}
 
 	ObjectBase *captureObject = g_engine->player().currentRoom()->getObjectByName("Capture");
@@ -141,6 +143,29 @@ void Menu::updateSelectedSavefile(bool hasJustSaved) {
 	captureGraphic->animation().overrideTexture(_selectedThumbnail);
 }
 
+void MenuV1::updateSelectedSavefile(bool hasJustSaved) {
+	Menu::updateSelectedSavefile(hasJustSaved);
+
+	ObjectBase *captureObject = g_engine->player().currentRoom()->getObjectByName("Capture");
+	scumm_assert(captureObject);
+	bool isInCorrectState = _currentState == MainMenuAction::Load || _currentState == MainMenuAction::Save;
+	captureObject->toggle(isInCorrectState && !isOnNewSlot());
+}
+
+void MenuV3::updateSelectedSavefile(bool hasJustSaved) {
+	Menu::updateSelectedSavefile(hasJustSaved);
+
+	auto getButton = [ ] (const char *name) {
+		MenuButton *button = dynamic_cast<MenuButton *>(g_engine->player().currentRoom()->getObjectByName(name));
+		scumm_assert(button != nullptr);
+		return button;
+	};
+
+	getButton("CARGAR")->isInteractable() = !isOnNewSlot();
+	getButton("ANTERIOR")->toggle(_selectedSavefileI > 0);
+	getButton("SIGUIENTE")->toggle(!isOnNewSlot());
+}
+
 bool Menu::tryReadOldSavefile() {
 	auto savefile = ScopedPtr<InSaveFile>(
 		_saveFileMgr->openForLoading(_savefiles[_selectedSavefileI]));
@@ -176,7 +201,7 @@ void Menu::continueGame() {
 void Menu::triggerMainMenuAction(MainMenuAction action) {
 	switch (action) {
 	case MainMenuAction::ContinueGame:
-		g_engine->menu().continueGame();
+		continueGame();
 		break;
 	case MainMenuAction::Save:
 		triggerSave();
@@ -194,13 +219,67 @@ void Menu::triggerMainMenuAction(MainMenuAction action) {
 		dialog.runModal();
 	}break;
 	case MainMenuAction::OptionsMenu:
-		g_engine->menu().openOptionsMenu();
+		openOptionsMenu();
 		break;
 	case MainMenuAction::Exit:
 	case MainMenuAction::AlsoExit:
 		// implemented in AlcachofaEngine as it has its own event loop
 		g_engine->fadeExit();
 		break;
+	case MainMenuAction::NewGame:
+		// this action might be unused just like the only room it would appear: MENUPRINCIPALINICIO
+		g_engine->script().createProcess(MainCharacterKind::None, g_engine->world().initScriptName());
+		break;
+	default:
+		g_engine->game().unknownMenuAction((int32)action);
+		break;
+	}
+}
+
+void MenuV1::triggerMainMenuAction(MainMenuAction action) {
+	auto updateMusicVolume = [&] (int delta) {
+		auto &config = g_engine->config();
+		int volume = config.musicVolume();
+		volume = volume + (delta * Audio::Mixer::kMaxChannelVolume / 100);
+		volume = CLIP(volume, 0, (int)Audio::Mixer::kMaxChannelVolume);
+		config.musicVolume() = volume;
+		ConfMan.setInt("music_volume", config.musicVolume());
+		g_engine->syncSoundSettings();
+		// we do not use saveToScummVM here to avoid frequent disk flushes
+	};
+
+	const uint maxSavegameI = _currentState == MainMenuAction::Load
+		? _savefiles.size()
+		: _savefiles.size() + 1; // the "new savegame" slot
+	switch (action) {
+	case MainMenuAction::Load:
+		if (!isOnNewSlot())
+			Menu::triggerMainMenuAction(action);
+		break;
+	case MainMenuAction::PrevSave:
+		if (_currentState == MainMenuAction::OptionsMenu)
+			updateMusicVolume(-10);
+		else {
+			_selectedSavefileI = _selectedSavefileI == 0 ? maxSavegameI - 1 : _selectedSavefileI - 1;
+			updateSelectedSavefile(false);
+		}
+		break;
+	case MainMenuAction::NextSave:
+		if (_currentState == MainMenuAction::OptionsMenu)
+			updateMusicVolume(+10);
+		else {
+			_selectedSavefileI = maxSavegameI == 0 ? 0 : (_selectedSavefileI + 1) % maxSavegameI;
+			updateSelectedSavefile(false);
+		}
+		break;
+	default:
+		Menu::triggerMainMenuAction(action);
+		break;
+	}
+}
+
+void MenuV3::triggerMainMenuAction(MainMenuAction action) {
+	switch (action) {
 	case MainMenuAction::NextSave:
 		if (_selectedSavefileI < _savefiles.size()) {
 			_selectedSavefileI++;
@@ -213,12 +292,8 @@ void Menu::triggerMainMenuAction(MainMenuAction action) {
 			updateSelectedSavefile(false);
 		}
 		break;
-	case MainMenuAction::NewGame:
-		// this action might be unused just like the only room it would appear: MENUPRINCIPALINICIO
-		g_engine->script().createProcess(MainCharacterKind::None, g_engine->world().initScriptName());
-		break;
 	default:
-		warning("Unknown main menu action: %d", (int32)action);
+		Menu::triggerMainMenuAction(action);
 		break;
 	}
 }
@@ -236,16 +311,15 @@ void Menu::triggerLoad() {
 
 void Menu::triggerSave() {
 	String fileName;
-	if (_selectedSavefileI < _savefiles.size()) {
-		fileName = _savefiles[_selectedSavefileI]; // overwrite a previous save
-	} else {
+	if (isOnNewSlot()) {
 		// for a new savefile we figure out the next slot index
 		int nextSlot = _savefiles.empty()
 			? 1 // start at one to keep autosave alone
 			: parseSavestateSlot(_savefiles.back()) + 1;
 		fileName = g_engine->getSaveStateName(nextSlot);
 		_selectedSavefileDescription = String::format("Savestate %d", nextSlot);
-	}
+	} else
+		fileName = _savefiles[_selectedSavefileI]; // overwrite a previous save
 
 	Error error(kNoError);
 	auto savefile = ScopedPtr<OutSaveFile>(_saveFileMgr->openForSaving(fileName));
@@ -255,7 +329,7 @@ void Menu::triggerSave() {
 		error = g_engine->saveGameStream(savefile.get());
 	if (error.getCode() == kNoError) {
 		g_engine->getMetaEngine()->appendExtendedSave(savefile.get(), g_engine->getTotalPlayTime(), _selectedSavefileDescription, false);
-		if (_selectedSavefileI >= _savefiles.size())
+		if (isOnNewSlot())
 			_savefiles.push_back(fileName);
 		updateSelectedSavefile(true);
 	} else {
@@ -269,7 +343,9 @@ void Menu::openOptionsMenu() {
 	g_engine->player().changeRoom("MENUOPCIONES", true);
 }
 
-void Menu::setOptionsState() {
+void MenuV1::setOptionsState() {}
+
+void MenuV3::setOptionsState() {
 	Config &config = g_engine->config();
 	Room *optionsMenu = g_engine->world().getRoomByName("MENUOPCIONES");
 	scumm_assert(optionsMenu != nullptr);
@@ -359,7 +435,7 @@ void Menu::triggerOptionsValue(OptionsMenuValue valueId, float value) {
 void Menu::continueMainMenu() {
 	g_engine->config().saveToScummVM();
 	g_engine->syncSoundSettings();
-	g_engine->player().changeRoom("MENUPRINCIPAL", true);
+	g_engine->player().changeRoom(g_engine->game().getMenuRoom(), true);
 
 	updateSelectedSavefile(false);
 }
@@ -368,4 +444,18 @@ const Graphics::Surface *Menu::getBigThumbnail() const {
 	return _bigThumbnail.empty() ? nullptr : &_bigThumbnail.rawSurface();
 }
 
+void MenuV1::switchToState(MainMenuAction state) {
+	_currentState = state;
+
+	if (state == MainMenuAction::Load) {
+		if (isOnNewSlot()) {
+			_selectedSavefileI = _savefiles.empty() ? 0 : _savefiles.size() - 1;
+			updateSelectedSavefile(false);
+		}
+	} else if (state == MainMenuAction::Save) {
+		_selectedSavefileI = _savefiles.size();
+		updateSelectedSavefile(false);
+	}
+}
+
 }
diff --git a/engines/alcachofa/menu.h b/engines/alcachofa/menu.h
index 2a3dc7fd736..1194cb42f33 100644
--- a/engines/alcachofa/menu.h
+++ b/engines/alcachofa/menu.h
@@ -38,7 +38,9 @@ enum class MainMenuAction : int32 {
 	NextSave,
 	PrevSave,
 	NewGame,
-	AlsoExit // there seems to be no difference to Exit
+	AlsoExit, // there seems to be no difference to Exit
+
+	ConfirmSavestate, // only used in V1
 };
 
 enum class OptionsMenuAction : int32 {
@@ -59,18 +61,18 @@ enum class OptionsMenuValue : int32 {
 class Menu {
 public:
 	Menu();
+	virtual ~Menu();
 
 	inline bool isOpen() const { return _isOpen; }
 	inline uint32 millisBeforeMenu() const { return _millisBeforeMenu; }
 	inline Room *previousRoom() { return _previousRoom; }
 	inline FakeSemaphore &interactionSemaphore() { return _interactionSemaphore; }
 
-	void resetAfterLoad();
-	void updateOpeningMenu();
-	void triggerMainMenuAction(MainMenuAction action);
 	void triggerLoad();
+	void resetAfterLoad();
+	virtual void updateOpeningMenu();
 
-	void openOptionsMenu();
+	virtual void triggerMainMenuAction(MainMenuAction action);
 	void triggerOptionsAction(OptionsMenuAction action);
 	void triggerOptionsValue(OptionsMenuValue valueId, float value);
 
@@ -79,13 +81,16 @@ public:
 	// as such - may return nullptr
 	const Graphics::Surface *getBigThumbnail() const;
 
-private:
+protected:
+	inline bool isOnNewSlot() const { return _selectedSavefileI >= _savefiles.size(); }
+	virtual void updateSelectedSavefile(bool hasJustSaved);
+	virtual void setOptionsState() = 0;
+
+	void openOptionsMenu();
 	void triggerSave();
-	void updateSelectedSavefile(bool hasJustSaved);
 	bool tryReadOldSavefile();
 	void continueGame();
 	void continueMainMenu();
-	void setOptionsState();
 
 	bool
 		_isOpen = false,
@@ -103,6 +108,31 @@ private:
 	Common::SaveFileManager *_saveFileMgr;
 };
 
+class MenuV3 : public Menu {
+public:
+	void triggerMainMenuAction(MainMenuAction action) override;
+
+protected:
+	void updateSelectedSavefile(bool hasJustSaved) override;
+	void setOptionsState() override;
+};
+
+class MenuV1 : public Menu {
+public:
+	void updateOpeningMenu() override;
+	void triggerMainMenuAction(MainMenuAction action) override;
+
+protected:
+	void updateSelectedSavefile(bool hasJustSaved) override;
+	void setOptionsState() override;
+
+private:
+	friend class ButtonV1;
+	void switchToState(MainMenuAction state);
+
+	MainMenuAction _currentState = MainMenuAction::ConfirmSavestate;
+};
+
 }
 
 #endif // ALCACHOFA_MENU_H
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index ae5125b352d..003c7be738a 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -24,6 +24,7 @@
 
 #include "alcachofa/shape.h"
 #include "alcachofa/graphics.h"
+#include "alcachofa/menu.h"
 
 #include "common/serializer.h"
 
@@ -164,14 +165,20 @@ public:
 	static constexpr const char *kClassName = "CBoton";
 	ButtonV1(Room *room, Common::SeekableReadStream &stream);
 
-	inline byte actionId() const { return _actionId; }
-	inline const Common::String &actionName() const { return _actionName; }
-
+	void loadResources() override;
+	void draw();
+	void update() override;
+	void onHoverUpdate() override;
+	void onClick() override;
 	const char *typeName() const override;
 
 private:
-	byte _actionId;
-	Common::String _actionName;
+	MenuV1 &menu();
+
+	MainMenuAction _action = {};
+	Common::String _graphicName;
+	ObjectBase *_graphicObject = nullptr;
+	bool _isHovered = false;
 };
 
 class MenuButton : public PhysicalObject {
diff --git a/engines/alcachofa/ui-objects.cpp b/engines/alcachofa/ui-objects.cpp
index 79b35bba11c..ca39d4c253f 100644
--- a/engines/alcachofa/ui-objects.cpp
+++ b/engines/alcachofa/ui-objects.cpp
@@ -33,9 +33,140 @@ namespace Alcachofa {
 const char *ButtonV1::typeName() const { return "ButtonV1"; }
 
 ButtonV1::ButtonV1(Room *room, Common::SeekableReadStream &stream)
-	: PhysicalObject(room, stream)
-	, _actionId(stream.readByte())
-	, _actionName(readVarString(stream)) {}
+	: PhysicalObject(room, stream) {
+	byte actionId = stream.readByte();
+	_graphicName = readVarString(stream);
+
+	switch (actionId) {
+	case 0:
+		_action = MainMenuAction::Save;
+		break;
+	case 1:
+		_action = MainMenuAction::Load;
+		break;
+	case 2:
+		_action = MainMenuAction::ContinueGame;
+		break;
+	case 3:
+		_action = MainMenuAction::Exit;
+		break;
+	case 4:
+		_action = MainMenuAction::InternetMenu;
+		break;
+	case 5:
+		_action = MainMenuAction::OptionsMenu;
+		break;
+	case 6:
+		_action = MainMenuAction::PrevSave;
+		break;
+	case 7:
+		_action = MainMenuAction::NextSave;
+		break;
+	case 10:
+		_action = MainMenuAction::ConfirmSavestate;
+		break;
+	default:
+		g_engine->game().unknownMenuAction(actionId);
+		break;
+	}
+}
+
+void ButtonV1::loadResources() {
+	if (!_graphicName.empty()) {
+		_graphicObject = room()->getObjectByName(_graphicName.c_str());
+		scumm_assert(_graphicObject != nullptr);
+		_graphicObject->toggle(false);
+	}
+}
+
+void ButtonV1::draw() {
+	if (menu()._currentState != _action)
+		return;
+
+	static constexpr uint kBufferSize = 32;
+	static char buffer[kBufferSize];
+	const char *text = nullptr;
+	switch (_action) {
+	case MainMenuAction::Save:
+		if (menu().isOnNewSlot())
+			text = "ENTRAR VACIA";
+		break;
+	case MainMenuAction::OptionsMenu:
+		int volumePercent = g_engine->config().musicVolume() * 10 / Audio::Mixer::kMaxChannelVolume;
+		snprintf(buffer, kBufferSize, "%s: %d", "VOLUMEN", volumePercent * 10);
+		text = buffer;
+		break;
+	}
+	if (text != nullptr) {
+		g_engine->drawQueue().add<TextDrawRequest>(
+			g_engine->globalUI().dialogFont(),
+			text,
+			Point(300, 250),
+			200,
+			true,
+			kWhite,
+			1
+		);
+	}
+}
+
+void ButtonV1::update() {
+	PhysicalObject::update();
+
+	if (_graphicObject != nullptr)
+		_graphicObject->toggle(_isHovered || menu()._currentState == _action);
+	_isHovered = false;
+
+	if (_action == MainMenuAction::ContinueGame && g_engine->input().wasMenuKeyPressed())
+		onClick();
+}
+
+void ButtonV1::onHoverUpdate() {
+	// we mainly override this function to disable drawing the object name
+
+	// only savegame selection buttons highlight on hover
+	if (_action == MainMenuAction::PrevSave || _action == MainMenuAction::NextSave)
+		_isHovered = true;
+}
+
+void ButtonV1::onClick() {
+	auto &menuState = menu()._currentState;
+	switch (_action) {
+	case MainMenuAction::Save:
+	case MainMenuAction::Load:
+	case MainMenuAction::OptionsMenu:
+		g_engine->config().saveToScummVM();
+		menu().switchToState(_action);
+		break;
+	case MainMenuAction::NextSave:
+	case MainMenuAction::PrevSave:
+		if (menuState == MainMenuAction::Load ||
+			menuState == MainMenuAction::Save ||
+			menuState == MainMenuAction::OptionsMenu)
+			g_engine->menu().triggerMainMenuAction(_action);
+		break;
+	case MainMenuAction::ContinueGame:
+	case MainMenuAction::InternetMenu:
+	case MainMenuAction::Exit:
+		g_engine->config().saveToScummVM();
+		g_engine->menu().triggerMainMenuAction(_action);
+		break;
+	case MainMenuAction::ConfirmSavestate:
+		if (menuState == MainMenuAction::Load ||
+			menuState == MainMenuAction::Save)
+			g_engine->menu().triggerMainMenuAction(menuState);
+		break;
+	default:
+		assert(false && "Unimplemented ButtonV1 action");
+		break;
+	}
+}
+
+MenuV1 &ButtonV1::menu() {
+	auto menuPtr = dynamic_cast<MenuV1 *>(&g_engine->menu());
+	assert(menuPtr != nullptr);
+	return *menuPtr;
+}
 
 const char *MenuButton::typeName() const { return "MenuButton"; }
 


Commit: a304228f6f9a70ab44edae439f04030947e38221
    https://github.com/scummvm/scummvm/commit/a304228f6f9a70ab44edae439f04030947e38221
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:30+01:00

Commit Message:
ALCACHOFA: Add subtitles toggle key

Changed paths:
    engines/alcachofa/input.cpp
    engines/alcachofa/input.h
    engines/alcachofa/metaengine.cpp
    engines/alcachofa/metaengine.h
    engines/alcachofa/player.cpp


diff --git a/engines/alcachofa/input.cpp b/engines/alcachofa/input.cpp
index bbf89cbfa5e..a883907aca1 100644
--- a/engines/alcachofa/input.cpp
+++ b/engines/alcachofa/input.cpp
@@ -37,6 +37,7 @@ void Input::nextFrame() {
 	_wasMouseRightReleased = false;
 	_wasMenuKeyPressed = false;
 	_wasInventoryKeyPressed = false;
+	_wasSubtitlesKeyPressed = false;
 	updateMousePos3D(); // camera transformation might have changed
 }
 
@@ -77,6 +78,9 @@ bool Input::handleEvent(const Common::Event &event) {
 		case EventAction::InputInventory:
 			_wasInventoryKeyPressed = true;
 			return true;
+		case EventAction::InputSubtitles:
+			_wasSubtitlesKeyPressed = true;
+			return true;
 		default:
 			return false;
 		}
diff --git a/engines/alcachofa/input.h b/engines/alcachofa/input.h
index 71285556b7f..11e03db9706 100644
--- a/engines/alcachofa/input.h
+++ b/engines/alcachofa/input.h
@@ -43,6 +43,7 @@ public:
 	inline bool isAnyMouseDown() const { return _isMouseLeftDown || _isMouseRightDown; }
 	inline bool wasMenuKeyPressed() const { return _wasMenuKeyPressed; }
 	inline bool wasInventoryKeyPressed() const { return _wasInventoryKeyPressed; }
+	inline bool wasSubtitlesKeyPressed() const { return _wasSubtitlesKeyPressed; }
 	inline Common::Point mousePos2D() const { return _mousePos2D; }
 	inline Common::Point mousePos3D() const { return _mousePos3D; }
 	const Input &debugInput() const { scumm_assert(_debugInput != nullptr); return *_debugInput; }
@@ -63,7 +64,8 @@ private:
 		_isMouseLeftDown = false,
 		_isMouseRightDown = false,
 		_wasMenuKeyPressed = false,
-		_wasInventoryKeyPressed = false;
+		_wasInventoryKeyPressed = false,
+		_wasSubtitlesKeyPressed = false;
 	Common::Point
 		_mousePos2D,
 		_mousePos3D;
diff --git a/engines/alcachofa/metaengine.cpp b/engines/alcachofa/metaengine.cpp
index a8c57db2e06..c53bf7b5168 100644
--- a/engines/alcachofa/metaengine.cpp
+++ b/engines/alcachofa/metaengine.cpp
@@ -105,7 +105,12 @@ KeymapArray AlcachofaMetaEngine::initKeymaps(const char *target) const {
 	act = new Action("INVENTORY", _("Inventory"));
 	act->setCustomEngineActionEvent((CustomEventType)EventAction::InputInventory);
 	act->addDefaultInputMapping("SPACE");
-	act->addDefaultInputMapping("JOY_B");
+	act->addDefaultInputMapping("JOY_X");
+	keymap->addAction(act);
+
+	act = new Action("SUBTITLES", _("Toggle subtitles"));
+	act->setCustomEngineActionEvent((CustomEventType)EventAction::InputSubtitles);
+	act->addDefaultInputMapping("C+t");
 	keymap->addAction(act);
 
 	return Keymap::arrayOf(keymap);
diff --git a/engines/alcachofa/metaengine.h b/engines/alcachofa/metaengine.h
index a01f695668c..f5a3a73eadc 100644
--- a/engines/alcachofa/metaengine.h
+++ b/engines/alcachofa/metaengine.h
@@ -29,7 +29,8 @@ namespace Alcachofa {
 enum class EventAction {
 	LoadFromMenu,
 	InputMenu,
-	InputInventory
+	InputInventory,
+	InputSubtitles
 };
 
 }
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 183a87ba814..044ff6769dd 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -39,6 +39,13 @@ Player::Player()
 void Player::preUpdate() {
 	_selectedObject = nullptr;
 	_cursorFrameI = 0;
+
+	if (g_engine->input().wasSubtitlesKeyPressed()) {
+		auto &config = g_engine->config();
+		config.subtitles() = !config.subtitles();
+		config.saveToScummVM();
+		debug(1, "Toggled subtitles to %s", config.subtitles() ? "on" : "off");
+	}
 }
 
 void Player::postUpdate() {


Commit: f5d7db401713c6858dc4b1f85bf0f752148ecda1
    https://github.com/scummvm/scummvm/commit/f5d7db401713c6858dc4b1f85bf0f752148ecda1
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:30+01:00

Commit Message:
ALCACHOFA: V1: Fix subtitle position

Changed paths:
    engines/alcachofa/game-movie-adventure-original.cpp
    engines/alcachofa/game-movie-adventure-special.cpp
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/game.h
    engines/alcachofa/rooms.cpp


diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index 453db2c7c9a..69a32a5691f 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -179,6 +179,10 @@ public:
 		return kNoXORKey;
 	}
 
+	Point getSubtitlePos() override {
+		return Point(g_system->getWidth() / 2, 150);
+	}
+
 	const char *getMenuRoom() override {
 		return "MENU";
 	}
diff --git a/engines/alcachofa/game-movie-adventure-special.cpp b/engines/alcachofa/game-movie-adventure-special.cpp
index da35b7a0ff8..3bfcd72d60f 100644
--- a/engines/alcachofa/game-movie-adventure-special.cpp
+++ b/engines/alcachofa/game-movie-adventure-special.cpp
@@ -232,6 +232,10 @@ public:
 		return kEmbeddedXORKey;
 	}
 
+	Point getSubtitlePos() override {
+		return Point(g_system->getWidth() / 2, g_system->getHeight() - 200);
+	}
+
 	const char *getMenuRoom() override {
 		return "MENUPRINCIPAL";
 	}
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 4ea63b6bb76..620d996c065 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -350,7 +350,7 @@ struct SayTextTask final : public Task {
 				g_engine->drawQueue().add<TextDrawRequest>(
 					g_engine->globalUI().dialogFont(),
 					g_engine->world().getDialogLine(_dialogId),
-					Point(g_system->getWidth() / 2, g_system->getHeight() - 200),
+					g_engine->game().getSubtitlePos(),
 					-1, true, kWhite, -kForegroundOrderCount);
 			}
 
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index a626d0f8ec5..3cb30f64c19 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -62,6 +62,7 @@ public:
 	virtual const char *getDialogFileName() = 0;
 	virtual const char *getObjectFileName() = 0;
 	virtual char getTextFileKey() = 0;
+	virtual Common::Point getSubtitlePos() = 0;
 	virtual const char *getMenuRoom() = 0;
 	virtual const char *getInitScriptName() = 0;
 	virtual int32 getKernelTaskArgCount(int32 kernelTaskI); // only necessary for V1
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 470264603b5..13cdbeed73e 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -865,10 +865,15 @@ static char *trimTrailing(char *start, char *end) {
 }
 
 void World::loadLocalizedNames() {
-	loadEncryptedFile(
-		g_engine->game().getObjectFileName(),
-		g_engine->game().getTextFileKey(),
-		_namesChunk);
+	const char *filename = g_engine->game().getObjectFileName();
+	char textFileKey = g_engine->game().getTextFileKey();
+#ifdef ALCACHOFA_DEBUG
+	if (File::exists("OBJETOS.MOD.TXT")) {
+		filename = "OBJETOS.MOD.TXT";
+		textFileKey = 0;
+	}
+#endif
+	loadEncryptedFile(filename, textFileKey, _namesChunk);
 	char *lineStart = _namesChunk.begin(), *fileEnd = _namesChunk.end() - 1;
 
 	if (*lineStart == '\"') {
@@ -921,10 +926,15 @@ void World::loadDialogLines() {
 	 * - The ID does not have to be correct, it is ignored by the original engine.
 	 * - We only need the dialog line and insert null-terminators where appropriate.
 	 */
-	loadEncryptedFile(
-		g_engine->game().getDialogFileName(),
-		g_engine->game().getTextFileKey(),
-		_dialogChunk);
+	const char *filename = g_engine->game().getDialogFileName();
+	char textFileKey = g_engine->game().getTextFileKey();
+#ifdef ALCACHOFA_DEBUG
+	if (File::exists("TEXTOS.MOD.TXT")) {
+		filename = "TEXTOS.MOD.TXT";
+		textFileKey = 0;
+	}
+#endif
+	loadEncryptedFile(filename, textFileKey, _dialogChunk);
 	char *lineStart = _dialogChunk.begin(), *fileEnd = _dialogChunk.end() - 1;
 	while (lineStart < fileEnd) {
 		char *lineEnd = find(lineStart, fileEnd, '\n');


Commit: 72f6a63534ec26aff3ff8aaf0495e33c5fc8e4fc
    https://github.com/scummvm/scummvm/commit/72f6a63534ec26aff3ff8aaf0495e33c5fc8e4fc
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:34+01:00

Commit Message:
ALCACHOFA: Add call address stack

In V1 instruction addresses are stored on a different stack, luckily a two-stack-solution is compatible to V3

Changed paths:
    engines/alcachofa/common.h
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/common.h b/engines/alcachofa/common.h
index c30e05631ed..b498a7b5bc7 100644
--- a/engines/alcachofa/common.h
+++ b/engines/alcachofa/common.h
@@ -154,7 +154,14 @@ inline void syncArray(Common::Serializer &serializer, Common::Array<T> &array, v
 }
 
 template<typename T>
-inline void syncStack(Common::Serializer &serializer, Common::Stack<T> &stack, void (*serializeFunction)(Common::Serializer &, T &)) {
+inline void syncStack(
+	Common::Serializer &serializer,
+	Common::Stack<T> &stack,
+	void (*serializeFunction)(Common::Serializer &, T &),
+	Common::Serializer::Version minVersion = 0) {
+	if (serializer.getVersion() < minVersion)
+		return;
+
 	auto size = stack.size();
 	serializer.syncAsUint32LE(size);
 	if (serializer.isLoading()) {
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 2b9d21114f4..0353cc011e9 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -107,6 +107,10 @@ static void syncAsSint32LE(Serializer &s, int32 &value) {
 	s.syncAsSint32LE(value);
 }
 
+static void syncAsUint32LE(Serializer &s, uint32 &value) {
+	s.syncAsUint32LE(value);
+}
+
 void Script::syncGame(Serializer &s) {
 	s.syncArray(_variables.data(), _variables.size(), syncAsSint32LE);
 }
@@ -224,8 +228,8 @@ struct ScriptTask final : public Task {
 		, _name(forkParent._name + " FORKED")
 		, _pc(forkParent._pc)
 		, _characterLock(forkParent._characterLock) {
-		for (uint i = 0; i < forkParent._stack.size(); i++)
-			_stack.push(forkParent._stack[i]);
+		for (uint i = 0; i < forkParent._valueStack.size(); i++)
+			_valueStack.push(forkParent._valueStack[i]);
 		pushNumber(1); // this task is the forked one
 		debugC(SCRIPT_DEBUG_LVL_TASKS, kDebugScript, "%u: Script fork from %u at %u", process.pid(), forkParent.process().pid(), _pc);
 	}
@@ -258,10 +262,10 @@ struct ScriptTask final : public Task {
 
 				debugN("%u: %5u %-12s %8d Stack: ",
 					process().pid(), _pc - 1, opName, instruction._arg);
-				if (_stack.empty())
+				if (_valueStack.empty())
 					debug("empty");
 				else {
-					const auto &top = _stack.top();
+					const auto &top = _valueStack.top();
 					switch (top._type) {
 					case StackEntryType::Number:
 						debug("Number %d", top._number);
@@ -289,9 +293,9 @@ struct ScriptTask final : public Task {
 			switch (opMap[instruction._op]) {
 			case ScriptOp::Nop: break;
 			case ScriptOp::Dup:
-				if (_stack.empty())
+				if (_valueStack.empty())
 					error("Script tried to duplicate stack top, but stack is empty");
-				_stack.push(_stack.top());
+				_valueStack.push(_valueStack.top());
 				break;
 			case ScriptOp::PushAddr:
 				pushVariable(instruction._arg);
@@ -414,23 +418,32 @@ struct ScriptTask final : public Task {
 	}
 
 	void syncGame(Serializer &s) override {
-		assert(s.isSaving() || (_characterLock.isReleased() && _stack.empty()));
+		assert(s.isSaving() || (
+			_characterLock.isReleased() && _valueStack.empty() && _callStack.empty()));
 
 		s.syncString(_name);
 		s.syncAsUint32LE(_pc);
 		s.syncAsByte(_returnsFromKernelCall);
 		s.syncAsByte(_isFirstExecution);
 
-		uint count = _stack.size();
+		uint count = _valueStack.size();
 		s.syncAsUint32LE(count);
 		if (s.isLoading()) {
-			for (uint i = 0; i < count; i++)
-				_stack.push(StackEntry(s));
+			for (uint i = 0; i < count; i++) {
+				const StackEntry entry(s);
+				if (entry._type == StackEntryType::Instruction)
+					// we still have to support old saves
+					_callStack.push(entry._index);
+				else
+					_valueStack.push(entry);
+			}
 		} else {
 			for (uint i = 0; i < count; i++)
-				_stack[i].syncGame(s);
+				_valueStack[i].syncGame(s);
 		}
 
+		syncStack(s, _callStack, syncAsUint32LE, SaveVersion::kWithEngineV10);
+
 		bool hasLock = !_characterLock.isReleased();
 		s.syncAsByte(hasLock);
 		if (s.isLoading() && hasLock)
@@ -467,7 +480,7 @@ private:
 	}
 
 	void pushNumber(int32 value) {
-		_stack.push({ StackEntryType::Number, value });
+		_valueStack.push({ StackEntryType::Number, value });
 	}
 
 	// For the following methods error recovery is not really viable
@@ -476,23 +489,23 @@ private:
 		uint32 index = offset / sizeof(int32);
 		if (offset % sizeof(int32) != 0 || index >= _script._variables.size())
 			error("Script tried to push invalid variable offset");
-		_stack.push({ StackEntryType::Variable, index });
+		_valueStack.push({ StackEntryType::Variable, index });
 	}
 
 	void pushString(uint32 offset) {
 		if (offset >= _script._strings->size())
 			error("Script tried to push invalid string offset");
-		_stack.push({ StackEntryType::String, offset });
+		_valueStack.push({ StackEntryType::String, offset });
 	}
 
 	void pushInstruction(uint32 pc) {
-		_stack.push({ StackEntryType::Instruction, pc });
+		_callStack.push(pc);
 	}
 
 	StackEntry pop() {
-		if (_stack.empty())
+		if (_valueStack.empty())
 			error("Script tried to pop empty stack");
-		return _stack.pop();
+		return _valueStack.pop();
 	}
 
 	int32 popNumber() {
@@ -517,23 +530,24 @@ private:
 	}
 
 	uint32 popInstruction() {
-		auto entry = pop();
-		if (entry._type != StackEntryType::Instruction)
-			error("Script tried to pop but top of stack is not an instruction");
-		return entry._index;
+		if (_callStack.empty()) {
+			warning("Script tried to pop empty call stack");
+			return UINT_MAX; // this will just exit script execution
+		}
+		return _callStack.pop();
 	}
 
 	void popN(int32 count) {
-		if (count < 0 || (uint)count > _stack.size())
+		if (count < 0 || (uint)count > _valueStack.size())
 			error("Script tried to pop more entries than are available on the stack");
 		for (int32 i = 0; i < count; i++)
-			_stack.pop();
+			_valueStack.pop();
 	}
 
 	StackEntry getArg(uint argI) {
-		if (_stack.size() < argI + 1)
+		if (_valueStack.size() < argI + 1)
 			error("Script did not supply enough arguments for kernel call");
-		return _stack[_stack.size() - 1 - argI];
+		return _valueStack[_valueStack.size() - 1 - argI];
 	}
 
 	int32 getNumberArg(uint argI) {
@@ -1044,7 +1058,10 @@ private:
 	}
 
 	Script &_script;
-	Stack<StackEntry> _stack;
+	// in V3 value and call return addresses used the same stack
+	// in V1 they are separate, but the two-stack-solution should be compatible with V3
+	Stack<StackEntry> _valueStack;
+	Stack<uint32> _callStack;
 	String _name;
 	uint32 _pc = 0;
 	int32 _lastKernelTaskI = 0;


Commit: 6ab60b86483e4a6aa2e638e82d5a67abac7773b3
    https://github.com/scummvm/scummvm/commit/6ab60b86483e4a6aa2e638e82d5a67abac7773b3
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:34+01:00

Commit Message:
ALCACHOFA: V1: Fix missing character icon during dialog menus

Changed paths:
    engines/alcachofa/game-objects.cpp


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 620d996c065..207b618b147 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -1074,6 +1074,7 @@ struct DialogMenuTask final : public Task {
 	TaskReturn run() override {
 		TASK_BEGIN;
 		layoutLines();
+		process().unlockInteraction();
 		while (true) {
 			TASK_YIELD(1);
 			if (g_engine->player().activeCharacter() != _character) //-V779
@@ -1085,6 +1086,7 @@ struct DialogMenuTask final : public Task {
 			_clickedLineI = updateLines();
 			if (_clickedLineI != UINT_MAX) {
 				TASK_YIELD(2);
+				process().lockInteraction();
 				TASK_WAIT(3, _character->sayText(process(), _character->_dialogLines[_clickedLineI]._dialogId));
 				int32 returnValue = _character->_dialogLines[_clickedLineI]._returnValue;
 				_character->_dialogLines.clear();


Commit: f6f596eb1e3a91176b45f846777c423bda82e43d
    https://github.com/scummvm/scummvm/commit/f6f596eb1e3a91176b45f846777c423bda82e43d
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:34+01:00

Commit Message:
ALCACHOFA: V1: Fix various original bugs in POBLADO_INDIO

Changed paths:
    engines/alcachofa/game-movie-adventure-original.cpp
    engines/alcachofa/game-movie-adventure-special.cpp
    engines/alcachofa/game.cpp
    engines/alcachofa/game.h
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index 69a32a5691f..f846eaa3d81 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -248,6 +248,13 @@ public:
 		return false;
 	}
 
+	Character *unknownSayTextCharacter(const char *name, int32 dialogId) override {
+		// an original bug in room POBLADO_INDIO, a dialog line would be skipped
+		if (!scumm_stricmp(name, "JEFE_INDIO_HABLA_POSTE"))
+			return dynamic_cast<Character *>(g_engine->player().currentRoom()->getObjectByName("JEFE_HABLA_POSTE"));
+		return Game::unknownSayTextCharacter(name, dialogId);
+	}
+
 	void missingAnimation(const String &fileName) override {
 		static const char *exemptions[] = {
 			nullptr
@@ -266,6 +273,39 @@ public:
 		else
 			Game::missingAnimation(fileName);
 	}
+
+	void onUserChangedCharacter() override {
+		// An original bug in room POBLADO_INDIO if filemon is bound and mortadelo enters the room
+		// the door A_PUENTE which was disabled is reenabled to allow mortadelo leaving
+		// However if the user now changes character, the door is still enabled and filemon can
+		// enter a ghost state walking through a couple rooms and softlocking.
+		if (g_engine->player().currentRoom()->name().equalsIgnoreCase("POBLADO_INDIO"))
+			g_engine->script().createProcess(g_engine->player().activeCharacterKind(), "ENTRAR_POBLADO_INDIO");
+	}
+
+	PointObject *unknownGoPutTarget(const Process &process, const char *action, const char *name) override {
+		if (scumm_stricmp(action, "put"))
+			return Game::unknownGoPutTarget(process, action, name);
+
+		if (!scumm_stricmp("A_Poblado_Indio", name)) {
+			// A_Poblado_Indio is a Door but is originally cast into a PointObject
+			// a pointer and the draw order is then interpreted as position and the character snapped onto the floor shape.
+			// Instead I just use the A_Poblado_Indio1 object which exists as counter-part for A_Poblado_Indio2 which should have been used
+			auto target = dynamic_cast<PointObject *>(
+				g_engine->world().getObjectByName(process.character(), "A_Poblado_Indio1"));
+			if (target == nullptr)
+				_message("Unknown put target A_Poblado_Indio1 during exemption for A_Poblado_Indio");
+			return target;
+		}
+
+		return Game::unknownGoPutTarget(process, action, name);
+	}
+
+	void missingSound(const Common::String &fileName) override {
+		if (fileName == "CHAS")
+			return;
+		return Game::missingSound(fileName);
+	}
 };
 
 Game *Game::createForMovieAdventureOriginal() {
diff --git a/engines/alcachofa/game-movie-adventure-special.cpp b/engines/alcachofa/game-movie-adventure-special.cpp
index 3bfcd72d60f..51cdea7e01f 100644
--- a/engines/alcachofa/game-movie-adventure-special.cpp
+++ b/engines/alcachofa/game-movie-adventure-special.cpp
@@ -501,10 +501,10 @@ public:
 		Game::unknownAnimateObject(name);
 	}
 
-	void unknownSayTextCharacter(const char *name, int32 dialogId) override {
+	Character *unknownSayTextCharacter(const char *name, int32 dialogId) override {
 		if (!scumm_stricmp(name, "OFELIA") && dialogId == 3737)
-			return;
-		Game::unknownSayTextCharacter(name, dialogId);
+			return nullptr;
+		return Game::unknownSayTextCharacter(name, dialogId);
 	}
 
 	void missingSound(const String &fileName) override {
diff --git a/engines/alcachofa/game.cpp b/engines/alcachofa/game.cpp
index 9ed6a7dfefc..32e87807a0f 100644
--- a/engines/alcachofa/game.cpp
+++ b/engines/alcachofa/game.cpp
@@ -148,8 +148,9 @@ void Game::missingAnimation(const String &fileName) {
 	_message("Could not open animation %s", fileName.c_str());
 }
 
-void Game::unknownSayTextCharacter(const char *name, int32) {
+Character *Game::unknownSayTextCharacter(const char *name, int32) {
 	unknownScriptCharacter("say text", name);
+	return nullptr;
 }
 
 void Game::unknownChangeCharacterRoom(const char *name) {
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index 3cb30f64c19..1d3cf800107 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -103,7 +103,7 @@ public:
 	virtual PointObject *unknownGoPutTarget(const Process &process, const char *action, const char *name); ///< May return an alternative target to use
 	virtual void unknownChangeCharacterRoom(const char *name);
 	virtual void unknownAnimateCharacterObject(const char *name);
-	virtual void unknownSayTextCharacter(const char *name, int32 dialogId);
+	virtual Character *unknownSayTextCharacter(const char *name, int32 dialogId);
 	virtual void unknownAnimateTalkingObject(const char *name);
 	virtual void unknownClearInventoryTarget(int characterKind);
 	virtual void unknownCamLerpTarget(const char *action, const char *name);
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 0353cc011e9..54acc48b28a 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -867,10 +867,10 @@ private:
 			Character *_character = strcmp(characterName, "AMBOS") == 0
 				? &relatedCharacter()
 				: getObjectArg<Character>(0);
-			if (_character == nullptr) {
-				g_engine->game().unknownSayTextCharacter(characterName, dialogId);
+			if (_character == nullptr)
+				_character = g_engine->game().unknownSayTextCharacter(characterName, dialogId);
+			if (_character == nullptr)
 				return TaskReturn::finish(1);
-			}
 			return TaskReturn::waitFor(_character->sayText(process(), dialogId));
 		};
 		case ScriptKernelTask::SetDialogLineReturn:


Commit: ff43ad36dc2829b2f4dcfad787f12dbd9bfaa0b7
    https://github.com/scummvm/scummvm/commit/ff43ad36dc2829b2f4dcfad787f12dbd9bfaa0b7
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:35+01:00

Commit Message:
ALCACHOFA: V1: Fix object ref from other room

Changed paths:
    engines/alcachofa/game-movie-adventure-original.cpp
    engines/alcachofa/game.cpp
    engines/alcachofa/game.h
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index f846eaa3d81..c1cddf8ba70 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -283,6 +283,25 @@ public:
 			g_engine->script().createProcess(g_engine->player().activeCharacterKind(), "ENTRAR_POBLADO_INDIO");
 	}
 
+	PointObject *getPointFromRoom(const char *roomName, const char *objectName, const char *action) {
+		const auto *room = g_engine->world().getRoomByName(roomName);
+		if (room == nullptr) {
+			_message("Could not find room %s to get %s for %s");
+			return nullptr;
+		}
+		auto *object = room->getObjectByName(objectName);
+		if (object == nullptr) {
+			_message("Could not find object %s in room %s for %s");
+			return nullptr;
+		}
+		auto *point = dynamic_cast<PointObject *>(object);
+		if (point == nullptr) {
+			_message("Object %s in room %s for %s is not a point object");
+			return nullptr;
+		}
+		return point;
+	}
+
 	PointObject *unknownGoPutTarget(const Process &process, const char *action, const char *name) override {
 		if (scumm_stricmp(action, "put"))
 			return Game::unknownGoPutTarget(process, action, name);
@@ -298,9 +317,24 @@ public:
 			return target;
 		}
 
+		// an original bug, Pos_Final_Morta/File is defined in room ENTRADA_PUEBLO
+		// but the current room is ENTRADA_PUEBLO_INTRO
+		if (!scumm_stricmp(name, "Pos_Final_Morta"))
+			return getPointFromRoom("ENTRADA_PUEBLO", "Pos_Final_Morta", action);
+		if (!scumm_stricmp(name, "Pos_Final_File"))
+			return getPointFromRoom("ENTRADA_PUEBLO", "Pos_Final_File", action);
+
 		return Game::unknownGoPutTarget(process, action, name);
 	}
 
+	PointObject *unknownCamLerpTarget(const char *action, const char *name) override {
+		// the same bug as in unknownGoPutTarget
+		if (!scumm_stricmp(name, "Pos_Final_Morta"))
+			return getPointFromRoom("ENTRADA_PUEBLO", "Pos_Final_Morta", action);
+
+		return Game::unknownCamLerpTarget(action, name);
+	}
+
 	void missingSound(const Common::String &fileName) override {
 		if (fileName == "CHAS")
 			return;
diff --git a/engines/alcachofa/game.cpp b/engines/alcachofa/game.cpp
index 32e87807a0f..8e1e640b429 100644
--- a/engines/alcachofa/game.cpp
+++ b/engines/alcachofa/game.cpp
@@ -169,8 +169,9 @@ void Game::unknownClearInventoryTarget(int characterKind) {
 	_message("Invalid clear inventory character kind: %d", characterKind);
 }
 
-void Game::unknownCamLerpTarget(const char *action, const char *name) {
+PointObject *Game::unknownCamLerpTarget(const char *action, const char *name) {
 	_message("Invalid target object for %s: %s", action, name);
+	return nullptr;
 }
 
 void Game::unknownKernelTask(int task) {
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index 1d3cf800107..95c5c79653d 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -106,7 +106,7 @@ public:
 	virtual Character *unknownSayTextCharacter(const char *name, int32 dialogId);
 	virtual void unknownAnimateTalkingObject(const char *name);
 	virtual void unknownClearInventoryTarget(int characterKind);
-	virtual void unknownCamLerpTarget(const char *action, const char *name);
+	virtual PointObject *unknownCamLerpTarget(const char *action, const char *name);
 	virtual void unknownKernelTask(int task);
 	virtual void unknownScriptProcedure(const Common::String &procedure);
 	virtual void unknownMenuAction(int32 actionId);
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 54acc48b28a..52284b6e5ee 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -950,10 +950,10 @@ private:
 			if (!process().isActiveForPlayer())
 				return TaskReturn::finish(0); // contrary to ...ResettingZ this one does not delay if not active
 			auto pointObject = getObjectArg<PointObject>(0);
-			if (pointObject == nullptr) {
-				g_engine->game().unknownCamLerpTarget("LerpCamToObjectKeepingZ", getStringArg(0));
+			if (pointObject == nullptr)
+				pointObject = g_engine->game().unknownCamLerpTarget("LerpCamToObjectKeepingZ", getStringArg(0));
+			if (pointObject == nullptr)
 				return TaskReturn::finish(1);
-			}
 			return TaskReturn::waitFor(g_engine->camera().lerpPos(process(),
 				as2D(pointObject->position()),
 				getNumberArg(1), EasingType::Linear));
@@ -962,10 +962,10 @@ private:
 			if (!process().isActiveForPlayer())
 				return TaskReturn::waitFor(delay(getNumberArg(1)));
 			auto pointObject = getObjectArg<PointObject>(0);
-			if (pointObject == nullptr) {
-				g_engine->game().unknownCamLerpTarget("LerpCamToObjectResettingZ", getStringArg(0));
+			if (pointObject == nullptr)
+				pointObject = g_engine->game().unknownCamLerpTarget("LerpCamToObjectResettingZ", getStringArg(0));
+			if (pointObject == nullptr)
 				return TaskReturn::finish(1);
-			}
 			return TaskReturn::waitFor(g_engine->camera().lerpPos(process(),
 				as3D(pointObject->position()),
 				getNumberArg(1), (EasingType)getNumberArg(2)));
@@ -976,10 +976,10 @@ private:
 				// the scale will wait then snap the scale
 				return TaskReturn::waitFor(g_engine->camera().lerpScale(process(), targetScale, getNumberArg(2), EasingType::Linear));
 			auto pointObject = getObjectArg<PointObject>(0);
-			if (pointObject == nullptr) {
-				g_engine->game().unknownCamLerpTarget("LerpCamToObjectWithScale", getStringArg(0));
+			if (pointObject == nullptr)
+				pointObject = g_engine->game().unknownCamLerpTarget("LerpCamToObjectWithScale", getStringArg(0));
+			if (pointObject == nullptr)
 				return TaskReturn::finish(1);
-			}
 			return TaskReturn::waitFor(g_engine->camera().lerpPosScale(process(),
 				as3D(pointObject->position()), targetScale,
 				getNumberArg(2), (EasingType)getNumberArg(3), (EasingType)getNumberArg(4)));


Commit: b0f962d68cc0917e4614d7816734b7cd1dc1ddce
    https://github.com/scummvm/scummvm/commit/b0f962d68cc0917e4614d7816734b7cd1dc1ddce
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:35+01:00

Commit Message:
ALCACHOFA: V1: Downport fix for Puerta_Casa_Freddy_Intermedia

Changed paths:
    engines/alcachofa/game-movie-adventure-original.cpp


diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index c1cddf8ba70..53314a620b4 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -317,6 +317,11 @@ public:
 			return target;
 		}
 
+		if (!scumm_stricmp("Puerta_Casa_Freddy_Intermedia", name)) {
+			// Another case of a door being cast into a PointObject
+			return nullptr;
+		}
+
 		// an original bug, Pos_Final_Morta/File is defined in room ENTRADA_PUEBLO
 		// but the current room is ENTRADA_PUEBLO_INTRO
 		if (!scumm_stricmp(name, "Pos_Final_Morta"))


Commit: 23c14147b2a0777867870ce2bf855ae35ca59612
    https://github.com/scummvm/scummvm/commit/23c14147b2a0777867870ce2bf855ae35ca59612
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:35+01:00

Commit Message:
ALCACHOFA: Fix misuse of variable address for kernel args

Changed paths:
    engines/alcachofa/game-movie-adventure-original.cpp
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index 53314a620b4..7f82b8ead8d 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -303,6 +303,9 @@ public:
 	}
 
 	PointObject *unknownGoPutTarget(const Process &process, const char *action, const char *name) override {
+		if (!*name) // this can happen when a variable address is misused as string
+			return nullptr;
+
 		if (scumm_stricmp(action, "put"))
 			return Game::unknownGoPutTarget(process, action, name);
 
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 52284b6e5ee..93bc6221ec4 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -552,9 +552,15 @@ private:
 
 	int32 getNumberArg(uint argI) {
 		auto entry = getArg(argI);
-		if (entry._type != StackEntryType::Number)
+		switch (entry._type) {
+		case StackEntryType::Number:
+			return entry._number;
+		case StackEntryType::Variable:
+			warning("Misuse of variable address as number for arg %u at %u", argI, _pc);
+			return entry._number;
+		default:
 			error("Expected number in argument %u for kernel call", argI);
-		return entry._number;
+		}
 	}
 
 	MainCharacterKind getMainCharacterKindArg(uint argI) {
@@ -576,9 +582,15 @@ private:
 
 	const char *getStringArg(uint argI) {
 		auto entry = getArg(argI);
-		if (entry._type != StackEntryType::String)
+		switch (entry._type) {
+		case StackEntryType::String:
+			return &_script._strings[entry._index];
+		case StackEntryType::Variable:
+			warning("Misuse of variable address as string for arg %u at %u", argI, _pc);
+			return "";
+		default:
 			error("Expected string in argument %u for kernel call", argI);
-		return &_script._strings[entry._index];
+		}
 	}
 
 	int32 getNumberOrStringArg(uint argI) {


Commit: aff06afe3d37ecab4343fe60492f3c16c86405b3
    https://github.com/scummvm/scummvm/commit/aff06afe3d37ecab4343fe60492f3c16c86405b3
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:35+01:00

Commit Message:
ALCACHOFA: V1: Fix sound paths with extension and mixed-casing

Changed paths:
    engines/alcachofa/rooms.cpp
    engines/alcachofa/sounds.cpp


diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 13cdbeed73e..bb5956b18dc 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -1069,7 +1069,7 @@ public:
 private:
 	const SharedPtr<EmbeddedArchiveMember> getMemberInternal(const Path &path) const {
 		for (const auto &member : _members) {
-			if (member->getPathInArchive() == path)
+			if (member->getPathInArchive().equalsIgnoreCase(path))
 				return member;
 		}
 		return nullptr;
diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index 50a028ff5d1..64a2252e5df 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -113,8 +113,11 @@ static AudioStream *loadSND(File *file) {
 }
 
 static AudioStream *openAudio(const String &basePath) {
-	String path = basePath + ".SND";
 	File *file = new File();
+	if (file->open(basePath.c_str())) // only very rarely the basePath already has the extension
+		return makeWAVStream(file, DisposeAfterUse::YES);
+
+	String path = basePath + ".SND";
 	if (file->open(path.c_str()))
 		return file->size() == 0 // Movie Adventure has some null-size audio files, they are treated like infinite silence
 			? makeSilentAudioStream(8000, false) 


Commit: 07805df5966b9d233377299c52fca120948e01c9
    https://github.com/scummvm/scummvm/commit/07805df5966b9d233377299c52fca120948e01c9
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:35+01:00

Commit Message:
ALCACHOFA: Fix invalid music when changing chars

Changed paths:
    engines/alcachofa/sounds.cpp


diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index 64a2252e5df..71559cc562e 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -311,6 +311,8 @@ void Sounds::startMusic(int musicId) {
 	debugC(2, kDebugSounds, "startMusic %d", musicId);
 	assert(musicId >= 0);
 	fadeMusic();
+	if (musicId == 0)
+		return;
 	auto path = g_engine->game().getMusicPath(musicId);
 	_musicSoundID = playSoundInternal(path, Mixer::kMaxChannelVolume, Mixer::kMusicSoundType);
 	_isMusicPlaying = true;


Commit: 5fdf0dfd9933cc03a72f5b02ae8a674fea540c80
    https://github.com/scummvm/scummvm/commit/5fdf0dfd9933cc03a72f5b02ae8a674fea540c80
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:35+01:00

Commit Message:
ALCACHOFA: V1: Adapt ScriptTimerTask

Changed paths:
    engines/alcachofa/game-movie-adventure-original.cpp
    engines/alcachofa/script.cpp
    engines/alcachofa/script.h


diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index 7f82b8ead8d..38e91a6a665 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -86,7 +86,7 @@ static constexpr const ScriptKernelTask kScriptKernelTaskMap[] = {
 	ScriptKernelTask::Off,
 	ScriptKernelTask::Pickup,
 	ScriptKernelTask::Animate,
-	ScriptKernelTask::SheriffTakesCharacter,
+	ScriptKernelTask::HadNoMousePressFor,
 	ScriptKernelTask::ChangeCharacter,
 	ScriptKernelTask::LerpCamToObjectKeepingZ,
 	ScriptKernelTask::Drop,
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 93bc6221ec4..99dfe140878 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -153,12 +153,20 @@ struct ScriptTimerTask final : public Task {
 	TaskReturn run() override {
 		TASK_BEGIN;
 		{
-			uint32 timeSinceTimer = g_engine->script()._scriptTimer == 0 ? 0
-				: (g_engine->getMillis() - g_engine->script()._scriptTimer) / 1000;
+			_didPressMouse |= g_engine->input().wasAnyMousePressed();
+
+			// only in V31 there is the variable "CalcularTiempoSinPulsarRaton" which controls
+			// resetting the timer, for all other versions we have to do it implicitly anyways
+			if (g_engine->script()._scriptTimer == 0)
+				g_engine->script()._scriptTimer = g_engine->getMillis();
+
+			uint32 timeSinceTimer =  (g_engine->getMillis() - g_engine->script()._scriptTimer) / 1000;
 			if (_durationSec >= (int32)timeSinceTimer)
-				_result = g_engine->script().variable("SeHaPulsadoRaton") ? 0 : 2;
-			else
+				_result = _didPressMouse ? 0 : 2;
+			else {
 				_result = 1;
+				g_engine->script()._scriptTimer = 0;
+			}
 			g_engine->player().drawCursor();
 		}
 		TASK_YIELD(1); // Wait a frame to not produce an endless loop
@@ -174,6 +182,7 @@ struct ScriptTimerTask final : public Task {
 		Task::syncGame(s);
 		s.syncAsSint32LE(_durationSec);
 		s.syncAsSint32LE(_result);
+		s.syncAsByte(_didPressMouse, SaveVersion::kWithEngineV10);
 	}
 
 	const char *taskName() const override;
@@ -181,6 +190,7 @@ struct ScriptTimerTask final : public Task {
 private:
 	int32 _durationSec = 0;
 	int32 _result = 1;
+	bool _didPressMouse = false;
 };
 DECLARE_TASK(ScriptTimerTask)
 
@@ -678,8 +688,10 @@ private:
 			return getNumberArg(0) <= 0
 				? TaskReturn::finish(0)
 				: TaskReturn::waitFor(delay((uint32)getNumberArg(0)));
-		case ScriptKernelTask::HadNoMousePressFor:
-			return TaskReturn::waitFor(new ScriptTimerTask(process(), getNumberArg(0)));
+		case ScriptKernelTask::HadNoMousePressFor: {
+			auto duration = g_engine->isV1() ? 60 : getNumberArg(0);
+			return TaskReturn::waitFor(new ScriptTimerTask(process(), duration));
+		}
 		case ScriptKernelTask::Fork:
 			g_engine->scheduler().createProcess<ScriptTask>(process().character(), *this);
 			return TaskReturn::finish(0); // 0 means this is the forking process
diff --git a/engines/alcachofa/script.h b/engines/alcachofa/script.h
index e9cb9eb2846..e2595794fb1 100644
--- a/engines/alcachofa/script.h
+++ b/engines/alcachofa/script.h
@@ -126,8 +126,7 @@ enum class ScriptKernelTask {
 	LerpCamXYZ,
 	LerpCamToObjectKeepingZ,
 
-	SheriffTakesCharacter, ///< some special-case V1 tasks, unknown yet
-	ChangeDoor,
+	ChangeDoor, ///< some special-case V1 tasks, unknown yet
 	Disguise
 };
 


Commit: afa758a8e8487872dc0e7e1d4617f820b93fa834
    https://github.com/scummvm/scummvm/commit/afa758a8e8487872dc0e7e1d4617f820b93fa834
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:35+01:00

Commit Message:
ALCACHOFA: V1: Add support for Terror and Vaqueros

Changed paths:
    engines/alcachofa/detection_tables.h
    engines/alcachofa/game-movie-adventure-original.cpp
    engines/alcachofa/game.cpp
    engines/alcachofa/game.h


diff --git a/engines/alcachofa/detection_tables.h b/engines/alcachofa/detection_tables.h
index b907e020ed1..9a0bfe6aafd 100644
--- a/engines/alcachofa/detection_tables.h
+++ b/engines/alcachofa/detection_tables.h
@@ -23,6 +23,8 @@ namespace Alcachofa {
 
 const PlainGameDescriptor alcachofaGames[] = {
 	{ "aventuradecine", "Mort & Phil: A Movie Adventure" },
+	{ "terror", "Mortadelo y Filemón: Terror, Espanto y Pavor"},
+	{ "vaqueros", "Mortadelo y Filemón: Dos vaqueros chapuceros"},
 	{ 0, 0 }
 };
 
@@ -146,6 +148,38 @@ const AlcachofaGameDescription gameDescriptions[] = {
 		EngineVersion::V1_0
 	},
 
+	//
+	// Terror, Espanto y Pavor
+	//
+	{
+		// Install folder from the disk/the ISO within the Steam distribution
+		{
+			"terror",
+			"Mortadelo y Filemón: Terror, Espanto y Pavor",
+			AD_ENTRY1s("terror.emc", "dc9357ee618bff160e2e2afa168ba913", 170113868),
+			Common::ES_ESP,
+			Common::kPlatformWindows,
+			ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE | ADGF_CD
+		},
+		EngineVersion::V1_0
+	},
+
+	//
+	// Dos vaqueros chapuceros
+	//
+	{
+		// Install folder from the disk/the ISO within the Steam distribution
+		{
+			"vaqueros",
+			"Mortadelo y Filemón: Dos vaqueros chapuceros",
+			AD_ENTRY1s("oeste.emc", "b4c1084557d4cfbae336f0e741ec9e9f", 183099320),
+			Common::ES_ESP,
+			Common::kPlatformWindows,
+			ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE | ADGF_CD
+		},
+		EngineVersion::V1_0
+	},
+
 	{ AD_TABLE_END_MARKER, EngineVersion::V1_0 }
 };
 
diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index 38e91a6a665..ea38e413173 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -133,17 +133,27 @@ static constexpr const int kKernelTaskArgCounts[] = {
 	0
 };
 
-class GameMovieAdventureOriginal : public Game {
-public:
-	GameMovieAdventureOriginal() {
-		const auto &desc = g_engine->gameDescription();
-		if (desc.desc.flags & ADGF_CD) {
-			const Path gameDir = ConfMan.getPath("path");
-			SearchMan.addDirectory(gameDir.append("disk1/Install"));
-			SearchMan.addDirectory(gameDir.append("disk2/Install"));
-		}
-	}
+static constexpr const char *kMapFilesMovieAdventure[] = {
+	"oeste.emc",
+	"terror.emc",
+	"global.emc",
+	nullptr
+};
+
+static constexpr const char *kMapFilesTerror[] = {
+	"terror.emc",
+	"global.emc",
+	nullptr
+};
+
+static constexpr const char *kMapFilesVaqueros[] = {
+	"oeste.emc",
+	"global.emc",
+	nullptr
+};
 
+class GameWithVersion1 : public Game {
+public:
 	Point getResolution() override {
 		return Point(800, 600);
 	}
@@ -152,16 +162,6 @@ public:
 		return Point(266, 200);
 	}
 
-	static constexpr const char *kMapFiles[] = {
-		"oeste.emc",
-		"terror.emc",
-		"global.emc",
-		nullptr
-	};
-	const char *const *getMapFiles() override {
-		return kMapFiles;
-	}
-
 	GameFileReference getScriptFileRef() override {
 		// V1 embeds the script into global.emc, it is overridden during world load
 		return {};
@@ -209,20 +209,10 @@ public:
 			g_engine->world().mortadelo().room() == g_engine->world().filemon().room();
 	}
 
-	Path getVideoPath(int32 videoId) override {
-		return Path(String::format("disk1/Install/bin/data%02d.bin", videoId));
-	}
-
 	String getSoundPath(const char *filename) override {
 		return filename;
 	}
 
-	String getMusicPath(int32 trackId) override {
-		const Room *room = g_engine->player().currentRoom();
-		const int diskId = room != nullptr && room->mapIndex() == 1 ? 2 : 1;
-		return String::format("disk%d/track%02d", diskId, trackId);
-	}
-
 	int32 getCharacterJingle(MainCharacterKind kind) override {
 		return kind == MainCharacterKind::Mortadelo ? 15 : 16;
 	}
@@ -344,14 +334,100 @@ public:
 	}
 
 	void missingSound(const Common::String &fileName) override {
-		if (fileName == "CHAS")
+		if (fileName == "CHAS" || fileName == "0563" || fileName == "M2137")
 			return;
 		return Game::missingSound(fileName);
 	}
 };
 
+class GameMovieAdventureOriginal : public GameWithVersion1 {
+public:
+	GameMovieAdventureOriginal() {
+		const auto &desc = g_engine->gameDescription();
+		if (desc.desc.flags & ADGF_CD) {
+			const Path gameDir = ConfMan.getPath("path");
+			SearchMan.addDirectory(gameDir.append("disk1/Install"));
+			SearchMan.addDirectory(gameDir.append("disk2/Install"));
+		}
+	}
+
+	const char *const *getMapFiles() override {
+		return kMapFilesMovieAdventure;
+	}
+
+	Path getVideoPath(int32 videoId) override {
+		return Path(String::format("disk1/Install/bin/data%02d.bin", videoId));
+	}
+
+	String getMusicPath(int32 trackId) override {
+		const Room *room = g_engine->player().currentRoom();
+		const int diskId = room != nullptr && room->mapIndex() == 1 ? 2 : 1;
+		return String::format("disk%d/track%02d", diskId, trackId);
+	}
+
+	void onLoadedGameFiles() override {
+		g_engine->script().variable("EsJuegoCompleto") = 0;
+	}
+};
+
+class GameHalfMovieAdventure : public GameWithVersion1 {
+public:
+	Path getVideoPath(int32 videoId) override {
+		return Path(String::format("bin/data%02d.bin", videoId));
+	}
+
+	String getMusicPath(int32 trackId) override {
+		return String::format("track%02d", trackId);
+	}
+
+	// probably the original CDs have music, the Steam release has no music...
+	void missingSound(const Common::String &fileName) override {
+		if (fileName.contains("track")) {
+			if (!_warnedAboutMusic) {
+				_warnedAboutMusic = true;
+				warning("This release does not contain music or the music was not extracted.");
+			}
+		} else
+			GameWithVersion1::missingSound(fileName);
+	}
+
+private:
+	bool _warnedAboutMusic = false;
+};
+
+class GameVaqueros : public GameHalfMovieAdventure {
+public:
+	const char *const *getMapFiles() override {
+		return kMapFilesVaqueros;
+	}
+
+	void onLoadedGameFiles() override {
+		g_engine->script().variable("EsJuegoCompleto") = 1;
+	}
+};
+
+
+class GameTerror : public GameHalfMovieAdventure {
+public:
+	const char *const *getMapFiles() override {
+		return kMapFilesTerror;
+	}
+
+	void onLoadedGameFiles() override {
+		g_engine->script().variable("EsJuegoCompleto") = 2;
+	}
+};
+
 Game *Game::createForMovieAdventureOriginal() {
 	return new GameMovieAdventureOriginal();
 }
 
+Game *Game::createForVaqueros() {
+	return new GameVaqueros();
+}
+
+Game *Game::createForTerror() {
+	return new GameTerror();
+}
+
 }
diff --git a/engines/alcachofa/game.cpp b/engines/alcachofa/game.cpp
index 8e1e640b429..abd4e098082 100644
--- a/engines/alcachofa/game.cpp
+++ b/engines/alcachofa/game.cpp
@@ -214,14 +214,23 @@ Game *Game::create() {
 	const auto &desc = g_engine->gameDescription();
 	switch (desc.engineVersion) {
 	case EngineVersion::V1_0:
-		return createForMovieAdventureOriginal();
+		switch (*desc.desc.gameId) {
+		case 'm':
+			return createForMovieAdventureOriginal();
+		case 't':
+			return createForTerror();
+		case 'v':
+			return createForVaqueros();
+		}
+		break;
 	case EngineVersion::V3_0:
 	case EngineVersion::V3_1:
 		return createForMovieAdventureSpecial();
 	default:
-		error("Unsupported or invalid engine version: %d", (int)desc.engineVersion);
 		break;
 	}
+
+	error("Unsupported or invalid engine version: %d", (int)desc.engineVersion);
 }
 
 }
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index 95c5c79653d..4e913aa0c0b 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -122,6 +122,8 @@ public:
 	static Game *create();
 	static Game *createForMovieAdventureSpecial();
 	static Game *createForMovieAdventureOriginal();
+	static Game *createForTerror();
+	static Game *createForVaqueros();
 
 	const Message _message;
 };


Commit: 92530947cee9ef89d5ff6a7b6e8ed7515e2083a8
    https://github.com/scummvm/scummvm/commit/92530947cee9ef89d5ff6a7b6e8ed7515e2083a8
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:35+01:00

Commit Message:
ALCACHOFA: V1: Fix loading aventuradecine saves

Changed paths:
    engines/alcachofa/game.cpp


diff --git a/engines/alcachofa/game.cpp b/engines/alcachofa/game.cpp
index abd4e098082..72ca3c35e4c 100644
--- a/engines/alcachofa/game.cpp
+++ b/engines/alcachofa/game.cpp
@@ -215,7 +215,7 @@ Game *Game::create() {
 	switch (desc.engineVersion) {
 	case EngineVersion::V1_0:
 		switch (*desc.desc.gameId) {
-		case 'm':
+		case 'a':
 			return createForMovieAdventureOriginal();
 		case 't':
 			return createForTerror();


Commit: 080caa0613eeb0f494a2352eabdf0467d4e121c0
    https://github.com/scummvm/scummvm/commit/080caa0613eeb0f494a2352eabdf0467d4e121c0
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:35+01:00

Commit Message:
ALCACHOFA: Fix display of untranslated object names

Changed paths:
    engines/alcachofa/graphics.cpp
    engines/alcachofa/graphics.h
    engines/alcachofa/rooms.cpp


diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index d47b1d07cdd..cb751f5c932 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -944,6 +944,8 @@ TextDrawRequest::TextDrawRequest(Font &font, const char *originalText, Point pos
 	// allocate on drawQueue to prevent having destruct it
 	assert(originalText != nullptr);
 	auto textLen = strlen(originalText);
+	if (textLen == 0)
+		return;
 	char *text = (char *)g_engine->drawQueue().allocator().allocateRaw(textLen + 1, 1);
 	memcpy(text, originalText, textLen + 1);
 
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 55649d22100..21e7c4db04f 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -424,12 +424,12 @@ private:
 	using TextLine = Common::Span<const byte>; ///< byte to convert 128+ characters to image indices
 
 	Font &_font;
-	int _posY, _height, _width;
+	int _posY = 0, _height = 0, _width = 0;
 	Color _color;
 	Common::Span<TextLine> _lines;
 	Common::Span<int> _posX;
 	TextLine _allLines[kMaxLines];
-	int _allPosX[kMaxLines];
+	int _allPosX[kMaxLines] = { 0 };
 };
 
 enum class FadeType {
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index bb5956b18dc..1a4cd60244c 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -685,7 +685,11 @@ const char *World::getLocalizedName(const String &name) const {
 	const char *localizedName;
 	return _localizedNames.tryGetVal(name.c_str(), localizedName)
 		? localizedName
+#ifdef ALCACHOFA_DEBUG
 		: name.c_str();
+#else
+		: "";
+#endif
 }
 
 const char *World::getDialogLine(int32 dialogId) const {


Commit: 7020ad213a0798fc69bf2c1c0533b30c6b3e5670
    https://github.com/scummvm/scummvm/commit/7020ad213a0798fc69bf2c1c0533b30c6b3e5670
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:35+01:00

Commit Message:
ALCACHOFA: V1: Fix character icon during input waiting

Changed paths:
    engines/alcachofa/input.cpp


diff --git a/engines/alcachofa/input.cpp b/engines/alcachofa/input.cpp
index a883907aca1..6ac957052ef 100644
--- a/engines/alcachofa/input.cpp
+++ b/engines/alcachofa/input.cpp
@@ -113,11 +113,11 @@ struct WaitForInputTask final : public Task {
 
 	TaskReturn run() override {
 		TASK_BEGIN;
-		process().unlockInteraction();
+
+		// originally this would unlock interaction
 		do {
 			TASK_YIELD(1);
 		} while(!g_engine->input().wasAnyMousePressed());
-		process().lockInteraction();
 		TASK_END;
 	}
 


Commit: c7a29913bbac70dee66259a3f3cf0f6ecb7c222a
    https://github.com/scummvm/scummvm/commit/c7a29913bbac70dee66259a3f3cf0f6ecb7c222a
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:35+01:00

Commit Message:
ALCACHOFA: V1: Fix CamDisguiseTask not moving camera

Changed paths:
    engines/alcachofa/camera.cpp


diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 9037fe15f36..00f2530dfcd 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -704,9 +704,10 @@ struct CamDisguiseTask final : public Task {
 			newPosition.setY(_startPosition.getY() + t);
 		else if (t <= 150)
 			newPosition.setY(_startPosition.getY() - t + 100);
-		else if (t <= 200)
+		else if (t >= 200)
 			newPosition.setY(_startPosition.getY() + t - 200);
 		_camera.setPosition(newPosition);
+		_camera.setFollow(nullptr);
 
 		return TaskReturn::yield();
 	}
@@ -726,7 +727,7 @@ struct CamDisguiseTask final : public Task {
 
 private:
 	Camera &_camera;
-	int32 _durationMs;
+	int32 _durationMs = 0;
 	uint32 _startTime = 0;
 	Vector2d _startPosition;
 };


Commit: 8fe6f1d12f8bd63ddf9754ddf1c74597d83f2d64
    https://github.com/scummvm/scummvm/commit/8fe6f1d12f8bd63ddf9754ddf1c74597d83f2d64
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:35+01:00

Commit Message:
ALCACHOFA: V1: Fix load being blocked after getting card deck in CARROMATO

Changed paths:
    engines/alcachofa/player.cpp


diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 044ff6769dd..1f19b7e3070 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -116,7 +116,9 @@ void Player::drawCursor(bool forceDefaultCursor) {
 }
 
 void Player::changeRoom(const Common::String &targetRoomName, bool resetCamera, bool isTemporary) {
-	debugC(1, kDebugGameplay, "Change room to %s", targetRoomName.c_str());
+	debugC(1, kDebugGameplay, "Change room from %s%s to %s%s",
+		_currentRoom == nullptr ? "<none>" : _currentRoom->name().c_str(), _isInTemporaryRoom ? " (temp)" : "",
+		targetRoomName.c_str(), isTemporary ? " (temp)" : "");
 
 	// original would be to always free all resources from globalRoom, inventory, GlobalUI
 	// We don't do that, it is unnecessary, all resources would be loaded right after
@@ -126,6 +128,9 @@ void Player::changeRoom(const Common::String &targetRoomName, bool resetCamera,
 		_currentRoom = nullptr;
 		return; // exiting game entirely
 	}
+	auto nextRoom = g_engine->world().getRoomByName(targetRoomName.c_str());
+	if (nextRoom == nullptr) // no good way to recover, leaving-the-room actions might already prevent further progress
+		error("Invalid room name: %s", targetRoomName.c_str());
 
 	if (_currentRoom != nullptr) {
 		g_engine->scheduler().killProcessByName("ACTUALIZAR_" + _currentRoom->name());
@@ -145,12 +150,11 @@ void Player::changeRoom(const Common::String &targetRoomName, bool resetCamera,
 	// this fixes a bug with all original games where changing the room in the inventory (e.g. iFOTO in aventura de cine)
 	// would overwrite the actual game room thus returning from the inventory one would be stuck in the temporary room
 	// If we know that a transition is temporary we prevent that and only remember the real game room
+	if (isTemporary && _roomBeforeInventory == nextRoom)
+		isTemporary = false; // this only looked like a temporary room change, but is not
 	_isInTemporaryRoom = isTemporary;
 
-	_currentRoom = g_engine->world().getRoomByName(targetRoomName.c_str());
-	if (_currentRoom == nullptr) // no good way to recover, leaving-the-room actions might already prevent further progress
-		error("Invalid room name: %s", targetRoomName.c_str());
-
+	_currentRoom = nextRoom;
 	if (!_didLoadGlobalRooms) {
 		_didLoadGlobalRooms = true;
 		g_engine->world().inventory().loadResources();


Commit: 0c8ded950469647b23986437c6b14c28d066eecc
    https://github.com/scummvm/scummvm/commit/0c8ded950469647b23986437c6b14c28d066eecc
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:35+01:00

Commit Message:
ALCACHOFA: V1: Disable collision avoidance

Changed paths:
    engines/alcachofa/game-movie-adventure-original.cpp
    engines/alcachofa/game-movie-adventure-special.cpp
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/game.h


diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index ea38e413173..b021c6ca1bb 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -238,6 +238,10 @@ public:
 		return false;
 	}
 
+	bool shouldAvoidCollisions() override {
+		return false;
+	}
+
 	Character *unknownSayTextCharacter(const char *name, int32 dialogId) override {
 		// an original bug in room POBLADO_INDIO, a dialog line would be skipped
 		if (!scumm_stricmp(name, "JEFE_INDIO_HABLA_POSTE"))
diff --git a/engines/alcachofa/game-movie-adventure-special.cpp b/engines/alcachofa/game-movie-adventure-special.cpp
index 51cdea7e01f..d8e5e9604ee 100644
--- a/engines/alcachofa/game-movie-adventure-special.cpp
+++ b/engines/alcachofa/game-movie-adventure-special.cpp
@@ -291,6 +291,10 @@ public:
 		return true;
 	}
 
+	bool shouldAvoidCollisions() override {
+		return true;
+	}
+
 	void onLoadedGameFiles() override {
 		// this notifies the script whether we are a demo
 		if (g_engine->world().loadedMapCount() == 2)
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 207b618b147..40666a9b702 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -912,7 +912,9 @@ void MainCharacter::walkTo(
 		strcmp(_activateAction, "MIRAR") != 0 &&
 		otherCharacter->currentlyUsing() != dynamic_cast<ObjectBase *>(_activateObject);
 
-	if (otherCharacter->room() == room() && evadeTarget.sqrDist(otherTarget) <= avoidanceDistSqr) {
+	if (otherCharacter->room() == room() &&
+		evadeTarget.sqrDist(otherTarget) <= avoidanceDistSqr &&
+		g_engine->game().shouldAvoidCollisions()) {
 		if (!otherCharacter->isBusy()) {
 			if (activeFloor != nullptr && activeFloor->findEvadeTarget(evadeTarget, activeDepthScale, avoidanceDistSqr, evadeTarget))
 				otherCharacter->WalkingCharacter::walkTo(evadeTarget);
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index 4e913aa0c0b..3d5219b1393 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -75,6 +75,7 @@ public:
 	virtual bool isAllowedToOpenMenu() = 0; ///< only the game-specific condition
 	virtual bool shouldScriptLockInteraction() = 0;
 	virtual bool shouldChangeCharacterUseGameLock() = 0;
+	virtual bool shouldAvoidCollisions() = 0;
 
 	virtual bool doesRoomHaveBackground(const Room *room);
 	virtual void unknownRoomObject(const Common::String &type);


Commit: e784fb80b929f87ca0eb5cf8d4a3b0107d5a3448
    https://github.com/scummvm/scummvm/commit/e784fb80b929f87ca0eb5cf8d4a3b0107d5a3448
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:35+01:00

Commit Message:
ALCACHOFA: V1: Fix size of main character

Changed paths:
    engines/alcachofa/game-movie-adventure-original.cpp
    engines/alcachofa/game-movie-adventure-special.cpp
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/game.h


diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index b021c6ca1bb..8fdbee83a87 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -242,6 +242,10 @@ public:
 		return false;
 	}
 
+	Point getMainCharacterSize() override {
+		return { 40, 220 };
+	}
+
 	Character *unknownSayTextCharacter(const char *name, int32 dialogId) override {
 		// an original bug in room POBLADO_INDIO, a dialog line would be skipped
 		if (!scumm_stricmp(name, "JEFE_INDIO_HABLA_POSTE"))
diff --git a/engines/alcachofa/game-movie-adventure-special.cpp b/engines/alcachofa/game-movie-adventure-special.cpp
index d8e5e9604ee..f26e49f3435 100644
--- a/engines/alcachofa/game-movie-adventure-special.cpp
+++ b/engines/alcachofa/game-movie-adventure-special.cpp
@@ -295,6 +295,10 @@ public:
 		return true;
 	}
 
+	Point getMainCharacterSize() override {
+		return { 60, 310};
+	}
+
 	void onLoadedGameFiles() override {
 		// this notifies the script whether we are a demo
 		if (g_engine->world().loadedMapCount() == 2)
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 40666a9b702..ef0c4c85f5b 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -863,11 +863,10 @@ void MainCharacter::update() {
 		_currentlyUsingObject = nullptr;
 	WalkingCharacter::update();
 
-	const int16 halfWidth = (int16)(60 * _graphicNormal.depthScale());
-	const int16 height = (int16)(310 * _graphicNormal.depthScale());
+	const Point size = g_engine->game().getMainCharacterSize() * _graphicNormal.depthScale();
 	shape()->setAsRectangle(Rect(
-		_currentPos.x - halfWidth, _currentPos.y - height,
-		_currentPos.x + halfWidth, _currentPos.y));
+		_currentPos.x - size.x, _currentPos.y - size.y,
+		_currentPos.x + size.x, _currentPos.y));
 
 	// These are set as members as FloorColor might want to change them
 	_alphaPremultiplier = room()->characterAlphaPremultiplier();
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index 3d5219b1393..2dd76ce3153 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -76,6 +76,7 @@ public:
 	virtual bool shouldScriptLockInteraction() = 0;
 	virtual bool shouldChangeCharacterUseGameLock() = 0;
 	virtual bool shouldAvoidCollisions() = 0;
+	virtual Common::Point getMainCharacterSize() = 0;
 
 	virtual bool doesRoomHaveBackground(const Room *room);
 	virtual void unknownRoomObject(const Common::String &type);


Commit: 2dc75afa6d8f25d17384e0884df1b6edd8cc5e53
    https://github.com/scummvm/scummvm/commit/2dc75afa6d8f25d17384e0884df1b6edd8cc5e53
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:35+01:00

Commit Message:
ALCACHOFA: V1: Add option to enable texture filtering

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.h
    engines/alcachofa/detection.h
    engines/alcachofa/detection_tables.h
    engines/alcachofa/game-movie-adventure-original.cpp
    engines/alcachofa/game-movie-adventure-special.cpp
    engines/alcachofa/game.h
    engines/alcachofa/graphics-opengl-shaders.cpp
    engines/alcachofa/graphics-opengl.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/metaengine.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index cbc232ecd90..530b908c736 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -79,6 +79,8 @@ Common::Error AlcachofaEngine::run() {
 	g_system->showMouse(false);
 	setDebugger(_console);
 	_game.reset(Game::create());
+	Config::registerDefaults();
+	_config.loadFromScummVM();
 	_world.load();
 	_renderer.reset(IRenderer::createOpenGLRenderer(game().getResolution()));
 	_drawQueue.reset(new DrawQueue(_renderer.get()));
@@ -441,8 +443,15 @@ bool AlcachofaEngine::tryLoadFromLauncher() {
 	return result;
 }
 
-Config::Config() {
-	loadFromScummVM();
+void Config::registerDefaults() {
+	Config c;
+	ConfMan.registerDefault("subtitles", c._subtitles);
+	ConfMan.registerDefault("high_quality", c._highQuality);
+	ConfMan.registerDefault("32_bits", c._bits32);
+	ConfMan.registerDefault("tex_filter", g_engine->game().shouldFilterTexturesByDefault());
+	ConfMan.registerDefault("music_volume", c._musicVolume);
+	ConfMan.registerDefault("speech_volume", c._speechVolume);
+	ConfMan.registerDefault("sfx_volume", c._speechVolume);
 }
 
 void Config::loadFromScummVM() {
@@ -451,12 +460,14 @@ void Config::loadFromScummVM() {
 	_subtitles = ConfMan.getBool("subtitles");
 	_highQuality = ConfMan.getBool("high_quality");
 	_bits32 = ConfMan.getBool("32_bits");
+	_texFilter = ConfMan.getBool("tex_filter");
 }
 
 void Config::saveToScummVM() {
 	ConfMan.setBool("subtitles", _subtitles);
 	ConfMan.setBool("high_quality", _highQuality);
 	ConfMan.setBool("32_bits", _bits32);
+	ConfMan.setBool("tex_filter", _texFilter);
 	ConfMan.setInt("music_volume", _musicVolume);
 	ConfMan.setInt("speech_volume", _speechVolume);
 	ConfMan.setInt("sfx_volume", _speechVolume);
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 406cd757732..45b2ca5b27f 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -62,7 +62,7 @@ namespace SaveVersion {
 	static constexpr const Common::Serializer::Version kInitial = 0;
 	static constexpr const Common::Serializer::Version kWithEngineV10 = 1;
 };
-static constexpr const Common::Serializer::Version kCurrentSaveVersion = SaveVersion::kInitial;
+static constexpr const Common::Serializer::Version kCurrentSaveVersion = SaveVersion::kWithEngineV10;
 
 class MySerializer : public Common::Serializer {
 public:
@@ -81,14 +81,14 @@ public:
 
 class Config {
 public:
-	Config();
-
 	inline bool &subtitles() { return _subtitles; }
 	inline bool &highQuality() { return _highQuality; }
 	inline bool &bits32() { return _bits32; }
+	inline bool &texFilter() { return _texFilter; }
 	inline uint8 &musicVolume() { return _musicVolume; }
 	inline uint8 &speechVolume() { return _speechVolume; }
 
+	static void registerDefaults();
 	void loadFromScummVM();
 	void saveToScummVM();
 
@@ -96,7 +96,8 @@ private:
 	bool
 		_subtitles = true,
 		_highQuality = true,
-		_bits32 = true;
+		_bits32 = true,
+		_texFilter = true;
 	uint8
 		_musicVolume = 255,
 		_speechVolume = 255;
diff --git a/engines/alcachofa/detection.h b/engines/alcachofa/detection.h
index 0c8ce953738..f505a2e55b8 100644
--- a/engines/alcachofa/detection.h
+++ b/engines/alcachofa/detection.h
@@ -58,8 +58,9 @@ extern const PlainGameDescriptor alcachofaGames[];
 
 extern const AlcachofaGameDescription gameDescriptions[];
 
-#define GAMEOPTION_HIGH_QUALITY GUIO_GAMEOPTIONS1 // I should comment what this does, but I don't know
-#define GAMEOPTION_32BITS GUIO_GAMEOPTIONS2
+#define GAMEOPTION_HIGH_QUALITY GUIO_GAMEOPTIONS1 // toggles some special effect objects
+#define GAMEOPTION_32BITS GUIO_GAMEOPTIONS2 // renders in 16/32 bit color 
+#define GAMEOPTION_TEXTURE_FILTER GUIO_GAMEOPTIONS3 // toggles texture filters
 
 } // End of namespace Alcachofa
 
diff --git a/engines/alcachofa/detection_tables.h b/engines/alcachofa/detection_tables.h
index 9a0bfe6aafd..72c3d779612 100644
--- a/engines/alcachofa/detection_tables.h
+++ b/engines/alcachofa/detection_tables.h
@@ -143,7 +143,8 @@ const AlcachofaGameDescription gameDescriptions[] = {
 			),
 			Common::ES_ESP,
 			Common::kPlatformWindows,
-			ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE | ADGF_CD
+			ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE | ADGF_CD,
+			GUIO1(GAMEOPTION_TEXTURE_FILTER)
 		},
 		EngineVersion::V1_0
 	},
@@ -159,7 +160,8 @@ const AlcachofaGameDescription gameDescriptions[] = {
 			AD_ENTRY1s("terror.emc", "dc9357ee618bff160e2e2afa168ba913", 170113868),
 			Common::ES_ESP,
 			Common::kPlatformWindows,
-			ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE | ADGF_CD
+			ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE | ADGF_CD,
+			GUIO1(GAMEOPTION_TEXTURE_FILTER)
 		},
 		EngineVersion::V1_0
 	},
@@ -175,7 +177,8 @@ const AlcachofaGameDescription gameDescriptions[] = {
 			AD_ENTRY1s("oeste.emc", "b4c1084557d4cfbae336f0e741ec9e9f", 183099320),
 			Common::ES_ESP,
 			Common::kPlatformWindows,
-			ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE | ADGF_CD
+			ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE | ADGF_CD,
+			GUIO1(GAMEOPTION_TEXTURE_FILTER)
 		},
 		EngineVersion::V1_0
 	},
diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index 8fdbee83a87..d6904937a45 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -217,6 +217,10 @@ public:
 		return kind == MainCharacterKind::Mortadelo ? 15 : 16;
 	}
 
+	bool shouldFilterTexturesByDefault() override {
+		return false;
+	}
+
 	bool shouldClipCamera() override {
 		return true;
 	}
diff --git a/engines/alcachofa/game-movie-adventure-special.cpp b/engines/alcachofa/game-movie-adventure-special.cpp
index f26e49f3435..e2f03b387f1 100644
--- a/engines/alcachofa/game-movie-adventure-special.cpp
+++ b/engines/alcachofa/game-movie-adventure-special.cpp
@@ -270,6 +270,10 @@ public:
 			kind == MainCharacterKind::Mortadelo ? "PistaMorta" : "PistaFile");
 	}
 
+	bool shouldFilterTexturesByDefault() override {
+		return true;
+	}
+
 	bool shouldClipCamera() override {
 		return g_engine->script().variable("EncuadrarCamara");
 	}
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index 2dd76ce3153..681d2bba447 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -70,6 +70,7 @@ public:
 	virtual Common::String getSoundPath(const char *filename) = 0; ///< Without file-extension
 	virtual Common::String getMusicPath(int32 trackId) = 0; ///< Without file-extension
 	virtual int32 getCharacterJingle(MainCharacterKind kind) = 0;
+	virtual bool shouldFilterTexturesByDefault() = 0;
 	virtual bool shouldClipCamera() = 0;
 	virtual bool isAllowedToInteract() = 0;
 	virtual bool isAllowedToOpenMenu() = 0; ///< only the game-specific condition
diff --git a/engines/alcachofa/graphics-opengl-shaders.cpp b/engines/alcachofa/graphics-opengl-shaders.cpp
index 6e348332b43..1cb30356b29 100644
--- a/engines/alcachofa/graphics-opengl-shaders.cpp
+++ b/engines/alcachofa/graphics-opengl-shaders.cpp
@@ -189,7 +189,7 @@ private:
 		setBlendFunc(_currentBlendMode);
 		_shader.setUniform("projection", _projection);
 		_shader.setUniform("blendMode", _currentTexture == nullptr ? 5 : (int)_currentBlendMode);
-		_shader.setUniform("posterize", g_engine->config().highQuality() ? 1 : 0);
+		_shader.setUniform("posterize", g_engine->config().bits32() ? 0 : 1);
 		_shader.setUniform1f("lodBias", _currentLodBias);
 		_shader.setUniform("texture", 0);
 		_batchTexture = _currentTexture;
@@ -248,7 +248,7 @@ private:
 				gl_FragColor = var_color;
 			}
 
-			if (posterize == 0) {
+			if (posterize == 1) {
 				// shave off 3 bits for that 16-bit look
 				gl_FragColor = floor(gl_FragColor * (256.0 / 8.0)) / (256.0 / 8.0);
 			}
diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index 044bacbc651..5dd55492dc4 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -48,8 +48,10 @@ OpenGLTexture::OpenGLTexture(int32 w, int32 h, bool withMipmaps)
 	OpenGL::clearGLError(); // we will just ignore it
 	GL_CALL(glGenTextures(1, &_handle));
 	GL_CALL(glBindTexture(GL_TEXTURE_2D, _handle));
-	GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR));
-	GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
+
+	const bool filter = g_engine->config().texFilter();
+	GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter ? GL_LINEAR_MIPMAP_LINEAR : GL_NEAREST));
+	GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter ? GL_LINEAR : GL_NEAREST));
 	setMirrorWrap(false);
 }
 
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index cb751f5c932..ad3add55d5d 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -447,10 +447,10 @@ void Animation::load() {
 		texHeight = nextHigher2(maxBounds.height());
 	}
 	_renderedSurface.create(texWidth, texHeight, g_engine->renderer().getPixelFormat());
-	_renderedTexture = g_engine->renderer().createTexture(texWidth, texHeight, true);
+	_renderedTexture = g_engine->renderer().createTexture(texWidth, texHeight, g_engine->config().texFilter());
 
 	// We always create mipmaps, even for the backgrounds that usually do not scale much,
-	// the exception to this is the thumbnails for the savestates.
+	// the exception to this is the thumbnails for the savestates. (and disabling filter also disables mipmaps)
 	// If we need to reduce graphics memory usage in the future, we can change it right here
 }
 
diff --git a/engines/alcachofa/metaengine.cpp b/engines/alcachofa/metaengine.cpp
index c53bf7b5168..ee44b373758 100644
--- a/engines/alcachofa/metaengine.cpp
+++ b/engines/alcachofa/metaengine.cpp
@@ -56,6 +56,17 @@ static const ADExtraGuiOptionsMap optionsList[] = {
 			0
 		}
 	},
+	{
+		GAMEOPTION_TEXTURE_FILTER,
+		{
+			_s("Filter textures"),
+			_s("Whether textures should be linearly filtered"),
+			"tex_filter",
+			false, // it is used for V1 where textures are normally not filtered, V3 originally always filters
+			0,
+			0
+		}
+	},
 	AD_EXTRA_GUI_OPTIONS_TERMINATOR
 };
 


Commit: 25a0e0e1936235ebbe8098330d2bad73b2ff1500
    https://github.com/scummvm/scummvm/commit/25a0e0e1936235ebbe8098330d2bad73b2ff1500
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:35+01:00

Commit Message:
ALCACHOFA: V1: Fix sound issues in Intro_Terror

Changed paths:
    engines/alcachofa/console.cpp
    engines/alcachofa/console.h
    engines/alcachofa/rooms.cpp


diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
index 9206f1c19d4..33b463f6a06 100644
--- a/engines/alcachofa/console.cpp
+++ b/engines/alcachofa/console.cpp
@@ -53,6 +53,9 @@ Console::Console() : GUI::Debugger() {
 	registerCmd("playVideo", WRAP_METHOD(Console, cmdPlayVideo));
 	registerCmd("script", WRAP_METHOD(Console, cmdScript));
 	registerCmd("toggleObject", WRAP_METHOD(Console, cmdToggleObject));
+	registerCmd("playVoice", WRAP_METHOD(Console, cmdPlaySound));
+	registerCmd("playSFX", WRAP_METHOD(Console, cmdPlaySound));
+	registerCmd("dumpFile", WRAP_METHOD(Console, cmdDumpFile));
 }
 
 Console::~Console() {}
@@ -333,4 +336,46 @@ bool Console::cmdToggleObject(int argc, const char **args) {
 	return true;
 }
 
+bool Console::cmdPlaySound(int argc, const char **args) {
+	if (argc != 2) {
+		debugPrintf("usage: %s <sound-name>\n", args[0]);
+		return true;
+	}
+
+	if (tolower(args[0][5]) == 'V')
+		g_engine->sounds().playVoice(args[1]);
+	else
+		g_engine->sounds().playSFX(args[1]);
+	return false;
+}
+
+bool Console::cmdDumpFile(int argc, const char **args) {
+	if (argc != 2 && argc != 3) {
+		debugPrintf("usage: %s <input-path> [<output-path>]\n", args[0]);
+		return true;
+	}
+
+	File input;
+	if (!input.open(args[1])) {
+		debugPrintf("Could not find input file: %s\n", args[1]);
+		return true;
+	}
+
+	const char *outputPath = argc == 2 ? args[1] : args[2];
+	DumpFile output;
+	if (!output.open(outputPath)) {
+		debugPrintf("Could not open output file: %s\n", outputPath);
+		return true;
+	}
+
+	constexpr const uint kBufferSize = 1024;
+	uint32 read;
+	byte buffer[kBufferSize];
+	do {
+		read = input.read(buffer, kBufferSize);
+		output.write(buffer, read);
+	} while (read == kBufferSize);
+	return true;
+}
+
 } // End of namespace Alcachofa
diff --git a/engines/alcachofa/console.h b/engines/alcachofa/console.h
index b288a8bfb68..d5e08a961fa 100644
--- a/engines/alcachofa/console.h
+++ b/engines/alcachofa/console.h
@@ -63,6 +63,8 @@ private:
 	bool cmdPlayVideo(int argc, const char **args);
 	bool cmdScript(int argc, const char **args);
 	bool cmdToggleObject(int argc, const char **args);
+	bool cmdPlaySound(int argc, const char **args);
+	bool cmdDumpFile(int argc, const char **args);
 
 	bool _showGraphics = false;
 	bool _showInteractables = false;
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 1a4cd60244c..faa6538ac5b 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -1067,7 +1067,7 @@ public:
 			return nullptr;
 		if (!_file->seek(member->_offset, SEEK_SET))
 			error("Could not seek to embedded file: %s at %u", path.toString().c_str(), member->_offset);
-		return new SeekableSubReadStream(_file.get(), member->_offset, member->_end, DisposeAfterUse::NO);
+		return new SafeSeekableSubReadStream(_file.get(), member->_offset, member->_end, DisposeAfterUse::NO);
 	}
 
 private:


Commit: 7faac192b0d78c44ba805ed9e1ac57a359d7d815
    https://github.com/scummvm/scummvm/commit/7faac192b0d78c44ba805ed9e1ac57a359d7d815
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-04T16:55:35+01:00

Commit Message:
ALCACHOFA: Fix compilation on ScopedPtr conversion

Changed paths:
    engines/alcachofa/rooms.cpp


diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index faa6538ac5b..adb2ce6b8c1 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -999,7 +999,7 @@ ScopedPtr<SeekableReadStream> World::openFileRef(const GameFileReference &ref) c
 		ScopedPtr<File> file(new File());
 		if (!file->open(Path(ref._path)))
 			return nullptr;
-		return file;
+		return ScopedPtr<SeekableReadStream>(file.release()); // Ubuntu 22 does allow the implicit conversion
 	}
 }
 




More information about the Scummvm-git-logs mailing list