[Scummvm-git-logs] scummvm master -> 639847ad8ca48f5797ef4d178673f61857771d7a

Helco noreply at scummvm.org
Tue Mar 31 15:23:43 UTC 2026


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

Summary:
b855388571 ALCACHOFA: V2: Fix secta startup
6db3b5b2e4 ALCACHOFA: V2: Fix gameplay bugs
aa54aa519f ALCACHOFA: V2: Patch script errors with nested dialog menus
470ba13bb4 ALCACHOFA: Fix two coverity issues
b7b1ba88c7 ALCACHOFA: Fix bracket style with if-else
1829417aaf ALCACHOFA: Fix kernel proc names in debug traces
0b749d7819 ALCACHOFA: V2: Adapt camera to V2
fbf769a5ab ALCACHOFA: V2: Fix subtitle position
c656618583 ALCACHOFA: V2: Fix camera speed in large rooms
4e51eaf599 ALCACHOFA: V2: Fix long-running animations
8eeecc3708 ALCACHOFA: V2: Add moscu and escarabajo detection entries
dea78dd95f ALCACHOFA: Fix signed/unsigned comparison warning
973d5f0127 ALCACHOFA: V2: Add corvino, balones and mamelucos detection entries
471b404af5 ALCACHOFA: V2: Fix startup of escarabajo and moscu
ea37a7d5a8 ALCACHOFA: V2: Fix script error on entering DESPACHO_MOMIEZ twice
ae3cba18ed ALCACHOFA: Reversing draw order within one layer
657c8c1292 ALCACHOFA: Reenable DefectoObjeto script
3d113845f1 ALCACHOFA: V2: Disable texture filtering by default
d9e3e66dc2 ALCACHOFA: Fix script loading with duplicated procedures
73a82da4d1 ALCACHOFA: Add walk fallback if character is outside the floor shape
3fce6a3eaa ALCACHOFA: Allow characters as camera targets
c21da18d93 ALCACHOFA: Remove ScriptKernelTask::ChangeDoor
a5bd081174 ALCACHOFA: V2: Fix loading saves after teleporting
639847ad8c ALCACHOFA: V2: Fix pickup script from non-char process


Commit: b85538857143fd09146b06d21f7d23dcb62ea019
    https://github.com/scummvm/scummvm/commit/b85538857143fd09146b06d21f7d23dcb62ea019
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:28+02:00

Commit Message:
ALCACHOFA: V2: Fix secta startup

- Allow mp3 sounds
- Stub CamShakeV2 kernel task
- Use V3 MainCharacterKind values
- Fix V2 font rendering
- Warn about unextracted videos
- Use player semaphore as interaction lock

Changed paths:
    engines/alcachofa/game-v2.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/script.cpp
    engines/alcachofa/script.h
    engines/alcachofa/sounds.cpp


diff --git a/engines/alcachofa/game-v2.cpp b/engines/alcachofa/game-v2.cpp
index ed1966a348f..3d86c847c60 100644
--- a/engines/alcachofa/game-v2.cpp
+++ b/engines/alcachofa/game-v2.cpp
@@ -19,6 +19,8 @@
  *
  */
 
+#include "gui/message.h"
+
 #include "alcachofa/alcachofa.h"
 #include "alcachofa/game.h"
 #include "alcachofa/script.h"
@@ -92,7 +94,7 @@ static constexpr const ScriptKernelTask kScriptKernelTaskMap[] = {
 	ScriptKernelTask::Drop,
 	ScriptKernelTask::CharacterDrop,
 	ScriptKernelTask::ChangeDoor,
-	ScriptKernelTask::CamShake,
+	ScriptKernelTask::CamShakeV2,
 	ScriptKernelTask::ToggleRoomFloor,
 	ScriptKernelTask::SetDialogLineReturn,
 	ScriptKernelTask::DialogMenu,
@@ -181,7 +183,7 @@ public:
 	}
 
 	bool isAllowedToInteract() override {
-		return true; // original would be checking an unused script variable "Ocupados"
+		return g_engine->player().semaphore().isReleased();
 	}
 
 	bool shouldScriptLockInteraction() override {
@@ -214,6 +216,20 @@ static constexpr const char *kMapFilesSecta[] = {
 
 class GameSecta : public GameWithVersion2 {
 public:
+	GameSecta() {
+		// only the Steam Release has only the Videos in an ISO...
+		if (!SearchMan.hasFile(getVideoPath(0)) && SearchMan.hasFile("VIDS.iso")) {
+			_videosAreExtracted = false;
+			GUI::MessageDialog dialog("Please extract VIDS.iso in order to play videos.");
+			dialog.runModal();
+		}
+	}
+
+	bool isKnownBadVideo(int32 videoId) override {
+		// all videos are known bad if they are not extracted
+		return !_videosAreExtracted;
+	}
+
 	const char *const *getMapFiles() override {
 		return kMapFilesSecta;
 	}
@@ -221,6 +237,9 @@ public:
 	char getTextFileKey() override {
 		return static_cast<char>(0xA3);
 	}
+
+private:
+	bool _videosAreExtracted = true;
 };
 
 Game *Game::createForSecta() {
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index b476bbbe055..93322ee5aba 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -691,7 +691,7 @@ Font::Font(GameFileReference fileRef)
 	, _spaceImageI(g_engine->isV1() ? 94 : 0)
 	, _charSpacing(g_engine->isV1() ? 3 : 0) {}
 
-static void fixFontAtlasColors(ManagedSurface &surface) {
+static void fixFontAtlasColorsV1(ManagedSurface &surface) {
 	// In V1 the font contains green and black pixels where
 	//  - black pixels should stay black
 	//  - green pixels should be the text color
@@ -713,6 +713,20 @@ static void fixFontAtlasColors(ManagedSurface &surface) {
 	}
 }
 
+static void fixFontAtlasColorsV2(ManagedSurface &surface) {
+	// In V2 the font contains grayscale pixels and magenta as color key
+	// We just remove the color key to transparent
+	assert(surface.format.bytesPerPixel == 4);
+	const uint32 magenta = surface.format.ARGBToColor(255, 255, 0, 255);
+
+	for (int16 y = 0; y < surface.h; y++) {
+		uint32 *pixel = (uint32 *)surface.getBasePtr(0, y);
+		for (int16 x = 0; x < surface.w; x++, pixel++) {
+			*pixel = *pixel == magenta ? 0 : *pixel;
+		}
+	}
+}
+
 void Font::load() {
 	if (_isLoaded)
 		return;
@@ -752,7 +766,9 @@ void Font::load() {
 		_texMaxs[i].setY((offsetY + _images[i]->h) * invHeight);
 	}
 	if (g_engine->isV1())
-		fixFontAtlasColors(atlasSurface);
+		fixFontAtlasColorsV1(atlasSurface);
+	else if (g_engine->isV2())
+		fixFontAtlasColorsV2(atlasSurface);
 
 	_texture = g_engine->renderer().createTexture(atlasSurface.w, atlasSurface.h, false);
 	_texture->update(atlasSurface);
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 5063163d45f..1e8a800c64b 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -574,19 +574,19 @@ private:
 
 	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 (g_engine->isV1()) {
 			if (value < 0 || value > 1)
 				error("Unexpected value for main character kind: %d", value);
 			return value == 0
 				? MainCharacterKind::Mortadelo
 				: MainCharacterKind::Filemon;
 		}
+		else {
+			if (value < 0 || value > 2)
+				error("Unexpected value for main character kind: %d", value);
+			else
+				return (MainCharacterKind)value;
+		}
 	}
 
 	const char *getStringArg(uint argI) {
@@ -945,6 +945,9 @@ private:
 				Vector2d(getNumberArg(1), getNumberArg(2)),
 				Vector2d(getNumberArg(3), getNumberArg(4)),
 				getNumberArg(0)));
+		case ScriptKernelTask::CamShakeV2:
+			warning("STUB: CamShakeV2");
+			return TaskReturn::finish(0);
 		case ScriptKernelTask::LerpCamXY:
 			return TaskReturn::waitFor(g_engine->cameraV3().lerpPos(process(),
 				Vector2d(getNumberArg(0), getNumberArg(1)),
diff --git a/engines/alcachofa/script.h b/engines/alcachofa/script.h
index 5da8a929170..5afd1db2914 100644
--- a/engines/alcachofa/script.h
+++ b/engines/alcachofa/script.h
@@ -114,6 +114,7 @@ enum class ScriptKernelTask {
 	WaitCamStopping,
 	CamFollow,
 	CamShake,
+	CamShakeV2,
 	LerpCamXY,
 	LerpCamZ,
 	LerpCamScale,
diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index 71559cc562e..d6789ab1289 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -30,6 +30,7 @@
 #include "audio/decoders/wave.h"
 #include "audio/decoders/adpcm.h"
 #include "audio/decoders/raw.h"
+#include "audio/decoders/mp3.h"
 
 using namespace Common;
 using namespace Audio;
@@ -81,8 +82,47 @@ void Sounds::update() {
 	}
 }
 
+class XORReadStream final : public SeekableReadStream {
+public:
+	XORReadStream(SeekableReadStream *parent, byte key, DisposeAfterUse::Flag disposeAfterUse)
+		: _parent(parent, disposeAfterUse)
+		, _key(key) {}
+
+	uint32 read(void *dataPtr, uint32 maxSize) override {
+		uint32 size = _parent->read(dataPtr, maxSize);
+		byte *bytePtr = (byte *)dataPtr;
+		for (uint32 i = 0; i < size; i++)
+			*(bytePtr++) ^= _key;
+		return size;
+	}
+
+	bool eos() const override {
+		return _parent->eos();
+	}
+
+	int64 pos() const override {
+		return _parent->pos();
+	}
+
+	int64 size() const override {
+		return _parent->size();
+	}
+
+	bool seek(int64 offset, int whence) override {
+		return _parent->seek(offset, whence);
+	}
+
+private:
+	DisposablePtr<SeekableReadStream> _parent;
+	const byte _key;
+};
+
 static AudioStream *loadSND(File *file) {
-	// SND files are just WAV files with removed headers
+	// in V2 SND files are raw U8 PCM in mono 22100 encrypted with XOR
+	if (g_engine->isV2())
+		return makeRawStream(new XORReadStream(file, 0x55, DisposeAfterUse::YES), 22100, FLAG_UNSIGNED);
+
+	// in V1/V3 SND files are just WAV files with removed headers
 	const uint32 endOfFormat = file->readUint32LE() + 2 * sizeof(uint32);
 	if (endOfFormat < 24)
 		error("Invalid SND format size");
@@ -129,6 +169,18 @@ static AudioStream *openAudio(const String &basePath) {
 	if (file->open(path.c_str()))
 		return makeWAVStream(file, DisposeAfterUse::YES);
 
+	// Steam releases of V2 games use mp3 files
+	path.setChar('M', path.size() - 3);
+	path.setChar('P', path.size() - 2);
+	path.setChar('3', path.size() - 1);
+	if (file->open(path.c_str())) {
+#ifdef USE_MAD
+		return makeMP3Stream(file, DisposeAfterUse::YES);
+#else
+		return nullptr;
+#endif
+	}
+
 	delete file;
 	g_engine->game().missingSound(basePath);
 	return nullptr;


Commit: 6db3b5b2e443691bd7859f769c3e68e6ccf5da82
    https://github.com/scummvm/scummvm/commit/6db3b5b2e443691bd7859f769c3e68e6ccf5da82
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:29+02:00

Commit Message:
ALCACHOFA: V2: Fix gameplay bugs

- Temporary camera bounds fix
- GlobalUI is not visible
- Item lookup is case-sensitive
- Unknown voice line for MORTA_ATADO
- Map teleports do not work
- Drop item from none-character-process

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/game-v2.cpp
    engines/alcachofa/rooms.cpp
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index c36a0da111c..d4098630113 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -84,10 +84,10 @@ Common::Error AlcachofaEngine::run() {
 	_world.load();
 	_renderer.reset(IRenderer::createOpenGLRenderer(game().getResolution()));
 	_drawQueue.reset(new DrawQueue(_renderer.get()));
-	_camera.reset(isV1() ? static_cast<Camera *>(new CameraV1()) : new CameraV3());
+	_camera.reset(isV1() || isV2() ? static_cast<Camera *>(new CameraV1()) : new CameraV3());
 	_script.reset(new Script());
 	_player.reset(new Player());
-	_globalUI.reset(isV1() ? static_cast<GlobalUI *>(new GlobalUIV1()) : new GlobalUIV3());
+	_globalUI.reset(isV1() || isV2() ? static_cast<GlobalUI *>(new GlobalUIV1()) : new GlobalUIV3());
 	_menu.reset(isV1() ? static_cast<Menu *>(new MenuV1()) : new MenuV3());
 	setMillis(0);
 	game().onLoadedGameFiles();
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 93296c4a163..e4152f28d15 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -996,7 +996,7 @@ void MainCharacter::clearInventory() {
 
 Item *MainCharacter::getItemByName(const String &name) const {
 	for (auto *item : _items) {
-		if (item->name() == name)
+		if (item->name().equalsIgnoreCase(name))
 			return item;
 	}
 	return nullptr;
diff --git a/engines/alcachofa/game-v2.cpp b/engines/alcachofa/game-v2.cpp
index 3d86c847c60..9f9d64ea5f8 100644
--- a/engines/alcachofa/game-v2.cpp
+++ b/engines/alcachofa/game-v2.cpp
@@ -170,6 +170,11 @@ public:
 			kind == MainCharacterKind::Mortadelo ? "PistaMorta" : "PistaFile");
 	}
 
+	bool hasMortadeloVoice(const Character *character) override {
+		return Game::hasMortadeloVoice(character) ||
+			character->name().equalsIgnoreCase("MORTA_ATADO");
+	}
+
 	bool shouldFilterTexturesByDefault() override {
 		return true; // TODO: Check this!
 	}
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index fa5b6d42289..6888d73f3e3 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -780,8 +780,8 @@ bool World::loadWorldFileV2(const char *path) {
 	};
 	readGlobalAnim(GlobalAnimationKind::GeneralFont, GlobalAnimationKind::DialogFont);
 	readGlobalAnim(GlobalAnimationKind::Cursor, GlobalAnimationKind::Count);
-	readGlobalAnim(GlobalAnimationKind::MortadeloIcon, GlobalAnimationKind::MortadeloDisabledIcon);
 	readGlobalAnim(GlobalAnimationKind::FilemonIcon, GlobalAnimationKind::FilemonDisabledIcon);
+	readGlobalAnim(GlobalAnimationKind::MortadeloIcon, GlobalAnimationKind::MortadeloDisabledIcon);
 	readGlobalAnim(GlobalAnimationKind::InventoryIcon, GlobalAnimationKind::InventoryDisabledIcon);
 
 	readRooms(file);
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 1e8a800c64b..0eed880bac7 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -833,6 +833,12 @@ private:
 			}
 			character->resetTalking();
 			character->room() = targetRoom;
+			if (g_engine->isV2() && character == g_engine->player().activeCharacter()) {
+				// this mechanic also exists in V1 but does not seem to be used
+				// as the script also changes the room to the target when placing characters
+				g_engine->player().changeRoom(getStringArg(1), true);
+				g_engine->sounds().setMusicToRoom(targetRoom->musicID());
+			}
 			return TaskReturn::finish(1);
 		}
 		case ScriptKernelTask::LerpCharacterLodBias: {
@@ -908,6 +914,11 @@ private:
 			return TaskReturn::finish(1);
 		}
 		case ScriptKernelTask::Drop:
+			if (process().character() == MainCharacterKind::None) {
+				// This happens in Secta, the original game just ignores this case
+				warning("Tried to drop from none-character-process: %s at %u", getStringArg(0), _pc);
+			}
+			else
 			relatedCharacter().drop(getStringArg(0));
 			return TaskReturn::finish(1);
 		case ScriptKernelTask::CharacterDrop: {


Commit: aa54aa519f6266940ff39812d4e63d379841f19c
    https://github.com/scummvm/scummvm/commit/aa54aa519f6266940ff39812d4e63d379841f19c
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:29+02:00

Commit Message:
ALCACHOFA: V2: Patch script errors with nested dialog menus

Changed paths:
    engines/alcachofa/game-v2.cpp
    engines/alcachofa/game-v3.cpp
    engines/alcachofa/script.cpp
    engines/alcachofa/script.h


diff --git a/engines/alcachofa/game-v2.cpp b/engines/alcachofa/game-v2.cpp
index 9f9d64ea5f8..ee4be6202f5 100644
--- a/engines/alcachofa/game-v2.cpp
+++ b/engines/alcachofa/game-v2.cpp
@@ -212,6 +212,17 @@ public:
 	}
 };
 
+class GameWithVersion2_0 : public GameWithVersion2 {
+public:
+	void onLoadedGameFiles() override {
+		GameWithVersion2::onLoadedGameFiles();
+
+		auto &script = g_engine->script();
+		script.fixNestedMenuPop(5921); // Mortadelo talking to ARQUEOLOGOS in CARRETERA
+		script.fixNestedMenuPop(20898); // Filemon talking to MANOLO in FILE_PIRAMIDE
+	}
+};
+
 static constexpr const char *kMapFilesSecta[] = {
 	"Mapas/mapa1.emc",
 	"Mapas/mapa2.emc",
@@ -219,7 +230,7 @@ static constexpr const char *kMapFilesSecta[] = {
 	nullptr
 };
 
-class GameSecta : public GameWithVersion2 {
+class GameSecta : public GameWithVersion2_0 {
 public:
 	GameSecta() {
 		// only the Steam Release has only the Videos in an ISO...
diff --git a/engines/alcachofa/game-v3.cpp b/engines/alcachofa/game-v3.cpp
index 1ede881c22f..65b6bd740e8 100644
--- a/engines/alcachofa/game-v3.cpp
+++ b/engines/alcachofa/game-v3.cpp
@@ -392,6 +392,12 @@ public:
 		return { kScriptKernelTaskMapV30, ARRAYSIZE(kScriptKernelTaskMapV30) };
 	}
 
+	void onLoadedGameFiles() override {
+		GameWithVersion3::onLoadedGameFiles();
+		g_engine->script().fixNestedMenuPop(39038); // Mortadelo talking to MERACDER in BAZAAR
+		g_engine->script().fixNestedMenuPop(39130); // also ^
+	}
+
 	void updateScriptVariables() override {
 		GameWithVersion3::updateScriptVariables();
 
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 0eed880bac7..0393498baff 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -270,29 +270,35 @@ struct ScriptTask final : public Task {
 				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, opName, instruction._arg);
+				debugN("%u: %5u %-12s %8d Stack(%u): ",
+					process().pid(), _pc - 1, opName, instruction._arg, _valueStack.size());
 				if (_valueStack.empty())
 					debug("empty");
 				else {
-					const auto &top = _valueStack.top();
-					switch (top._type) {
-					case StackEntryType::Number:
-						debug("Number %d", top._number);
-						break;
-					case StackEntryType::Variable:
-						debug("Var %u (%d)", top._index, _script._variables[top._index]);
-						break;
-					case StackEntryType::Instruction:
-						debug("Instr %u", top._index);
-						break;
-					case StackEntryType::String:
-						debug("String %u (\"%s\")", top._index, getStringArg(0));
-						break;
-					default:
-						debug("INVALID");
-						break;
+					uint count = MIN(uint(3), _valueStack.size());
+					for (uint i = 0; i < count; i++) {
+						if (i != 0)
+							debugN(", ");
+						const auto &top = _valueStack[_valueStack.size() - 1 - i];
+						switch (top._type) {
+						case StackEntryType::Number:
+							debugN("Number %d", top._number);
+							break;
+						case StackEntryType::Variable:
+							debugN("Var %u (%d)", top._index, _script._variables[top._index]);
+							break;
+						case StackEntryType::Instruction:
+							debugN("Instr %u", top._index);
+							break;
+						case StackEntryType::String:
+							debugN("String %u (\"%s\")", top._index, getStringArg(0));
+							break;
+						default:
+							debugN("INVALID");
+							break;
+						}
 					}
+					debug("");
 				}
 			}
 
@@ -919,7 +925,7 @@ private:
 				warning("Tried to drop from none-character-process: %s at %u", getStringArg(0), _pc);
 			}
 			else
-			relatedCharacter().drop(getStringArg(0));
+				relatedCharacter().drop(getStringArg(0));
 			return TaskReturn::finish(1);
 		case ScriptKernelTask::CharacterDrop: {
 			auto &character = g_engine->world().getMainCharacterByKind(getMainCharacterKindArg(1));
@@ -1144,4 +1150,23 @@ void Script::setScriptTimer(bool reset) {
 		_scriptTimer = g_engine->getMillis();
 }
 
+void Script::fixNestedMenuPop(uint32 pc) {
+	/* There seems to have been a script compiler bug related to nested dialog menus,
+	 * where an additional PopN 1 is called underflowing the value stack.
+	 * I see no good way to fix in the script interpreter so instead we patch the script
+	 *
+	 * This happens in:
+	 *   aventuradecine-cd-remastered-win-es
+	 *   secta-win-es
+	 *   escarabajo-win-es
+	 *   moscu-win-es
+	 */
+	scumm_assert(pc < _instructions.size());
+	auto &instr = _instructions[pc];
+	scumm_assert(g_engine->game().getScriptOpMap()[instr._op] == ScriptOp::PopN && instr._arg == 1);
+
+	// the additional pop is a fallback for a switch that is never called, so just not popping is fine
+	instr._arg = 0;
+}
+
 }
diff --git a/engines/alcachofa/script.h b/engines/alcachofa/script.h
index 5afd1db2914..c4e39a7df8d 100644
--- a/engines/alcachofa/script.h
+++ b/engines/alcachofa/script.h
@@ -177,6 +177,7 @@ public:
 	inline bool hasVariable(const char *name) const { return _variableNames.contains(name); }
 
 	void setScriptTimer(bool reset);
+	void fixNestedMenuPop(uint32 pc);
 private:
 	friend struct ScriptTask;
 	friend struct ScriptTimerTask;


Commit: 470ba13bb46d35b59c6e924aced62e1b0c7f373d
    https://github.com/scummvm/scummvm/commit/470ba13bb46d35b59c6e924aced62e1b0c7f373d
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:29+02:00

Commit Message:
ALCACHOFA: Fix two coverity issues

- Access moved-from object
- Raw pointers not initialized in ctor

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


diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 6888d73f3e3..59860e26fce 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -1056,7 +1056,7 @@ public:
 		// e.g. c:\myf2000\textos\dialogos.txt
 		// but the filenames alone do not clash so we flatten everything
 
-		skipVarString(*file);
+		skipVarString(*_file);
 		uint32 totalSize = _file->readUint32LE();
 		int64 endPosition = _file->pos() + totalSize;
 		_file->skip(2);
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index 775267202dd..5d6235c7c82 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -225,9 +225,9 @@ private:
 	GameFileReference _globalAnimations[(int)GlobalAnimationKind::Count];
 	Common::String _initScriptName;
 	GameFileReference _scriptFileRef;
-	Room *_globalRoom;
-	Inventory *_inventory;
-	MainCharacter *_filemon, *_mortadelo;
+	Room *_globalRoom = nullptr;
+	Inventory *_inventory = nullptr;
+	MainCharacter *_filemon = nullptr, *_mortadelo = nullptr;
 	uint8 _loadedMapCount = 0;
 	Common::HashMap<const char *, const char *,
 		Common::Hash<const char *>,


Commit: b7b1ba88c7ca28a8357bd40d2739b207ef9cb762
    https://github.com/scummvm/scummvm/commit/b7b1ba88c7ca28a8357bd40d2739b207ef9cb762
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:29+02:00

Commit Message:
ALCACHOFA: Fix bracket style with if-else

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


diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 59860e26fce..c403deac30c 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -42,8 +42,7 @@ Room::Room(World *world, SeekableReadStream &stream)
 	if (g_engine->isV1()) {
 		readRoomV1(stream);
 		readObjects(stream);
-	}
-	else
+	} else
 		readRoomV2and3(stream, false);
 	initBackground();
 }
@@ -72,8 +71,7 @@ static ObjectBase *readRoomObject(Room *room, const String &type, SeekableReadSt
 			return new EditBoxV2(room, stream);
 		else
 			return new EditBoxV3(room, stream);
-	}
-	else if (type == PushButton::kClassName)
+	} else if (type == PushButton::kClassName)
 		return new PushButton(room, stream);
 	else if (type == CheckBox::kClassName)
 		return new CheckBox(room, stream);
@@ -84,8 +82,7 @@ static ObjectBase *readRoomObject(Room *room, const String &type, SeekableReadSt
 			return new SlideButtonV2(room, stream);
 		else
 			return new SlideButtonV3(room, stream);
-	}
-	else if (type == IRCWindow::kClassName)
+	} else if (type == IRCWindow::kClassName)
 		return new IRCWindow(room, stream);
 	else if (type == MessageBox::kClassName)
 		return new MessageBox(room, stream);
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 0393498baff..00d178e9816 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -484,8 +484,7 @@ private:
 
 		if (g_engine->isV1()) {
 			popN(g_engine->game().getKernelTaskArgCount(_lastKernelTaskI));
-		}
-		else {
+		} else {
 			scumm_assert(
 				_pc < _script._instructions.size() &&
 				g_engine->game().getScriptOpMap()[_script._instructions[_pc]._op] == ScriptOp::PopN);
@@ -586,8 +585,7 @@ private:
 			return value == 0
 				? MainCharacterKind::Mortadelo
 				: MainCharacterKind::Filemon;
-		}
-		else {
+		} else {
 			if (value < 0 || value > 2)
 				error("Unexpected value for main character kind: %d", value);
 			else
@@ -923,8 +921,7 @@ private:
 			if (process().character() == MainCharacterKind::None) {
 				// This happens in Secta, the original game just ignores this case
 				warning("Tried to drop from none-character-process: %s at %u", getStringArg(0), _pc);
-			}
-			else
+			} else
 				relatedCharacter().drop(getStringArg(0));
 			return TaskReturn::finish(1);
 		case ScriptKernelTask::CharacterDrop: {


Commit: 1829417aaf1578a872b9e6131f7488596b7cc06e
    https://github.com/scummvm/scummvm/commit/1829417aaf1578a872b9e6131f7488596b7cc06e
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:29+02:00

Commit Message:
ALCACHOFA: Fix kernel proc names in debug traces

Changed paths:
    engines/alcachofa/script-debug.h
    engines/alcachofa/script.h


diff --git a/engines/alcachofa/script-debug.h b/engines/alcachofa/script-debug.h
index 96bab4d7232..1580926390e 100644
--- a/engines/alcachofa/script-debug.h
+++ b/engines/alcachofa/script-debug.h
@@ -70,6 +70,7 @@ static const char *const KernelCallNames[] = {
 	"StopAndTurnMe",
 	"ChangeCharacter",
 	"SayText",
+	"SayTextV2",
 	"Go",
 	"Put",
 	"ChangeCharacterRoom",
@@ -101,12 +102,14 @@ static const char *const KernelCallNames[] = {
 	"WaitCamStopping",
 	"CamFollow",
 	"CamShake",
+	"CamShakeV2",
 	"LerpCamXY",
 	"LerpCamZ",
 	"LerpCamScale",
 	"LerpCamToObjectWithScale",
 	"LerpCamToObjectResettingZ",
 	"LerpCamRotation",
+	"LerpOrSetCam",
 	"FadeIn",
 	"FadeOut",
 	"FadeIn2",
@@ -114,7 +117,6 @@ static const char *const KernelCallNames[] = {
 	"LerpCamXYZ",
 	"LerpCamToObjectKeepingZ",
 
-	"SheriffTakesCharacter",
 	"ChangeDoor",
 	"Disguise"
 };
diff --git a/engines/alcachofa/script.h b/engines/alcachofa/script.h
index c4e39a7df8d..9383d3870f0 100644
--- a/engines/alcachofa/script.h
+++ b/engines/alcachofa/script.h
@@ -35,6 +35,7 @@ class Process;
 
 // the ScriptOp and ScriptKernelTask enums represent the *implemented* order
 // the specific Game instance maps the version-specific op codes to our order
+// keep the order in sync with ScriptOpNames/KernelCallNames in script-debug.h
 
 enum class ScriptOp {
 	Nop,
@@ -129,7 +130,7 @@ enum class ScriptKernelTask {
 	LerpCamXYZ,
 	LerpCamToObjectKeepingZ,
 
-	ChangeDoor, ///< some special-case V1 tasks, unknown yet
+	ChangeDoor, ///< NOOP 
 	Disguise
 };
 


Commit: 0b749d78195ec0ce81fd87ca00bdd763ccabd695
    https://github.com/scummvm/scummvm/commit/0b749d78195ec0ce81fd87ca00bdd763ccabd695
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:29+02:00

Commit Message:
ALCACHOFA: V2: Adapt camera to V2

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/camera.cpp
    engines/alcachofa/camera.h
    engines/alcachofa/game-v2.cpp
    engines/alcachofa/script-debug.h
    engines/alcachofa/script.cpp
    engines/alcachofa/script.h


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index d4098630113..beb81dd0a2e 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -84,7 +84,7 @@ Common::Error AlcachofaEngine::run() {
 	_world.load();
 	_renderer.reset(IRenderer::createOpenGLRenderer(game().getResolution()));
 	_drawQueue.reset(new DrawQueue(_renderer.get()));
-	_camera.reset(isV1() || isV2() ? static_cast<Camera *>(new CameraV1()) : new CameraV3());
+	_camera.reset(Camera::create());
 	_script.reset(new Script());
 	_player.reset(new Player());
 	_globalUI.reset(isV1() || isV2() ? static_cast<GlobalUI *>(new GlobalUIV1()) : new GlobalUIV3());
diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 3bf2a86d285..204b3f3e17b 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -34,6 +34,17 @@ namespace Alcachofa {
 //
 // Base camera
 //
+Camera *Camera::create() {
+	if (g_engine->isV1())
+		return new CameraV1();
+	else if (g_engine->isV2())
+		return new CameraV2();
+	else if (g_engine->isV3())
+		return new CameraV3();
+	else
+		error("Camera is not implemented for this engine version");
+}
+
 Camera::~Camera() {}
 
 static Matrix4 scale2DMatrix(float scale) {
@@ -187,17 +198,40 @@ void CameraV1::update() {
 			_isLerping = true;
 		}
 	} else if (_isLerping) {
-		auto distance = newCenter.getDistanceTo(_target);
-		auto move = deltaTime * _lerpSpeed;
-		_lastUpdateTime = g_engine->getMillis();
+		updateLerping(newCenter, deltaTime, _lerpSpeed);
+	}
 
-		if (move < distance)
-			newCenter += (_target - newCenter) / distance * move;
-		else {
-			newCenter = _target;
-			_isLerping = false;
-		}
+	setAppliedCenter(newCenter);
+}
+
+void CameraV1::updateLerping(Vector3d &newCenter, float deltaTime, float speed) {
+	auto distance = newCenter.getDistanceTo(_target);
+	auto move = deltaTime * speed;
+	_lastUpdateTime = g_engine->getMillis();
+
+	if (move < distance)
+		newCenter += (_target - newCenter) / distance * move;
+	else {
+		newCenter = _target;
+		_isLerping = false;
 	}
+}
+
+void CameraV2::update() {
+	auto deltaTime = (g_engine->getMillis() - _lastUpdateTime) / 1000.0f;
+	auto newCenter = _appliedCenter;
+
+	if (_followTarget != nullptr) {
+		_target = as3D(_followTarget->position());
+		auto delta = _target - _appliedCenter;
+		_isLerping |= MAX(fabsf(delta.x()), fabsf(delta.y())) > 35.0f;
+
+		if (_isLerping)
+			updateLerping(newCenter, deltaTime, _lerpSpeed * _followTarget->graphic()->depthScale());
+		else
+			_lastUpdateTime = g_engine->getMillis();
+	} else if (_isLerping)
+		updateLerping(newCenter, deltaTime, _lerpSpeed);
 
 	setAppliedCenter(newCenter);
 }
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index 35d2c98ee4e..6abbde15376 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -37,6 +37,7 @@ static constexpr const float kInvBaseScale = 1.0f / kBaseScale;
 
 class Camera {
 public:
+	static Camera *create();
 	virtual ~Camera();
 	virtual Math::Angle rotation() const = 0;
 	virtual float scale() const = 0;
@@ -93,8 +94,9 @@ public:
 
 	Task *disguise(Process &process, int32 duration);
 
-private:
+protected:
 	friend struct CamV1DisguiseTask;
+	void updateLerping(Math::Vector3d &newCenter, float deltaTime, float speed);
 
 	WalkingCharacter *_followTarget = nullptr;
 	Math::Vector3d _target;
@@ -103,7 +105,13 @@ private:
 	uint32 _lastUpdateTime = 0;
 };
 
-class CameraV3 : public Camera {
+// V2 is so similar that only the update needs to be changed
+class CameraV2 final : public CameraV1 {
+public:
+	void update() override;
+};
+
+class CameraV3 final : public Camera {
 public:
 	Math::Angle rotation() const override;
 	float scale() const override;
diff --git a/engines/alcachofa/game-v2.cpp b/engines/alcachofa/game-v2.cpp
index ee4be6202f5..4725848873c 100644
--- a/engines/alcachofa/game-v2.cpp
+++ b/engines/alcachofa/game-v2.cpp
@@ -94,7 +94,7 @@ static constexpr const ScriptKernelTask kScriptKernelTaskMap[] = {
 	ScriptKernelTask::Drop,
 	ScriptKernelTask::CharacterDrop,
 	ScriptKernelTask::ChangeDoor,
-	ScriptKernelTask::CamShakeV2,
+	ScriptKernelTask::Disguise,
 	ScriptKernelTask::ToggleRoomFloor,
 	ScriptKernelTask::SetDialogLineReturn,
 	ScriptKernelTask::DialogMenu,
diff --git a/engines/alcachofa/script-debug.h b/engines/alcachofa/script-debug.h
index 1580926390e..a908ea4f320 100644
--- a/engines/alcachofa/script-debug.h
+++ b/engines/alcachofa/script-debug.h
@@ -102,7 +102,6 @@ static const char *const KernelCallNames[] = {
 	"WaitCamStopping",
 	"CamFollow",
 	"CamShake",
-	"CamShakeV2",
 	"LerpCamXY",
 	"LerpCamZ",
 	"LerpCamScale",
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 00d178e9816..611bae40ebb 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -959,9 +959,6 @@ private:
 				Vector2d(getNumberArg(1), getNumberArg(2)),
 				Vector2d(getNumberArg(3), getNumberArg(4)),
 				getNumberArg(0)));
-		case ScriptKernelTask::CamShakeV2:
-			warning("STUB: CamShakeV2");
-			return TaskReturn::finish(0);
 		case ScriptKernelTask::LerpCamXY:
 			return TaskReturn::waitFor(g_engine->cameraV3().lerpPos(process(),
 				Vector2d(getNumberArg(0), getNumberArg(1)),
@@ -1028,11 +1025,13 @@ private:
 				if (pointObject == nullptr)
 					return TaskReturn::finish(1);
 				g_engine->cameraV1().lerpOrSet(pointObject->position(), getNumberArg(1));
+				// cameraV1 could also be the inherited cameraV2 here
 			}
 			return TaskReturn::finish(0);
 		}
 		case ScriptKernelTask::Disguise: {
 			// a somewhat bouncy vertical camera movement used in V1
+			// in V2 this would be a linear vertical shake, but only the waitForInput part is used
 			// or waiting for user to click
 			const auto duration = getNumberArg(0);
 			return TaskReturn::waitFor(duration == 0
diff --git a/engines/alcachofa/script.h b/engines/alcachofa/script.h
index 9383d3870f0..1d388705db9 100644
--- a/engines/alcachofa/script.h
+++ b/engines/alcachofa/script.h
@@ -115,7 +115,6 @@ enum class ScriptKernelTask {
 	WaitCamStopping,
 	CamFollow,
 	CamShake,
-	CamShakeV2,
 	LerpCamXY,
 	LerpCamZ,
 	LerpCamScale,


Commit: fbf769a5ab8f04adebad9c4c71e875934961f63f
    https://github.com/scummvm/scummvm/commit/fbf769a5ab8f04adebad9c4c71e875934961f63f
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:29+02:00

Commit Message:
ALCACHOFA: V2: Fix subtitle position

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


diff --git a/engines/alcachofa/game-v2.cpp b/engines/alcachofa/game-v2.cpp
index 4725848873c..033c02304f6 100644
--- a/engines/alcachofa/game-v2.cpp
+++ b/engines/alcachofa/game-v2.cpp
@@ -128,7 +128,7 @@ public:
 	}
 	
 	Point getSubtitlePos() override {
-		return Point(g_system->getWidth() / 2, 150); // TODO: Check subtitle position
+		return Point(g_system->getWidth() / 2, g_system->getHeight() - 200);
 	}
 
 	const char *getMenuRoom() override {


Commit: c65661858335b8fc5b35b35b0f2ea2cc374eec48
    https://github.com/scummvm/scummvm/commit/c65661858335b8fc5b35b35b0f2ea2cc374eec48
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:29+02:00

Commit Message:
ALCACHOFA: V2: Fix camera speed in large rooms

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


diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 204b3f3e17b..9904ef42300 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -226,9 +226,14 @@ void CameraV2::update() {
 		auto delta = _target - _appliedCenter;
 		_isLerping |= MAX(fabsf(delta.x()), fabsf(delta.y())) > 35.0f;
 
-		if (_isLerping)
-			updateLerping(newCenter, deltaTime, _lerpSpeed * _followTarget->graphic()->depthScale());
-		else
+		if (_isLerping) {
+			// The original code contains this formula to attenuate camera speed
+			// However timing experiments show that this is somehow negated or overwritten
+			//float scaleFactor = _followTarget->graphic()->depthScale();
+			//if (scaleFactor < 19660 / 65535.0f)
+			//	scaleFactor = (13107 / 65535.0f) + scaleFactor / 3;
+			updateLerping(newCenter, deltaTime, _lerpSpeed);
+		} else
 			_lastUpdateTime = g_engine->getMillis();
 	} else if (_isLerping)
 		updateLerping(newCenter, deltaTime, _lerpSpeed);
@@ -244,6 +249,15 @@ void CameraV1::setRoomBounds(Graphic &background) {
 	_roomScale = 0;
 }
 
+void CameraV2::setRoomBounds(Graphic &background) {
+	Point bgSize = background.animation().imageSize(0);
+	float scaleFactor = background.scale() / (float)kBaseScale;
+	Point screenSize(g_system->getWidth(), g_system->getHeight());
+	_roomMin = as2D(background.topLeft() + screenSize / 2) * scaleFactor;
+	_roomMax = _roomMin + as2D(bgSize - screenSize) * scaleFactor;
+	_roomScale = 0;
+}
+
 void CameraV1::setFollow(WalkingCharacter *target) {
 	_lastUpdateTime = g_engine->getMillis();
 	_followTarget = target;
@@ -252,6 +266,11 @@ void CameraV1::setFollow(WalkingCharacter *target) {
 		setAppliedCenter(as3D(target->position()));
 }
 
+void CameraV2::setFollow(WalkingCharacter *target) {
+	CameraV1::setFollow(target);
+	_lerpSpeed = 230.0f;
+}
+
 void CameraV1::onChangedRoom(bool resetCamera) {
 	// nothing to do in V1
 }
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index 6abbde15376..bb2aa608c9f 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -105,10 +105,12 @@ protected:
 	uint32 _lastUpdateTime = 0;
 };
 
-// V2 is so similar that only the update needs to be changed
+// V2 is so similar that most can be reused from V1
 class CameraV2 final : public CameraV1 {
 public:
 	void update() override;
+	void setRoomBounds(Graphic &background) override;
+	void setFollow(WalkingCharacter *target) override;
 };
 
 class CameraV3 final : public Camera {


Commit: 4e51eaf599e29622b9f2375955d4a781570dff40
    https://github.com/scummvm/scummvm/commit/4e51eaf599e29622b9f2375955d4a781570dff40
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:29+02:00

Commit Message:
ALCACHOFA: V2: Fix long-running animations

Previously animations with more than 127 images would disappear sometimes.

Changed paths:
    engines/alcachofa/graphics.cpp


diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 93322ee5aba..336d53b7567 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -320,8 +320,8 @@ void AnimationBase::createIndexMappingV1and2(const Array<byte> &spriteOrder) {
 void AnimationBase::readFramesV1and2(Common::SeekableReadStream &stream, uint frameCount, uint spriteCount) {
 	for (uint i = 0; i < frameCount; i++) {
 		for (uint j = 0; j < spriteCount; j++) {
-			int imageI = stream.readSByte();
-			if (imageI <= 0) // we make sure that spriteBases + imageI <= 0 if the local imageI <= 0
+			int imageI = stream.readByte();
+			if (imageI <= 0 || imageI > _images.size()) // we make sure that spriteBases + imageI <= 0 if the local imageI is invalid
 				imageI = -(int)_spriteOffsets.size();
 			_spriteOffsets.push_back(imageI);
 		}


Commit: 8eeecc370894ea0348b3a3dae3e1ea7358d73f5a
    https://github.com/scummvm/scummvm/commit/8eeecc370894ea0348b3a3dae3e1ea7358d73f5a
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:29+02:00

Commit Message:
ALCACHOFA: V2: Add moscu and escarabajo detection entries

Changed paths:
    engines/alcachofa/detection_tables.h
    engines/alcachofa/game-v1.cpp
    engines/alcachofa/game-v2.cpp
    engines/alcachofa/game.cpp
    engines/alcachofa/game.h


diff --git a/engines/alcachofa/detection_tables.h b/engines/alcachofa/detection_tables.h
index 0732593acfc..1d47614740e 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" },
+	{ "escarabajo", "Mortadelo y Filemón: El escarabajo de Cleopatra" },
+	{ "moscu", "Mortadelo y Filemón: Operación Moscú" },
 	{ "secta", "Mortadelo y Filemón: La Sexta Secta" },
 	{ "terror", "Mortadelo y Filemón: Terror, Espanto y Pavor" },
 	{ "vaqueros", "Mortadelo y Filemón: Dos vaqueros chapuceros" },
@@ -137,7 +139,61 @@ const AlcachofaGameDescription gameDescriptions[] = {
 		{
 			"secta",
 			"Mortadelo y Filemón: La Sexta Secta",
-			AD_ENTRY1s("Fondos/MUSEO_O.ANI", "40a880c866aabbb5c09899d9b7ca66b6", 10630),
+			AD_ENTRY3s(
+				"Fondos/MUSEO_O.ANI", "40a880c866aabbb5c09899d9b7ca66b6", 10630,
+				"Mapas/mapa1.emc", "c04b7b6424c02d5da0719bdf648003a1", 36530,
+				"Mapas/mapa2.emc", "c04b7b6424c02d5da0719bdf648003a1", 67129),
+			Common::ES_ESP,
+			Common::kPlatformWindows,
+			ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE,
+			GUIO0()
+		},
+		EngineVersion::V2_0
+	},
+
+	//
+	// Operación Moscú
+	//
+	{
+		{
+			"moscu",
+			"Mortadelo y Filemón: Operación Moscú",
+			AD_ENTRY2s(
+				"Fondos/MUSEO_O.ANI", "40a880c866aabbb5c09899d9b7ca66b6", 10630,
+				"Mapas/mapa1.emc", "c04b7b6424c02d5da0719bdf648003a1", 36530),
+			Common::ES_ESP,
+			Common::kPlatformWindows,
+			ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE,
+			GUIO0()
+		},
+		EngineVersion::V2_0
+	},
+	// moscu had a variant with translated texts published on Steam
+	{
+		{
+			"moscu",
+			"Mortadelo y Filemón: Operación Moscú",
+			AD_ENTRY2s(
+				"Fondos/MUSEO_O.ANI", "479ae9100e1730ac03e6f3f84da91f32", 11265,
+				"Mapas/mapa1.emc", "c04b7b6424c02d5da0719bdf648003a1", 36530),
+			Common::EN_ANY,
+			Common::kPlatformWindows,
+			ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE,
+			GUIO0()
+		},
+		EngineVersion::V2_0
+	},
+
+	//
+	// El escarabajo de Cleopatra
+	//
+	{
+		{
+			"escarabajo",
+			"Mortadelo y Filemón: El escarabajo de Cleopatra",
+			AD_ENTRY2s(
+				"Fondos/MUSEO_O.ANI", "40a880c866aabbb5c09899d9b7ca66b6", 10630,
+				"Mapas/mapa2.emc", "c04b7b6424c02d5da0719bdf648003a1", 67129),
 			Common::ES_ESP,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE,
diff --git a/engines/alcachofa/game-v1.cpp b/engines/alcachofa/game-v1.cpp
index af0a23c11e3..951119522e0 100644
--- a/engines/alcachofa/game-v1.cpp
+++ b/engines/alcachofa/game-v1.cpp
@@ -398,7 +398,7 @@ public:
 	}
 
 	String getMusicPath(int32 trackId) override {
-		return String::format("track%02d", trackId);
+		return String::format("track%d", trackId);
 	}
 
 	// probably the original CDs have music, the Steam release has no music...
diff --git a/engines/alcachofa/game-v2.cpp b/engines/alcachofa/game-v2.cpp
index 033c02304f6..460e220c550 100644
--- a/engines/alcachofa/game-v2.cpp
+++ b/engines/alcachofa/game-v2.cpp
@@ -161,10 +161,6 @@ public:
 		return String("Sonidos/") + filename;
 	}
 
-	String getMusicPath(int32 trackId) override {
-		return String::format("Music/Track%02d", trackId);
-	}
-
 	int32 getCharacterJingle(MainCharacterKind kind) override {
 		return g_engine->script().variable(
 			kind == MainCharacterKind::Mortadelo ? "PistaMorta" : "PistaFile");
@@ -221,6 +217,10 @@ public:
 		script.fixNestedMenuPop(5921); // Mortadelo talking to ARQUEOLOGOS in CARRETERA
 		script.fixNestedMenuPop(20898); // Filemon talking to MANOLO in FILE_PIRAMIDE
 	}
+
+	char getTextFileKey() override {
+		return static_cast<char>(0xA3);
+	}
 };
 
 static constexpr const char *kMapFilesSecta[] = {
@@ -230,6 +230,18 @@ static constexpr const char *kMapFilesSecta[] = {
 	nullptr
 };
 
+static constexpr const char *kMapFilesMoscu[] = {
+	"Mapas/mapa1.emc",
+	"Mapas/global.emc",
+	nullptr
+};
+
+static constexpr const char *kMapFilesEscarabajo[] = {
+	"Mapas/mapa2.emc",
+	"Mapas/global.emc",
+	nullptr
+};
+
 class GameSecta : public GameWithVersion2_0 {
 public:
 	GameSecta() {
@@ -246,20 +258,64 @@ public:
 		return !_videosAreExtracted;
 	}
 
+	void onLoadedGameFiles() override {
+		g_engine->script().variable("EsJuegoCompleto") = 0;
+	}
+
 	const char *const *getMapFiles() override {
 		return kMapFilesSecta;
 	}
 
-	char getTextFileKey() override {
-		return static_cast<char>(0xA3);
+	String getMusicPath(int32 trackId) override {
+		const Room *room = g_engine->player().currentRoom();
+		const char *dirName = room != nullptr && room->mapIndex() == 1 ? "Music_Cleopatra" : "Music";
+		return String::format("%s/Track%02d", dirName, trackId);
 	}
 
 private:
 	bool _videosAreExtracted = true;
 };
 
+class GameMoscu : public GameWithVersion2_0 {
+public:
+	void onLoadedGameFiles() override {
+		g_engine->script().variable("EsJuegoCompleto") = 1;
+	}
+
+	const char *const *getMapFiles() override {
+		return kMapFilesMoscu;
+	}
+
+	String getMusicPath(int32 trackId) override {
+		return String::format("track%d", trackId);
+	}
+};
+
+class GameEscarabajo : public GameWithVersion2_0 {
+public:
+	void onLoadedGameFiles() override {
+		g_engine->script().variable("EsJuegoCompleto") = 2;
+	}
+
+	const char *const *getMapFiles() override {
+		return kMapFilesEscarabajo;
+	}
+
+	String getMusicPath(int32 trackId) override {
+		return String::format("track%d", trackId);
+	}
+};
+
 Game *Game::createForSecta() {
 	return new GameSecta();
 }
 
+Game *Game::createForMoscu() {
+	return new GameMoscu();
+}
+
+Game *Game::createForEscarabajo() {
+	return new GameEscarabajo();
+}
+
 }
diff --git a/engines/alcachofa/game.cpp b/engines/alcachofa/game.cpp
index 02d61a657ed..c882e99e13b 100644
--- a/engines/alcachofa/game.cpp
+++ b/engines/alcachofa/game.cpp
@@ -227,6 +227,10 @@ Game *Game::create() {
 		switch (*desc.desc.gameId) {
 		case 's':
 			return createForSecta();
+		case 'm':
+			return createForMoscu();
+		case 'e':
+			return createForEscarabajo();
 		}
 		break;
 	case EngineVersion::V3_0:
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index f7a5a1ff51f..b7354842ad7 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -128,6 +128,8 @@ public:
 	static Game *createForTerror(); // V1
 	static Game *createForVaqueros(); // V1
 	static Game *createForSecta(); // V2
+	static Game *createForMoscu(); // V2
+	static Game *createForEscarabajo(); // V2
 
 	const Message _message;
 };


Commit: dea78dd95f0ed1a5d750d5f1f7beb7c307579b03
    https://github.com/scummvm/scummvm/commit/dea78dd95f0ed1a5d750d5f1f7beb7c307579b03
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:29+02:00

Commit Message:
ALCACHOFA: Fix signed/unsigned comparison warning

Changed paths:
    engines/alcachofa/graphics.cpp


diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 336d53b7567..f2e752a2921 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -321,7 +321,7 @@ void AnimationBase::readFramesV1and2(Common::SeekableReadStream &stream, uint fr
 	for (uint i = 0; i < frameCount; i++) {
 		for (uint j = 0; j < spriteCount; j++) {
 			int imageI = stream.readByte();
-			if (imageI <= 0 || imageI > _images.size()) // we make sure that spriteBases + imageI <= 0 if the local imageI is invalid
+			if (imageI <= 0 || (uint)imageI > _images.size()) // we make sure that spriteBases + imageI <= 0 if the local imageI is invalid
 				imageI = -(int)_spriteOffsets.size();
 			_spriteOffsets.push_back(imageI);
 		}


Commit: 973d5f01279fb8c671ed05cb86d88081de2d1a49
    https://github.com/scummvm/scummvm/commit/973d5f01279fb8c671ed05cb86d88081de2d1a49
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:29+02:00

Commit Message:
ALCACHOFA: V2: Add corvino, balones and mamelucos detection entries

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


diff --git a/engines/alcachofa/detection.h b/engines/alcachofa/detection.h
index f505a2e55b8..2c45bb86ac0 100644
--- a/engines/alcachofa/detection.h
+++ b/engines/alcachofa/detection.h
@@ -36,7 +36,8 @@ enum AlcachofaDebugChannels {
 
 enum class EngineVersion {
 	V1_0 = 10, // edicion orginal, vaqueros and terror
-	V2_0 = 20, // the rest
+	V2_0 = 20, // secta, moscu and escarabajo
+	V2_1 = 21, // corvino, balones and mamelucos
 	V3_0 = 30, // Remastered movie adventure (used for original spanish release)
 	V3_1 = 31, // Remastered movie adventure (for german release and english/spanish steam release)
 };
diff --git a/engines/alcachofa/detection_tables.h b/engines/alcachofa/detection_tables.h
index 1d47614740e..ec6db19eed2 100644
--- a/engines/alcachofa/detection_tables.h
+++ b/engines/alcachofa/detection_tables.h
@@ -23,7 +23,10 @@ namespace Alcachofa {
 
 const PlainGameDescriptor alcachofaGames[] = {
 	{ "aventuradecine", "Mort & Phil: A Movie Adventure" },
+	{ "balones", "Mortadelo y Filemón: Balones y Patadones" },
+	{ "corvino", "Mortadelo y Filemón: La Banda de Corvino" },
 	{ "escarabajo", "Mortadelo y Filemón: El escarabajo de Cleopatra" },
+	{ "mamelucos", "Mortadelo y Filemón: Mamelucos a la Romana" },
 	{ "moscu", "Mortadelo y Filemón: Operación Moscú" },
 	{ "secta", "Mortadelo y Filemón: La Sexta Secta" },
 	{ "terror", "Mortadelo y Filemón: Terror, Espanto y Pavor" },
@@ -132,6 +135,61 @@ const AlcachofaGameDescription gameDescriptions[] = {
 		EngineVersion::V3_1
 	},
 
+	//
+	// La Banda de Corvino
+	//
+	{
+		{
+			"corvino",
+			"Mortadelo y Filemón: La Banda de Corvino",
+			AD_ENTRY3s(
+				"Fondos/MUSEO_O.ANI", "830443af2290a96a95703a17c1915c21", 9732, // this file contains object names, thus detects the language
+				"Mapas/mapa1.emc", "d0a8eb184e813cf337840bb0e5270ee8", 33515,
+				"Mapas/mapa2.emc", "d0a8eb184e813cf337840bb0e5270ee8", 40452),
+			Common::ES_ESP,
+			Common::kPlatformWindows,
+			ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE,
+			GUIO0()
+		},
+		EngineVersion::V2_1
+	},
+
+	//
+	// Balones y Patadones
+	//
+	{
+		{
+			"balones",
+			"Mortadelo y Filemón: Balones y Patadones",
+			AD_ENTRY2s(
+				"Fondos/MUSEO_O.ANI", "830443af2290a96a95703a17c1915c21", 9732,
+				"Mapas/mapa1.emc", "d0a8eb184e813cf337840bb0e5270ee8", 33515),
+			Common::ES_ESP,
+			Common::kPlatformWindows,
+			ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE,
+			GUIO0()
+		},
+		EngineVersion::V2_1
+	},
+
+	//
+	// Mamelucos a la Romana
+	//
+	{
+		{
+			"corvino",
+			"Mortadelo y Filemón: Mamelucos a la Romana",
+			AD_ENTRY2s(
+				"Fondos/MUSEO_O.ANI", "830443af2290a96a95703a17c1915c21", 9732,
+				"Mapas/mapa2.emc", "d0a8eb184e813cf337840bb0e5270ee8", 40452),
+			Common::ES_ESP,
+			Common::kPlatformWindows,
+			ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE,
+			GUIO0()
+		},
+		EngineVersion::V2_1
+	},
+
 	//
 	// La Sexta Secta
 	//


Commit: 471b404af57bc4ab105be4e3a507dc04e38d0fe5
    https://github.com/scummvm/scummvm/commit/471b404af57bc4ab105be4e3a507dc04e38d0fe5
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:29+02:00

Commit Message:
ALCACHOFA: V2: Fix startup of escarabajo and moscu

Changed paths:
    engines/alcachofa/game-v2.cpp
    engines/alcachofa/game.cpp
    engines/alcachofa/game.h
    engines/alcachofa/rooms.cpp


diff --git a/engines/alcachofa/game-v2.cpp b/engines/alcachofa/game-v2.cpp
index 460e220c550..20e6eed0978 100644
--- a/engines/alcachofa/game-v2.cpp
+++ b/engines/alcachofa/game-v2.cpp
@@ -289,10 +289,18 @@ public:
 	String getMusicPath(int32 trackId) override {
 		return String::format("track%d", trackId);
 	}
+
+	bool isKnownBadVideo(int32 videoId) override {
+		return videoId == 0; // MPEG-4 codec is unsupported
+	}
 };
 
 class GameEscarabajo : public GameWithVersion2_0 {
 public:
+	GameEscarabajo() {
+		_hasMessedUpEncoding = !SearchMan.hasFile(Path(reencode("Animaciones/M\xC1SCARA MUSEO_RECEPCI\xD3N.ANI")));
+	}
+
 	void onLoadedGameFiles() override {
 		g_engine->script().variable("EsJuegoCompleto") = 2;
 	}
@@ -304,6 +312,31 @@ public:
 	String getMusicPath(int32 trackId) override {
 		return String::format("track%d", trackId);
 	}
+
+	bool isKnownBadVideo(int32 videoId) override {
+		return videoId == 0; // MPEG-4 codec is unsupported
+	}
+
+	String reencodePath(const String &path) override {
+		if (!_hasMessedUpEncoding)
+			return Game::reencodePath(path);
+
+		// The Steam release has wrong characters due to some messed up UTF8 conversion
+		U32String u32String = path.decode(Common::CodePage::kISO8859_1);
+		for (uint i = 0; i < u32String.size(); i++) {
+			const auto ch = u32String[i];
+			if (ch == 0xC1) // Á -> ╡
+				u32String[i] = 0x2561;
+			else if (ch == 0xD3) // Ó -> α
+				u32String[i] = 0x03B1;
+			else if (ch == 0xCD) // Í -> ╓
+				u32String[i] = 0x2553;
+		}
+		return u32String.encode();
+	}
+
+private:
+	bool _hasMessedUpEncoding = false;
 };
 
 Game *Game::createForSecta() {
diff --git a/engines/alcachofa/game.cpp b/engines/alcachofa/game.cpp
index c882e99e13b..db0a7d79965 100644
--- a/engines/alcachofa/game.cpp
+++ b/engines/alcachofa/game.cpp
@@ -39,6 +39,11 @@ void Game::onLoadedGameFiles() {}
 
 void Game::drawScreenStates() {}
 
+String Game::reencodePath(const String &path) {
+	// Except for the messed up Stream release of escarabajo, this suffices
+	return reencode(path);
+}
+
 int32 Game::getKernelTaskArgCount(int32 kernelTaskI) {
 	(void)kernelTaskI;
 	return 0;
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index b7354842ad7..c91ef61f8ac 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -59,6 +59,7 @@ public:
 	virtual Common::Span<const ScriptKernelTask> getScriptKernelTaskMap() = 0;
 	virtual void updateScriptVariables() = 0;
 	virtual void drawScreenStates();
+	virtual Common::String reencodePath(const Common::String &path);
 	virtual const char *getDialogFileName() = 0;
 	virtual const char *getObjectFileName() = 0;
 	virtual char getTextFileKey() = 0;
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index c403deac30c..9781d35647c 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -637,7 +637,9 @@ ObjectBase *World::getObjectByName(MainCharacterKind character, const char *name
 		return getObjectByName(name);
 	const auto &player = g_engine->player();
 	ObjectBase *result = nullptr;
-	if (player.activeCharacterKind() == character && player.currentRoom() != player.activeCharacter()->room())
+	if (player.activeCharacterKind() == character &&
+		player.currentRoom() != nullptr &&
+		player.currentRoom() != player.activeCharacter()->room())
 		result = player.currentRoom()->getObjectByName(name);
 	if (result == nullptr)
 		result = player.activeCharacter()->room()->getObjectByName(name);
@@ -1015,7 +1017,7 @@ GameFileReference World::readFileRef(SeekableReadStream &stream) const {
 		stream.skip(size);
 		return { name, (uint32)_files.size(), offset, size };
 	} else
-		return GameFileReference(reencode(name));
+		return GameFileReference(g_engine->game().reencodePath(name));
 }
 
 ScopedPtr<SeekableReadStream> World::openFileRef(const GameFileReference &ref) const {


Commit: ea37a7d5a856b005a875cb1405b5d4ec10f428cc
    https://github.com/scummvm/scummvm/commit/ea37a7d5a856b005a875cb1405b5d4ec10f428cc
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:30+02:00

Commit Message:
ALCACHOFA: V2: Fix script error on entering DESPACHO_MOMIEZ twice

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


diff --git a/engines/alcachofa/game-v2.cpp b/engines/alcachofa/game-v2.cpp
index 20e6eed0978..40ceb67aee2 100644
--- a/engines/alcachofa/game-v2.cpp
+++ b/engines/alcachofa/game-v2.cpp
@@ -221,6 +221,13 @@ public:
 	char getTextFileKey() override {
 		return static_cast<char>(0xA3);
 	}
+
+	PointObject *unknownCamLerpTarget(const char *action, const char *name) override {
+		// Original bug: a main character being reinterpret_cast to a PointObject, undefined behavior ensues
+		if (scumm_stricmp(name, "FILEMON"))
+			return Game::unknownCamLerpTarget(action, name);
+		return nullptr;
+	}
 };
 
 static constexpr const char *kMapFilesSecta[] = {


Commit: ae3cba18ed1bb880a3b5ab2daa5f45b7cfa9fe45
    https://github.com/scummvm/scummvm/commit/ae3cba18ed1bb880a3b5ab2daa5f45b7cfa9fe45
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:30+02:00

Commit Message:
ALCACHOFA: Reversing draw order within one layer

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index e4152f28d15..70d826eb3c9 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -930,7 +930,7 @@ void MainCharacter::walkTo(
 
 void MainCharacter::draw() {
 	if (this == &g_engine->world().mortadelo()) {
-		if (_currentPos.y <= g_engine->world().filemon()._currentPos.y) {
+		if (_currentPos.y > g_engine->world().filemon()._currentPos.y) {
 			g_engine->world().mortadelo().drawInner();
 			g_engine->world().filemon().drawInner();
 		} else {
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index f2e752a2921..d01d4ea91a9 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -1248,8 +1248,8 @@ void DrawQueue::draw() {
 	for (int8 order = kOrderCount - 1; order >= 0; order--) {
 		_renderer->setLodBias(_lodBiasPerOrder[order]);
 		for (uint8 requestI = 0; requestI < _requestsPerOrderCount[order]; requestI++) {
-			_requestsPerOrder[order][requestI]->draw();
-			_requestsPerOrder[order][requestI]->~IDrawRequest();
+			_requestsPerOrder[order][_requestsPerOrderCount[order] - 1 - requestI]->draw();
+			_requestsPerOrder[order][_requestsPerOrderCount[order] - 1 - requestI]->~IDrawRequest();
 		}
 	}
 	_allocator.deallocateAll();


Commit: 657c8c1292fc95d415477fb3fa16f78c7e133aab
    https://github.com/scummvm/scummvm/commit/657c8c1292fc95d415477fb3fa16f78c7e133aab
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:30+02:00

Commit Message:
ALCACHOFA: Reenable DefectoObjeto script

Changed paths:
    engines/alcachofa/player.cpp


diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index e3ed38daec9..745c140d715 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -211,10 +211,9 @@ void Player::triggerObject(ObjectBase *object, const char *action) {
 	_activeCharacter->currentlyUsing() = nullptr;
 	if (scumm_stricmp(action, "MIRAR") == 0)
 		script.createProcess(activeCharacterKind(), "DefectoMirar");
-	//else if (action[0] == 'i' && object->name()[0] == 'i')
-	// This case can happen if you combine two objects without procedure, the original engine
-	// would attempt to start the procedure "DefectoObjeto" which does not exist
-	// (this should be revised when working on further games)
+	else if (action[0] == 'i' && object->name()[0] == 'i' && script.hasProcedure("DefectoObjeto"))
+		// This case can happen if you combine two objects without procedure. This does not happen in all games
+		script.createProcess(activeCharacterKind(), "DefectoObjeto");
 	else
 		script.createProcess(activeCharacterKind(), "DefectoUsar");
 }


Commit: 3d113845f199dd35e2da32a27757963fecb1e1c5
    https://github.com/scummvm/scummvm/commit/3d113845f199dd35e2da32a27757963fecb1e1c5
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:30+02:00

Commit Message:
ALCACHOFA: V2: Disable texture filtering by default

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


diff --git a/engines/alcachofa/game-v2.cpp b/engines/alcachofa/game-v2.cpp
index 40ceb67aee2..a8bd65ce7a8 100644
--- a/engines/alcachofa/game-v2.cpp
+++ b/engines/alcachofa/game-v2.cpp
@@ -172,7 +172,7 @@ public:
 	}
 
 	bool shouldFilterTexturesByDefault() override {
-		return true; // TODO: Check this!
+		return false;
 	}
 
 	bool shouldClipCamera() override {


Commit: d9e3e66dc207cf29fee43297a0fa1c3afd8f9927
    https://github.com/scummvm/scummvm/commit/d9e3e66dc207cf29fee43297a0fa1c3afd8f9927
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:30+02:00

Commit Message:
ALCACHOFA: Fix script loading with duplicated procedures

Changed paths:
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 611bae40ebb..3fac2bfe734 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -80,7 +80,7 @@ Script::Script() {
 		String name = readVarString(*file);
 		uint32 offset = file->readUint32LE();
 		file->skip(sizeof(uint32));
-		_procedures[name] = offset - 1; // originally one-based, but let's not.
+		_procedures[name] = offset; 
 	}
 
 	uint32 behaviorCount = file->readUint32LE();
@@ -93,7 +93,9 @@ Script::Script() {
 			String name = behaviorName + readVarString(*file);
 			uint32 offset = file->readUint32LE();
 			file->skip(sizeof(uint32));
-			_procedures[name] = offset - 1;
+			uint32 &storedOffset = _procedures.getOrCreateVal(name);
+			if (storedOffset == 0) // keep the offset one-based so we can detect previous procedures and not override them
+				storedOffset = offset;
 		}
 	}
 
@@ -1130,10 +1132,11 @@ Process *Script::createProcess(MainCharacterKind character, const String &proced
 		g_engine->game().unknownScriptProcedure(procedure);
 		return nullptr;
 	}
+	assert(offset > 0); // offsets are stored one-based to simplify loading
 	FakeLock lock;
 	if (!(flags & ScriptFlags::IsBackground))
 		lock = FakeLock("script", g_engine->player().semaphoreFor(character));
-	Process *process = g_engine->scheduler().createProcess<ScriptTask>(character, procedure, offset, Common::move(lock));
+	Process *process = g_engine->scheduler().createProcess<ScriptTask>(character, procedure, offset - 1, Common::move(lock));
 	process->name() = procedure;
 	return process;
 }


Commit: 73a82da4d14417f53ceedcc6a28917efc2aad198
    https://github.com/scummvm/scummvm/commit/73a82da4d14417f53ceedcc6a28917efc2aad198
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:30+02:00

Commit Message:
ALCACHOFA: Add walk fallback if character is outside the floor shape

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 70d826eb3c9..eee8e4b2fb7 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -715,8 +715,10 @@ void WalkingCharacter::walkTo(
 
 	_pathPoints.clear();
 	auto floor = room()->activeFloor();
-	if (floor != nullptr)
-		floor->findPath(_sourcePos, target, _pathPoints);
+	if (floor != nullptr && !floor->findPath(_sourcePos, target, _pathPoints)) {
+		// just walk directly, ignoring the floor shape altogether
+		_pathPoints.push(target);
+	}
 	if (_pathPoints.empty()) {
 		_isWalking = false;
 		onArrived();


Commit: 3fce6a3eaa559e732fb6bc3c4baf5379270f58da
    https://github.com/scummvm/scummvm/commit/3fce6a3eaa559e732fb6bc3c4baf5379270f58da
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:30+02:00

Commit Message:
ALCACHOFA: Allow characters as camera targets

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


diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
index 33b463f6a06..6dc40a3e592 100644
--- a/engines/alcachofa/console.cpp
+++ b/engines/alcachofa/console.cpp
@@ -56,6 +56,8 @@ Console::Console() : GUI::Debugger() {
 	registerCmd("playVoice", WRAP_METHOD(Console, cmdPlaySound));
 	registerCmd("playSFX", WRAP_METHOD(Console, cmdPlaySound));
 	registerCmd("dumpFile", WRAP_METHOD(Console, cmdDumpFile));
+	registerCmd("procedureAt", WRAP_METHOD(Console, cmdProcedureAt));
+	registerCmd("pa", WRAP_METHOD(Console, cmdProcedureAt));
 }
 
 Console::~Console() {}
@@ -378,4 +380,22 @@ bool Console::cmdDumpFile(int argc, const char **args) {
 	return true;
 }
 
+bool Console::cmdProcedureAt(int argc, const char **args) {
+	if (argc != 2) {
+		debugPrintf("usage: %s pc\n", args[0]);
+		return true;
+	}
+
+	char *end = nullptr;
+	uint32 pc = (uint32)strtoul(args[1], &end, 10);
+	if (end == nullptr || *end != '\0') {
+		debugPrintf("pc has to be an unsigned integer\n");
+		return true;
+	}
+
+	auto procedure = g_engine->script().procedureAt(pc);
+	debugPrintf("%u is part of %s\n", pc, procedure.c_str());
+	return true;
+}
+
 } // End of namespace Alcachofa
diff --git a/engines/alcachofa/console.h b/engines/alcachofa/console.h
index d5e08a961fa..e4cb24c2eae 100644
--- a/engines/alcachofa/console.h
+++ b/engines/alcachofa/console.h
@@ -65,6 +65,7 @@ private:
 	bool cmdToggleObject(int argc, const char **args);
 	bool cmdPlaySound(int argc, const char **args);
 	bool cmdDumpFile(int argc, const char **args);
+	bool cmdProcedureAt(int argc, const char **args);
 
 	bool _showGraphics = false;
 	bool _showInteractables = false;
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 3fac2bfe734..5516ec6d8d3 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -142,6 +142,30 @@ bool Script::hasProcedure(const Common::String &procedure) const {
 	return _procedures.contains(procedure);
 }
 
+String Script::procedureAt(uint32 pc) const {
+	// this method is very inefficient but it is only used for debugging
+	typedef Pair<String, uint32> Node;
+	Array<Node> sorted;
+	sorted.reserve(_procedures.size());
+	for (const auto &proc : _procedures)
+		sorted.emplace_back(proc._key, proc._value);
+	sort(sorted.begin(), sorted.end(),
+		[ ] (const Node &a, const Node &b) { return a.second < b.second; });
+
+	// next lowest procedure
+	uint min = 0, max = sorted.size() - 1;
+	while (min < max) {
+		uint center = (min + max + 1) / 2;
+		if (sorted[center].second == pc)
+			min = max = center;
+		else if (sorted[center].second > pc)
+			max = center - 1;
+		else
+			min = center;
+	}
+	return sorted[min].first;
+}
+
 struct ScriptTimerTask final : public Task {
 	ScriptTimerTask(Process &process, int32 durationSec)
 		: Task(process)
@@ -634,6 +658,21 @@ private:
 		return dynamic_cast<TObject *>(object);
 	}
 
+	const Point kInvalidPoint = { INT16_MIN, INT16_MIN };
+	Point getCamLerpTargetArg(const char *action, uint argI) {
+		auto *pointObject = getObjectArg<PointObject>(argI);
+		if (pointObject != nullptr)
+			return pointObject->position();
+
+		// in V2 a main character (we can reduce to walking character) can be used instead
+		auto *character = getObjectArg<WalkingCharacter>(argI);
+		if (character != nullptr)
+			return character->position();
+
+		pointObject = g_engine->game().unknownCamLerpTarget(action, getStringArg(argI));
+		return pointObject == nullptr ? kInvalidPoint : pointObject->position();
+	}
+
 	MainCharacter &relatedCharacter() {
 		if (g_engine->isV1()) {
 			auto character = g_engine->player().activeCharacter();
@@ -984,49 +1023,39 @@ private:
 		case ScriptKernelTask::LerpCamToObjectKeepingZ: {
 			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)
-				pointObject = g_engine->game().unknownCamLerpTarget("LerpCamToObjectKeepingZ", getStringArg(0));
-			if (pointObject == nullptr)
+			auto target = getCamLerpTargetArg("LerpCamToObjectKeepingZ", 0);
+			if (target == kInvalidPoint)
 				return TaskReturn::finish(1);
-			return TaskReturn::waitFor(g_engine->cameraV3().lerpPos(process(),
-				as2D(pointObject->position()),
-				getNumberArg(1), EasingType::Linear));
+			return TaskReturn::waitFor(
+				g_engine->cameraV3().lerpPos(process(), as2D(target), getNumberArg(1), EasingType::Linear));
 		}
 		case ScriptKernelTask::LerpCamToObjectResettingZ: {
 			if (!process().isActiveForPlayer())
 				return TaskReturn::waitFor(delay(getNumberArg(1)));
-			auto pointObject = getObjectArg<PointObject>(0);
-			if (pointObject == nullptr)
-				pointObject = g_engine->game().unknownCamLerpTarget("LerpCamToObjectResettingZ", getStringArg(0));
-			if (pointObject == nullptr)
+			auto target = getCamLerpTargetArg("LerpCamToObjectResettingZ", 0);
+			if (target == kInvalidPoint)
 				return TaskReturn::finish(1);
-			return TaskReturn::waitFor(g_engine->cameraV3().lerpPos(process(),
-				as3D(pointObject->position()),
-				getNumberArg(1), (EasingType)getNumberArg(2)));
+			return TaskReturn::waitFor(
+				g_engine->cameraV3().lerpPos(process(), as3D(target), getNumberArg(1), (EasingType)getNumberArg(2)));
 		}
 		case ScriptKernelTask::LerpCamToObjectWithScale: {
 			float targetScale = getNumberArg(1) * 0.01f;
 			if (!process().isActiveForPlayer())
 				// the scale will wait then snap the scale
 				return TaskReturn::waitFor(g_engine->cameraV3().lerpScale(process(), targetScale, getNumberArg(2), EasingType::Linear));
-			auto pointObject = getObjectArg<PointObject>(0);
-			if (pointObject == nullptr)
-				pointObject = g_engine->game().unknownCamLerpTarget("LerpCamToObjectWithScale", getStringArg(0));
-			if (pointObject == nullptr)
+			auto target = getCamLerpTargetArg("LerpCamToObjectWithScale", 0);
+			if (target == kInvalidPoint)
 				return TaskReturn::finish(1);
 			return TaskReturn::waitFor(g_engine->cameraV3().lerpPosScale(process(),
-				as3D(pointObject->position()), targetScale,
+				as3D(target), targetScale,
 				getNumberArg(2), (EasingType)getNumberArg(3), (EasingType)getNumberArg(4)));
 		}
 		case ScriptKernelTask::LerpOrSetCam: {
 			if (process().isActiveForPlayer()) {
-				auto pointObject = getObjectArg<PointObject>(0);
-				if (pointObject == nullptr)
-					pointObject = g_engine->game().unknownCamLerpTarget("LerpOrSetCam", getStringArg(0));
-				if (pointObject == nullptr)
+				auto target = getCamLerpTargetArg("LerpOrSetCam", 0);
+				if (target == kInvalidPoint)
 					return TaskReturn::finish(1);
-				g_engine->cameraV1().lerpOrSet(pointObject->position(), getNumberArg(1));
+				g_engine->cameraV1().lerpOrSet(target, getNumberArg(1));
 				// cameraV1 could also be the inherited cameraV2 here
 			}
 			return TaskReturn::finish(0);
diff --git a/engines/alcachofa/script.h b/engines/alcachofa/script.h
index 1d388705db9..d57ea91a888 100644
--- a/engines/alcachofa/script.h
+++ b/engines/alcachofa/script.h
@@ -121,7 +121,7 @@ enum class ScriptKernelTask {
 	LerpCamToObjectWithScale,
 	LerpCamToObjectResettingZ,
 	LerpCamRotation,
-	LerpOrSetCam, // only V1
+	LerpOrSetCam, // only V1 and V2
 	FadeIn,
 	FadeOut,
 	FadeIn2,
@@ -170,6 +170,7 @@ public:
 		ScriptFlags flags = ScriptFlags::None);
 	bool hasProcedure(const Common::String &behavior, const Common::String &action) const;
 	bool hasProcedure(const Common::String &procedure) const;
+	Common::String procedureAt(uint32 pc) const;
 
 	using VariableNameIterator = Common::HashMap<Common::String, uint32>::const_iterator;
 	inline VariableNameIterator beginVariables() const { return _variableNames.begin(); }


Commit: c21da18d93d618385a91c97c6984e90076685ed3
    https://github.com/scummvm/scummvm/commit/c21da18d93d618385a91c97c6984e90076685ed3
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:30+02:00

Commit Message:
ALCACHOFA: Remove ScriptKernelTask::ChangeDoor

Turns out this kernel task was always a NOOP and is not used in any game

Changed paths:
    engines/alcachofa/game-v1.cpp
    engines/alcachofa/game-v2.cpp
    engines/alcachofa/script-debug.h
    engines/alcachofa/script.h


diff --git a/engines/alcachofa/game-v1.cpp b/engines/alcachofa/game-v1.cpp
index 951119522e0..4f056d77e21 100644
--- a/engines/alcachofa/game-v1.cpp
+++ b/engines/alcachofa/game-v1.cpp
@@ -90,7 +90,7 @@ static constexpr const ScriptKernelTask kScriptKernelTaskMap[] = {
 	ScriptKernelTask::ChangeCharacter,
 	ScriptKernelTask::LerpOrSetCam,
 	ScriptKernelTask::Drop,
-	ScriptKernelTask::ChangeDoor,
+	ScriptKernelTask::Nop,
 	ScriptKernelTask::Disguise,
 	ScriptKernelTask::ToggleRoomFloor,
 	ScriptKernelTask::SetDialogLineReturn,
diff --git a/engines/alcachofa/game-v2.cpp b/engines/alcachofa/game-v2.cpp
index a8bd65ce7a8..3ebeed161be 100644
--- a/engines/alcachofa/game-v2.cpp
+++ b/engines/alcachofa/game-v2.cpp
@@ -93,7 +93,7 @@ static constexpr const ScriptKernelTask kScriptKernelTaskMap[] = {
 	ScriptKernelTask::LerpOrSetCam,
 	ScriptKernelTask::Drop,
 	ScriptKernelTask::CharacterDrop,
-	ScriptKernelTask::ChangeDoor,
+	ScriptKernelTask::Nop,
 	ScriptKernelTask::Disguise,
 	ScriptKernelTask::ToggleRoomFloor,
 	ScriptKernelTask::SetDialogLineReturn,
diff --git a/engines/alcachofa/script-debug.h b/engines/alcachofa/script-debug.h
index a908ea4f320..96af847cfbc 100644
--- a/engines/alcachofa/script-debug.h
+++ b/engines/alcachofa/script-debug.h
@@ -115,8 +115,6 @@ static const char *const KernelCallNames[] = {
 	"FadeOut2",
 	"LerpCamXYZ",
 	"LerpCamToObjectKeepingZ",
-
-	"ChangeDoor",
 	"Disguise"
 };
 
diff --git a/engines/alcachofa/script.h b/engines/alcachofa/script.h
index d57ea91a888..378a3391a36 100644
--- a/engines/alcachofa/script.h
+++ b/engines/alcachofa/script.h
@@ -128,8 +128,6 @@ enum class ScriptKernelTask {
 	FadeOut2,
 	LerpCamXYZ,
 	LerpCamToObjectKeepingZ,
-
-	ChangeDoor, ///< NOOP 
 	Disguise
 };
 


Commit: a5bd081174c97297d9115a5dce21d754d25de198
    https://github.com/scummvm/scummvm/commit/a5bd081174c97297d9115a5dce21d754d25de198
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:30+02:00

Commit Message:
ALCACHOFA: V2: Fix loading saves after teleporting

Changed paths:
    engines/alcachofa/player.cpp


diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 745c140d715..00017417cfc 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -377,9 +377,10 @@ void Player::syncGame(Serializer &s) {
 
 	String roomName;
 	if (s.isSaving()) {
+		bool isInInventory = currentRoom() == &g_engine->world().inventory();
 		roomName =
 			g_engine->menu().isOpen() ? g_engine->menu().previousRoom()->name() // save from in-game menu
-			: _roomBeforeInventory != nullptr ? _roomBeforeInventory->name() // save from ScummVM while in inventory
+			: isInInventory && _roomBeforeInventory != nullptr ? _roomBeforeInventory->name() // save from ScummVM while in inventory
 			: currentRoom()->name(); // save from ScumnmVM global menu or autosave in normal gameplay
 	}
 	s.syncString(roomName);


Commit: 639847ad8ca48f5797ef4d178673f61857771d7a
    https://github.com/scummvm/scummvm/commit/639847ad8ca48f5797ef4d178673f61857771d7a
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-03-31T17:22:30+02:00

Commit Message:
ALCACHOFA: V2: Fix pickup script from non-char process

Changed paths:
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 5516ec6d8d3..f082b29e2ed 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -951,7 +951,12 @@ private:
 
 		// Inventory control
 		case ScriptKernelTask::Pickup:
-			relatedCharacter().pickup(getStringArg(0), !getNumberArg(1));
+			if (process().character() == MainCharacterKind::None) {
+				// This happens in Secta, the original game just ignores this case
+				// A ChangeCharacter(0) is executed right before so this is actually just a script bug
+				warning("Tried to drop from none-character-process: %s at %u", getStringArg(0), _pc);
+			} else
+				relatedCharacter().pickup(getStringArg(0), !getNumberArg(1));
 			return TaskReturn::finish(1);
 		case ScriptKernelTask::CharacterPickup: {
 			auto &character = g_engine->world().getMainCharacterByKind(getMainCharacterKindArg(1));




More information about the Scummvm-git-logs mailing list